From 7d202681e9c61467283fe12b9e8158ba4bd75b45 Mon Sep 17 00:00:00 2001 From: Elijah Stephenson Date: Wed, 24 Sep 2025 14:46:30 -0500 Subject: [PATCH 1/7] Optimize menu rendering with entity caching and fix slime/magma cube sizing - Add EntityRenderState caching to prevent visual reloading on menu open - Cache render states with proper scale (1.0F) to avoid double scaling - Fix slimes and magma cubes by setting size to 1 (smallest variant) - Disable AI and animations for consistent static rendering - Create fresh entities each time to avoid state corruption - Maintain caching benefits while fixing size and animation issues This eliminates the visual bug where mobs would initially load on default skeletons and gradually load correct models, taking up to 10 seconds for long lists of unlocked mobs in creative mode. --- .../tocraft/remorphed/RemorphedClient.java | 19 ++- .../remorphed/command/RemorphedCommand.java | 1 + .../remorphed/screen/RemorphedMenu.java | 130 ++++++++++++++++-- .../remorphed/screen/widget/EntityWidget.java | 15 +- .../remorphed/screen/widget/SkinWidget.java | 19 ++- 5 files changed, 162 insertions(+), 22 deletions(-) diff --git a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java index b3e6732..770e683 100644 --- a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java +++ b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java @@ -49,12 +49,21 @@ public static void renderEntityInInventory( Vector3f translation, Quaternionf rotation, @Nullable Quaternionf overrideCameraAngle, - LivingEntity entity + LivingEntity entity, + @Nullable EntityRenderState cachedRenderState ) { - EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); - EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(entity); - EntityRenderState entityRenderState = entityRenderer.createRenderState(entity, 1.0F); - entityRenderState.hitboxesRenderState = null; + EntityRenderState entityRenderState; + + if (cachedRenderState != null) { + // Use cached render state to avoid texture/model reloading + entityRenderState = cachedRenderState; + } else { + // Fallback: create new render state (shouldn't happen with proper caching) + EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(entity); + entityRenderState = entityRenderer.createRenderState(entity, 1.0F); + entityRenderState.hitboxesRenderState = null; + } GuiGraphicsAccessor accessor = ((GuiGraphicsAccessor) guiGraphics); accessor.getGuiRenderState().submitPicturesInPictureState(new GuiShapeRenderState(id, entityRenderState, translation, rotation, overrideCameraAngle, x1, y1, x2, y2, scale, accessor.getScissorStack().peek())); diff --git a/common/src/main/java/dev/tocraft/remorphed/command/RemorphedCommand.java b/common/src/main/java/dev/tocraft/remorphed/command/RemorphedCommand.java index e4a020c..2934bcc 100644 --- a/common/src/main/java/dev/tocraft/remorphed/command/RemorphedCommand.java +++ b/common/src/main/java/dev/tocraft/remorphed/command/RemorphedCommand.java @@ -362,6 +362,7 @@ public void register(CommandDispatcher dispatcher, CommandBu rootNode.addChild(hasSkin); } + dispatcher.getRoot().addChild(rootNode); } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java index 71f8fe9..47e1ebd 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java @@ -22,12 +22,17 @@ import net.minecraft.client.gui.layouts.LinearLayout; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.MagmaCube; +import net.minecraft.world.entity.monster.Slime; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -49,6 +54,11 @@ public class RemorphedMenu extends Screen { private final Map, Mob> renderEntities = new ConcurrentHashMap<>(); private final Map renderPlayers = new ConcurrentHashMap<>(); + // Cache for EntityRenderState with proper scale - this is what prevents visual reloading + private static final Map, EntityRenderState> CACHED_ENTITY_RENDER_STATES = new ConcurrentHashMap<>(); + private static final Map CACHED_PLAYER_RENDER_STATES = new ConcurrentHashMap<>(); + + private final SearchWidget searchBar = createSearchBar(); private final Button helpButton = createHelpButton(); private final PlayerWidget playerButton = createPlayerButton(); @@ -202,6 +212,7 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull if (fakePlayer != null) { boolean bl = Objects.equals(SkinShifter.getCurrentSkin(minecraft.player), skinProfile.getId()) && currentType == null; if (bl) currentRow = i; + EntityRenderState cachedPlayerRenderState = CACHED_PLAYER_RENDER_STATES.get(skinProfile); row.add(new SkinWidget( 0, 0, @@ -212,7 +223,8 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull this, PlayerMorph.getFavoriteSkins(minecraft.player).contains(skinProfile), bl, - Remorphed.canUseEveryShape(minecraft.player) || Remorphed.CONFIG.playerKillValue < 1 ? -1 : Remorphed.CONFIG.playerKillValue * PlayerMorph.getPlayerKills(minecraft.player, skinProfile.getId()) - PlayerMorph.getCounter(minecraft.player, skinProfile.getId()) + Remorphed.canUseEveryShape(minecraft.player) || Remorphed.CONFIG.playerKillValue < 1 ? -1 : Remorphed.CONFIG.playerKillValue * PlayerMorph.getPlayerKills(minecraft.player, skinProfile.getId()) - PlayerMorph.getCounter(minecraft.player, skinProfile.getId()), + cachedPlayerRenderState )); } else { Remorphed.LOGGER.error("invalid skin profile: {}", skinProfile); @@ -223,6 +235,7 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull if (entity != null) { boolean bl = type.equals(currentType); if (bl) currentRow = i; + EntityRenderState cachedRenderState = CACHED_ENTITY_RENDER_STATES.get(type); row.add(new EntityWidget<>( i * Remorphed.CONFIG.shapes_per_row + j, 0, @@ -234,7 +247,8 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull this, PlayerMorph.getFavoriteShapes(minecraft.player).contains(type), bl, - Remorphed.canUseEveryShape(minecraft.player) || Remorphed.getKillValue(type.getEntityType()) < 1 ? -1 : Remorphed.getKillValue(type.getEntityType()) * PlayerMorph.getKills(minecraft.player, type) - PlayerMorph.getCounter(minecraft.player, type) + Remorphed.canUseEveryShape(minecraft.player) || Remorphed.getKillValue(type.getEntityType()) < 1 ? -1 : Remorphed.getKillValue(type.getEntityType()) * PlayerMorph.getKills(minecraft.player, type) - PlayerMorph.getCounter(minecraft.player, type), + cachedRenderState )); } else { Remorphed.LOGGER.error("invalid shape type: {}", type.getEntityType().getDescriptionId()); @@ -256,11 +270,64 @@ public synchronized void populateUnlockedRenderEntities(Player player) { unlockedShapes.clear(); renderEntities.clear(); List> validUnlocked = Remorphed.getUnlockedShapes(player); + + + + // Create new entities and render states only for newly unlocked shapes + for (ShapeType type : validUnlocked) { + if (!CACHED_ENTITY_RENDER_STATES.containsKey(type)) { + try { + Entity entity = type.create(Minecraft.getInstance().level, player); + if (entity instanceof Mob living) { + // Fix slimes and magma cubes - set size to 1 (smallest) + if (living instanceof Slime slime) { + slime.setSize(1, true); + } else if (living instanceof MagmaCube magmaCube) { + magmaCube.setSize(1, true); + } + + // Disable animations for consistent rendering + living.setNoAi(true); + living.setInvulnerable(true); + + // Create and cache the EntityRenderState with 1.0F scale (like original) + // The actual scaling is handled by the widget's size calculation + EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(living); + EntityRenderState entityRenderState = entityRenderer.createRenderState(living, 1.0F); + entityRenderState.hitboxesRenderState = null; + CACHED_ENTITY_RENDER_STATES.put(type, entityRenderState); + + // Don't cache the entity - create fresh each time to avoid state issues + renderEntities.put(type, living); + } + } catch (Exception e) { + Remorphed.LOGGER.warn("Failed to create entity for type {}: {}", type.getEntityType(), e.getMessage()); + } + } else { + // Create fresh entity but use cached render state + Entity entity = type.create(Minecraft.getInstance().level, player); + if (entity instanceof Mob living) { + // Fix slimes and magma cubes - set size to 1 (smallest) + if (living instanceof Slime slime) { + slime.setSize(1, true); + } else if (living instanceof MagmaCube magmaCube) { + magmaCube.setSize(1, true); + } + + // Disable animations for consistent rendering + living.setNoAi(true); + living.setInvulnerable(true); + + renderEntities.put(type, living); + } + } + } + + // Add all valid unlocked shapes for (ShapeType type : validUnlocked) { - Entity entity = type.create(Minecraft.getInstance().level, player); - if (entity instanceof Mob living) { + if (CACHED_ENTITY_RENDER_STATES.containsKey(type)) { unlockedShapes.add(type); - renderEntities.put(type, living); } } } @@ -269,18 +336,61 @@ public synchronized void populateUnlockedRenderPlayers(Player player) { unlockedSkins.clear(); renderPlayers.clear(); List validUnlocked = Remorphed.getUnlockedSkins(player); - for (GameProfile profile : validUnlocked) { - if (profile.getId() != player.getUUID()) { - FakeClientPlayer entity = null; + + // Filter out the player's own skin + List filteredUnlocked = validUnlocked.stream() + .filter(profile -> profile.getId() != player.getUUID()) + .toList(); + + // Create new fake players and render states only for newly unlocked skins + for (GameProfile profile : filteredUnlocked) { + if (!CACHED_PLAYER_RENDER_STATES.containsKey(profile)) { + try { + if (minecraft != null) { + FakeClientPlayer entity = new FakeClientPlayer(minecraft.level, profile); + + // Create and cache the EntityRenderState with 1.0F scale (like original) + // The actual scaling is handled by the widget's size calculation + EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(entity); + EntityRenderState entityRenderState = entityRenderer.createRenderState(entity, 1.0F); + entityRenderState.hitboxesRenderState = null; + CACHED_PLAYER_RENDER_STATES.put(profile, entityRenderState); + + // Don't cache the player - create fresh each time to avoid state issues + renderPlayers.put(profile, entity); + } + } catch (Exception e) { + Remorphed.LOGGER.warn("Failed to create fake player for profile {}: {}", profile.getName(), e.getMessage()); + } + } else { + // Create fresh player but use cached render state if (minecraft != null) { - entity = new FakeClientPlayer(minecraft.level, profile); + FakeClientPlayer entity = new FakeClientPlayer(minecraft.level, profile); + renderPlayers.put(profile, entity); } + } + } + + // Add all valid unlocked skins + for (GameProfile profile : filteredUnlocked) { + if (CACHED_PLAYER_RENDER_STATES.containsKey(profile)) { unlockedSkins.add(profile); - renderPlayers.put(profile, entity); } } } + /** + * Clears the entity and player caches. Call this when the player logs out + * or when you want to force a complete refresh of all entities. + */ + public static void clearCache() { + CACHED_ENTITY_RENDER_STATES.clear(); + CACHED_PLAYER_RENDER_STATES.clear(); + Remorphed.LOGGER.info("Cleared render state caches"); + } + + protected void addFooter() { this.layout.addToFooter(Button.builder(CommonComponents.GUI_DONE, (button) -> this.onClose()).width(200).build()); } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java b/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java index 623abad..66f84af 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java @@ -16,9 +16,11 @@ import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.LivingEntity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -32,13 +34,20 @@ public class EntityWidget extends ShapeWidget { private final T entity; private final int size; private final int id; + private final EntityRenderState cachedRenderState; - public EntityWidget(int id, int x, int y, int width, int height, ShapeType type, @NotNull T entity, Screen parent, boolean isFavorite, boolean current, int availability) { + public EntityWidget(int id, int x, int y, int width, int height, ShapeType type, @NotNull T entity, Screen parent, boolean isFavorite, boolean current, int availability, @Nullable EntityRenderState cachedRenderState) { super(x, y, width, height, parent, isFavorite, current, availability); // int x, int y, int width, int height, message - this.size = (int) (Remorphed.CONFIG.entity_size * (1 / (Math.max(entity.getBbHeight(), entity.getBbWidth())))); + // Calculate size with cap for small entities like slimes and magma cubes + float entitySize = Math.max(entity.getBbHeight(), entity.getBbWidth()); + float scaleFactor = 1 / entitySize; + // Cap the scale factor to prevent slimes/magma cubes from being too big + scaleFactor = Math.min(scaleFactor, 2.0f); + this.size = (int) (Remorphed.CONFIG.entity_size * scaleFactor); this.type = type; this.entity = entity; this.id = id; + this.cachedRenderState = cachedRenderState; // Use cached render state with proper scale entity.setGlowingTag(true); setTooltip(Tooltip.create(ShapeType.createTooltipText(entity))); } @@ -93,7 +102,7 @@ protected void renderShape(GuiGraphics guiGraphics) { int l = topPos - 25; int m = leftPos + 20; int n = topPos + 35; - RemorphedClient.renderEntityInInventory(id, guiGraphics, k, l, m, n, size, new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), null, entity); + RemorphedClient.renderEntityInInventory(id, guiGraphics, k, l, m, n, size, new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), null, entity, cachedRenderState); } catch (Exception e) { Remorphed.LOGGER.error("Error while rendering {}", ShapeType.createTooltipText(entity).getString(), e); setCrashed(); diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java b/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java index b3e2bbe..7c97f98 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java @@ -2,6 +2,7 @@ import com.mojang.authlib.GameProfile; import dev.tocraft.remorphed.Remorphed; +import dev.tocraft.remorphed.RemorphedClient; import dev.tocraft.remorphed.impl.FakeClientPlayer; import dev.tocraft.remorphed.network.NetworkHandler; import dev.tocraft.walkers.api.PlayerShape; @@ -12,10 +13,11 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -24,12 +26,19 @@ public class SkinWidget extends ShapeWidget { private final GameProfile skin; private final FakeClientPlayer fakePlayer; private final int size; + private final EntityRenderState cachedRenderState; - public SkinWidget(int x, int y, int width, int height, @NotNull GameProfile skin, @NotNull FakeClientPlayer fakePlayer, Screen parent, boolean isFavorite, boolean isCurrent, int availability) { + public SkinWidget(int x, int y, int width, int height, @NotNull GameProfile skin, @NotNull FakeClientPlayer fakePlayer, Screen parent, boolean isFavorite, boolean isCurrent, int availability, @Nullable EntityRenderState cachedRenderState) { super(x, y, width, height, parent, isFavorite, isCurrent, availability); - this.size = (int) (Remorphed.CONFIG.entity_size * (1 / (Math.max(fakePlayer.getBbHeight(), fakePlayer.getBbWidth())))); + // Calculate size with cap for small entities like slimes and magma cubes + float entitySize = Math.max(fakePlayer.getBbHeight(), fakePlayer.getBbWidth()); + float scaleFactor = 1 / entitySize; + // Cap the scale factor to prevent slimes/magma cubes from being too big + scaleFactor = Math.min(scaleFactor, 2.0f); + this.size = (int) (Remorphed.CONFIG.entity_size * scaleFactor); this.skin = skin; this.fakePlayer = fakePlayer; + this.cachedRenderState = cachedRenderState; // Use cached render state with proper scale setTooltip(Tooltip.create(Component.literal(skin.getName()))); } @@ -56,7 +65,9 @@ protected void renderShape(GuiGraphics guiGraphics) { int l = topPos - 25; int m = leftPos + 20; int n = topPos + 35; - InventoryScreen.renderEntityInInventory(guiGraphics, k, l, m, n, size, new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), null, fakePlayer); + // Use a unique ID for each skin widget (based on skin UUID hash) + int id = skin.getId().hashCode(); + RemorphedClient.renderEntityInInventory(id, guiGraphics, k, l, m, n, size, new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), null, fakePlayer, cachedRenderState); } } From 32b86de1e63c36f8e9618659a97ac71354fec725 Mon Sep 17 00:00:00 2001 From: To_Craft Date: Fri, 10 Oct 2025 13:44:59 +0200 Subject: [PATCH 2/7] fix duplicate code for magma cubes (they're instances of Slime) --- .../src/main/java/dev/tocraft/remorphed/RemorphedClient.java | 2 ++ .../java/dev/tocraft/remorphed/screen/RemorphedMenu.java | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java index 770e683..c3c6301 100644 --- a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java +++ b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java @@ -7,6 +7,7 @@ import dev.tocraft.remorphed.handler.client.ClientPlayerRespawnHandler; import dev.tocraft.remorphed.mixin.client.accessor.GuiGraphicsAccessor; import dev.tocraft.remorphed.network.ClientNetworking; +import dev.tocraft.remorphed.screen.RemorphedMenu; import dev.tocraft.remorphed.screen.render.GuiShapeRenderState; import dev.tocraft.remorphed.tick.KeyPressHandler; import net.fabricmc.api.EnvType; @@ -36,6 +37,7 @@ public void initialize() { ClientNetworking.registerPacketHandlers(); ClientPlayerEvents.CLIENT_PLAYER_RESPAWN.register(new ClientPlayerRespawnHandler()); + ClientPlayerEvents.CLIENT_PLAYER_QUIT.register(player -> RemorphedMenu.clearCache()); // clear cache on world quit } public static void renderEntityInInventory( diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java index 47e1ebd..e06fee9 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java @@ -31,7 +31,6 @@ import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; -import net.minecraft.world.entity.monster.MagmaCube; import net.minecraft.world.entity.monster.Slime; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.Contract; @@ -282,8 +281,6 @@ public synchronized void populateUnlockedRenderEntities(Player player) { // Fix slimes and magma cubes - set size to 1 (smallest) if (living instanceof Slime slime) { slime.setSize(1, true); - } else if (living instanceof MagmaCube magmaCube) { - magmaCube.setSize(1, true); } // Disable animations for consistent rendering @@ -311,8 +308,6 @@ public synchronized void populateUnlockedRenderEntities(Player player) { // Fix slimes and magma cubes - set size to 1 (smallest) if (living instanceof Slime slime) { slime.setSize(1, true); - } else if (living instanceof MagmaCube magmaCube) { - magmaCube.setSize(1, true); } // Disable animations for consistent rendering From c1bdfa2ddb725b99036667a9f64750620239405b Mon Sep 17 00:00:00 2001 From: To_Craft Date: Fri, 10 Oct 2025 13:51:44 +0200 Subject: [PATCH 3/7] optimize menu rendering by FugLong --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df83b2..ea86383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ remorphed 7.1.0 - fix broken counter in menu - add delete functionality to Menu - press "X" or mouse-scroll-wheel to delete - fix duplicate entities in menu +- optimize menu rendering by FugLong remorphed 7.0 ================ From ac1177a2df62235347879b59076de4a2826eed96 Mon Sep 17 00:00:00 2001 From: Elijah Stephenson Date: Sun, 16 Nov 2025 15:54:26 -0600 Subject: [PATCH 4/7] Caching refactor, bugs fixed --- .../tocraft/remorphed/RemorphedClient.java | 24 +- .../client/ClientDisconnectHandler.java | 26 ++ .../client/EntityRenderCacheHandler.java | 63 +++++ .../remorphed/screen/EntityRenderCache.java | 256 ++++++++++++++++++ .../remorphed/screen/RemorphedMenu.java | 142 +++------- .../remorphed/screen/widget/EntityWidget.java | 19 +- .../remorphed/screen/widget/SkinWidget.java | 16 +- 7 files changed, 404 insertions(+), 142 deletions(-) create mode 100644 common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java create mode 100644 common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java create mode 100644 common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java diff --git a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java index c3c6301..61d0628 100644 --- a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java +++ b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java @@ -4,7 +4,9 @@ import dev.tocraft.craftedcore.event.client.ClientPlayerEvents; import dev.tocraft.craftedcore.event.client.ClientTickEvents; import dev.tocraft.craftedcore.registration.KeyBindingRegistry; +import dev.tocraft.remorphed.handler.client.ClientDisconnectHandler; import dev.tocraft.remorphed.handler.client.ClientPlayerRespawnHandler; +import dev.tocraft.remorphed.handler.client.EntityRenderCacheHandler; import dev.tocraft.remorphed.mixin.client.accessor.GuiGraphicsAccessor; import dev.tocraft.remorphed.network.ClientNetworking; import dev.tocraft.remorphed.screen.RemorphedMenu; @@ -34,10 +36,11 @@ public void initialize() { // Register event handlers ClientTickEvents.CLIENT_PRE.register(new KeyPressHandler()); + ClientTickEvents.CLIENT_PRE.register(new ClientDisconnectHandler()); + ClientTickEvents.CLIENT_PRE.register(new EntityRenderCacheHandler()); ClientNetworking.registerPacketHandlers(); ClientPlayerEvents.CLIENT_PLAYER_RESPAWN.register(new ClientPlayerRespawnHandler()); - ClientPlayerEvents.CLIENT_PLAYER_QUIT.register(player -> RemorphedMenu.clearCache()); // clear cache on world quit } public static void renderEntityInInventory( @@ -51,21 +54,12 @@ public static void renderEntityInInventory( Vector3f translation, Quaternionf rotation, @Nullable Quaternionf overrideCameraAngle, - LivingEntity entity, - @Nullable EntityRenderState cachedRenderState + LivingEntity entity ) { - EntityRenderState entityRenderState; - - if (cachedRenderState != null) { - // Use cached render state to avoid texture/model reloading - entityRenderState = cachedRenderState; - } else { - // Fallback: create new render state (shouldn't happen with proper caching) - EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); - EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(entity); - entityRenderState = entityRenderer.createRenderState(entity, 1.0F); - entityRenderState.hitboxesRenderState = null; - } + EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(entity); + EntityRenderState entityRenderState = entityRenderer.createRenderState(entity, 1.0F); + entityRenderState.hitboxesRenderState = null; GuiGraphicsAccessor accessor = ((GuiGraphicsAccessor) guiGraphics); accessor.getGuiRenderState().submitPicturesInPictureState(new GuiShapeRenderState(id, entityRenderState, translation, rotation, overrideCameraAngle, x1, y1, x2, y2, scale, accessor.getScissorStack().peek())); diff --git a/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java b/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java new file mode 100644 index 0000000..57c511c --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java @@ -0,0 +1,26 @@ +package dev.tocraft.remorphed.handler.client; + +import dev.tocraft.craftedcore.event.client.ClientTickEvents; +import dev.tocraft.remorphed.Remorphed; +import dev.tocraft.remorphed.screen.EntityRenderCache; +import net.minecraft.client.Minecraft; + +/** + * Handles clearing the entity render cache when the player disconnects from a world. + */ +public class ClientDisconnectHandler implements ClientTickEvents.Client { + + private boolean wasInWorld = false; + + @Override + public void tick(Minecraft client) { + boolean isInWorld = client.level != null && client.player != null; + + // Detect transition from in-world to not-in-world (disconnect) + if (wasInWorld && !isInWorld) { + EntityRenderCache.clearCache(); + } + + wasInWorld = isInWorld; + } +} diff --git a/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java b/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java new file mode 100644 index 0000000..0452a2d --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java @@ -0,0 +1,63 @@ +package dev.tocraft.remorphed.handler.client; + +import dev.tocraft.craftedcore.event.client.ClientTickEvents; +import dev.tocraft.remorphed.Remorphed; +import dev.tocraft.remorphed.screen.EntityRenderCache; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; + +/** + * Handles pre-loading entity render states when the player joins a world. + * This ensures the menu opens instantly without visual loading delays. + */ +public class EntityRenderCacheHandler implements ClientTickEvents.Client { + + private boolean wasInWorld = false; + private boolean hasPreloaded = false; + private int ticksInWorld = 0; + + @Override + public void tick(Minecraft client) { + boolean isInWorld = client.level != null && client.player != null; + + // Detect transition from not-in-world to in-world (world join) + if (!wasInWorld && isInWorld) { + // Reset state for new world + hasPreloaded = false; + ticksInWorld = 0; + } + + // Pre-load after being in world for a few ticks (ensures world is fully loaded) + if (isInWorld && !hasPreloaded) { + ticksInWorld++; + + // Wait 20 ticks (1 second) after joining to ensure everything is loaded + if (ticksInWorld >= 20) { + hasPreloaded = true; + preloadForPlayer(client.player); + } + } + + wasInWorld = isInWorld; + } + + /** + * Pre-loads all entity and player skin render states for the given player. + * This runs on the main client thread. + * + * @param player The player to pre-load entities for + */ + private void preloadForPlayer(LocalPlayer player) { + if (player == null) { + return; + } + + try { + // Pre-load entities and player skins on main thread + EntityRenderCache.preloadEntities(player); + EntityRenderCache.preloadPlayerSkins(player); + } catch (Exception e) { + Remorphed.LOGGER.error("[Remorphed] Failed to pre-load entity render cache", e); + } + } +} diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java b/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java new file mode 100644 index 0000000..caad94c --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java @@ -0,0 +1,256 @@ +package dev.tocraft.remorphed.screen; + +import com.mojang.authlib.GameProfile; +import dev.tocraft.remorphed.Remorphed; +import dev.tocraft.remorphed.impl.FakeClientPlayer; +import dev.tocraft.walkers.api.variant.ShapeType; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.MagmaCube; +import net.minecraft.world.entity.monster.Slime; +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages pre-loading and caching of entity render states for the Remorphed menu. + * This prevents the visual loading delay when opening the menu for the first time. + * + * The cache creates isolated, static snapshots of entity render states that won't be + * affected by real-world entities near the player. + */ +@Environment(EnvType.CLIENT) +public class EntityRenderCache { + + // Static caches for entity and player render states + private static final Map, CachedEntityData> ENTITY_CACHE = new ConcurrentHashMap<>(); + private static final Map PLAYER_CACHE = new ConcurrentHashMap<>(); + + /** + * Data container for cached entity information + * NOTE: We store the actual entity, NOT the EntityRenderState, because + * EntityRenderState objects are mutable and get updated by the rendering system. + */ + public static class CachedEntityData { + public final LivingEntity entity; + + public CachedEntityData(LivingEntity entity) { + this.entity = entity; + } + } + + /** + * Pre-loads all unlocked entity shapes for the given player. + * This should be called when the player joins a world. + * + * @param player The player whose unlocked shapes to cache + */ + public static void preloadEntities(Player player) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return; + } + + List> unlockedShapes = Remorphed.getUnlockedShapes(player); + + for (ShapeType type : unlockedShapes) { + // Skip if already cached + if (ENTITY_CACHE.containsKey(type)) { + continue; + } + + try { + // Create isolated entity instance + Entity entity = type.create(minecraft.level, player); + + if (entity instanceof Mob mob) { + // Make entity completely static and isolated + prepareStaticEntity(mob); + + // Cache the ENTITY itself (not render state!) + ENTITY_CACHE.put(type, new CachedEntityData(mob)); + } + } catch (Exception e) { + Remorphed.LOGGER.warn("[Remorphed] Failed to pre-load entity for type {}: {}", + type.getEntityType().getDescriptionId(), e.getMessage()); + } + } + } + + /** + * Pre-loads all unlocked player skins for the given player. + * This should be called when the player joins a world. + * + * @param player The player whose unlocked skins to cache + */ + public static void preloadPlayerSkins(Player player) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return; + } + + List unlockedSkins = Remorphed.getUnlockedSkins(player); + + for (GameProfile profile : unlockedSkins) { + // Skip player's own skin + if (profile.getId().equals(player.getUUID())) { + continue; + } + + // Skip if already cached + if (PLAYER_CACHE.containsKey(profile)) { + continue; + } + + try { + // Create isolated fake player instance + FakeClientPlayer fakePlayer = new FakeClientPlayer(minecraft.level, profile); + + // Make player invulnerable (no setNoAi for player entities) + fakePlayer.setInvulnerable(true); + + // Cache the ENTITY itself (not render state!) + PLAYER_CACHE.put(profile, new CachedEntityData(fakePlayer)); + } catch (Exception e) { + Remorphed.LOGGER.warn("[Remorphed] Failed to pre-load player skin for profile {}: {}", + profile.getName(), e.getMessage()); + } + } + } + + /** + * Prepares an entity to be completely static and isolated from world state. + * + * @param mob The mob to prepare + */ + private static void prepareStaticEntity(Mob mob) { + // Disable AI and make invulnerable to prevent any updates + mob.setNoAi(true); + mob.setInvulnerable(true); + + // Fix slimes and magma cubes - set to smallest size + if (mob instanceof Slime slime) { + slime.setSize(1, true); + } else if (mob instanceof MagmaCube magmaCube) { + magmaCube.setSize(1, true); + } + + // Enable glowing effect for menu display + mob.setGlowingTag(true); + } + + /** + * Gets the cached render data for an entity type. + * If not cached, returns null. + * + * @param type The shape type to get cached data for + * @return The cached data, or null if not cached + */ + @Nullable + public static CachedEntityData getCachedEntity(ShapeType type) { + return ENTITY_CACHE.get(type); + } + + /** + * Gets the cached render data for a player skin. + * If not cached, returns null. + * + * @param profile The game profile to get cached data for + * @return The cached data, or null if not cached + */ + @Nullable + public static CachedEntityData getCachedPlayerSkin(GameProfile profile) { + return PLAYER_CACHE.get(profile); + } + + /** + * Caches a single entity type on-demand. + * Used when a player unlocks a new shape while in-world. + * + * @param type The shape type to cache + * @param player The player context + */ + public static void cacheEntity(ShapeType type, Player player) { + if (ENTITY_CACHE.containsKey(type)) { + return; // Already cached + } + + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return; + } + + try { + Entity entity = type.create(minecraft.level, player); + + if (entity instanceof Mob mob) { + prepareStaticEntity(mob); + ENTITY_CACHE.put(type, new CachedEntityData(mob)); + Remorphed.LOGGER.debug("[Remorphed] Cached entity on-demand: {}", type.getEntityType().getDescriptionId()); + } + } catch (Exception e) { + Remorphed.LOGGER.warn("[Remorphed] Failed to cache entity on-demand: {}", type.getEntityType().getDescriptionId(), e); + } + } + + /** + * Caches a single player skin on-demand. + * Used when a player unlocks a new skin while in-world. + * + * @param profile The game profile to cache + */ + public static void cachePlayerSkin(GameProfile profile) { + if (PLAYER_CACHE.containsKey(profile)) { + return; // Already cached + } + + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return; + } + + try { + FakeClientPlayer fakePlayer = new FakeClientPlayer(minecraft.level, profile); + fakePlayer.setInvulnerable(true); + + PLAYER_CACHE.put(profile, new CachedEntityData(fakePlayer)); + Remorphed.LOGGER.debug("[Remorphed] Cached player skin on-demand: {}", profile.getName()); + } catch (Exception e) { + Remorphed.LOGGER.warn("[Remorphed] Failed to cache player skin on-demand: {}", profile.getName(), e); + } + } + + /** + * Clears all cached entities. + * This should be called when the player disconnects from a world. + */ + public static void clearCache() { + ENTITY_CACHE.clear(); + PLAYER_CACHE.clear(); + } + + /** + * Gets the number of cached entities. + * + * @return The cache size + */ + public static int getCachedEntityCount() { + return ENTITY_CACHE.size(); + } + + /** + * Gets the number of cached player skins. + * + * @return The cache size + */ + public static int getCachedPlayerSkinCount() { + return PLAYER_CACHE.size(); + } +} diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java index e06fee9..8dc02d9 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java @@ -7,6 +7,7 @@ import dev.tocraft.remorphed.impl.PlayerMorph; import dev.tocraft.remorphed.mixin.client.accessor.ScreenAccessor; import dev.tocraft.remorphed.screen.widget.*; +import dev.tocraft.remorphed.screen.EntityRenderCache; import dev.tocraft.skinshifter.SkinShifter; import dev.tocraft.walkers.Walkers; import dev.tocraft.walkers.api.PlayerShape; @@ -22,9 +23,6 @@ import net.minecraft.client.gui.layouts.LinearLayout; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.player.AbstractClientPlayer; -import net.minecraft.client.renderer.entity.EntityRenderDispatcher; -import net.minecraft.client.renderer.entity.EntityRenderer; -import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.Entity; @@ -53,10 +51,6 @@ public class RemorphedMenu extends Screen { private final Map, Mob> renderEntities = new ConcurrentHashMap<>(); private final Map renderPlayers = new ConcurrentHashMap<>(); - // Cache for EntityRenderState with proper scale - this is what prevents visual reloading - private static final Map, EntityRenderState> CACHED_ENTITY_RENDER_STATES = new ConcurrentHashMap<>(); - private static final Map CACHED_PLAYER_RENDER_STATES = new ConcurrentHashMap<>(); - private final SearchWidget searchBar = createSearchBar(); private final Button helpButton = createHelpButton(); @@ -211,19 +205,17 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull if (fakePlayer != null) { boolean bl = Objects.equals(SkinShifter.getCurrentSkin(minecraft.player), skinProfile.getId()) && currentType == null; if (bl) currentRow = i; - EntityRenderState cachedPlayerRenderState = CACHED_PLAYER_RENDER_STATES.get(skinProfile); row.add(new SkinWidget( 0, 0, 0, 0, skinProfile, - new FakeClientPlayer(minecraft.level, skinProfile), + (FakeClientPlayer) fakePlayer, this, PlayerMorph.getFavoriteSkins(minecraft.player).contains(skinProfile), bl, - Remorphed.canUseEveryShape(minecraft.player) || Remorphed.CONFIG.playerKillValue < 1 ? -1 : Remorphed.CONFIG.playerKillValue * PlayerMorph.getPlayerKills(minecraft.player, skinProfile.getId()) - PlayerMorph.getCounter(minecraft.player, skinProfile.getId()), - cachedPlayerRenderState + Remorphed.canUseEveryShape(minecraft.player) || Remorphed.CONFIG.playerKillValue < 1 ? -1 : Remorphed.CONFIG.playerKillValue * PlayerMorph.getPlayerKills(minecraft.player, skinProfile.getId()) - PlayerMorph.getCounter(minecraft.player, skinProfile.getId()) )); } else { Remorphed.LOGGER.error("invalid skin profile: {}", skinProfile); @@ -234,7 +226,6 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull if (entity != null) { boolean bl = type.equals(currentType); if (bl) currentRow = i; - EntityRenderState cachedRenderState = CACHED_ENTITY_RENDER_STATES.get(type); row.add(new EntityWidget<>( i * Remorphed.CONFIG.shapes_per_row + j, 0, @@ -246,8 +237,7 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull this, PlayerMorph.getFavoriteShapes(minecraft.player).contains(type), bl, - Remorphed.canUseEveryShape(minecraft.player) || Remorphed.getKillValue(type.getEntityType()) < 1 ? -1 : Remorphed.getKillValue(type.getEntityType()) * PlayerMorph.getKills(minecraft.player, type) - PlayerMorph.getCounter(minecraft.player, type), - cachedRenderState + Remorphed.canUseEveryShape(minecraft.player) || Remorphed.getKillValue(type.getEntityType()) < 1 ? -1 : Remorphed.getKillValue(type.getEntityType()) * PlayerMorph.getKills(minecraft.player, type) - PlayerMorph.getCounter(minecraft.player, type) )); } else { Remorphed.LOGGER.error("invalid shape type: {}", type.getEntityType().getDescriptionId()); @@ -268,111 +258,59 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull public synchronized void populateUnlockedRenderEntities(Player player) { unlockedShapes.clear(); renderEntities.clear(); - List> validUnlocked = Remorphed.getUnlockedShapes(player); - + List> validUnlocked = Remorphed.getUnlockedShapes(player); - // Create new entities and render states only for newly unlocked shapes for (ShapeType type : validUnlocked) { - if (!CACHED_ENTITY_RENDER_STATES.containsKey(type)) { - try { - Entity entity = type.create(Minecraft.getInstance().level, player); - if (entity instanceof Mob living) { - // Fix slimes and magma cubes - set size to 1 (smallest) - if (living instanceof Slime slime) { - slime.setSize(1, true); - } - - // Disable animations for consistent rendering - living.setNoAi(true); - living.setInvulnerable(true); - - // Create and cache the EntityRenderState with 1.0F scale (like original) - // The actual scaling is handled by the widget's size calculation - EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); - EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(living); - EntityRenderState entityRenderState = entityRenderer.createRenderState(living, 1.0F); - entityRenderState.hitboxesRenderState = null; - CACHED_ENTITY_RENDER_STATES.put(type, entityRenderState); + // Try to get from global cache first + EntityRenderCache.CachedEntityData cachedData = EntityRenderCache.getCachedEntity(type); - // Don't cache the entity - create fresh each time to avoid state issues - renderEntities.put(type, living); - } - } catch (Exception e) { - Remorphed.LOGGER.warn("Failed to create entity for type {}: {}", type.getEntityType(), e.getMessage()); - } + if (cachedData != null && cachedData.entity instanceof Mob cachedMob) { + // Cache hit! Use the pre-loaded entity + renderEntities.put(type, cachedMob); + unlockedShapes.add(type); } else { - // Create fresh entity but use cached render state - Entity entity = type.create(Minecraft.getInstance().level, player); - if (entity instanceof Mob living) { - // Fix slimes and magma cubes - set size to 1 (smallest) - if (living instanceof Slime slime) { - slime.setSize(1, true); - } - - // Disable animations for consistent rendering - living.setNoAi(true); - living.setInvulnerable(true); - - renderEntities.put(type, living); + // Cache miss - create, prepare, and cache entity on-demand + EntityRenderCache.cacheEntity(type, player); + + // Now retrieve the prepared entity from cache + cachedData = EntityRenderCache.getCachedEntity(type); + if (cachedData != null && cachedData.entity instanceof Mob cachedMob) { + renderEntities.put(type, cachedMob); + unlockedShapes.add(type); } } } - - // Add all valid unlocked shapes - for (ShapeType type : validUnlocked) { - if (CACHED_ENTITY_RENDER_STATES.containsKey(type)) { - unlockedShapes.add(type); - } - } } public synchronized void populateUnlockedRenderPlayers(Player player) { unlockedSkins.clear(); renderPlayers.clear(); + List validUnlocked = Remorphed.getUnlockedSkins(player); - // Filter out the player's own skin - List filteredUnlocked = validUnlocked.stream() - .filter(profile -> profile.getId() != player.getUUID()) - .toList(); - - // Create new fake players and render states only for newly unlocked skins - for (GameProfile profile : filteredUnlocked) { - if (!CACHED_PLAYER_RENDER_STATES.containsKey(profile)) { - try { - if (minecraft != null) { - FakeClientPlayer entity = new FakeClientPlayer(minecraft.level, profile); - - // Create and cache the EntityRenderState with 1.0F scale (like original) - // The actual scaling is handled by the widget's size calculation - EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); - EntityRenderer entityRenderer = entityRenderDispatcher.getRenderer(entity); - EntityRenderState entityRenderState = entityRenderer.createRenderState(entity, 1.0F); - entityRenderState.hitboxesRenderState = null; - CACHED_PLAYER_RENDER_STATES.put(profile, entityRenderState); - - // Don't cache the player - create fresh each time to avoid state issues - renderPlayers.put(profile, entity); + for (GameProfile profile : validUnlocked) { + if (profile.getId() != player.getUUID()) { + // Try to get from global cache first + EntityRenderCache.CachedEntityData cachedData = EntityRenderCache.getCachedPlayerSkin(profile); + + if (cachedData != null && cachedData.entity instanceof FakeClientPlayer cachedPlayer) { + // Cache hit! Use the pre-loaded player + renderPlayers.put(profile, cachedPlayer); + unlockedSkins.add(profile); + } else { + // Cache miss - create, prepare, and cache player on-demand + EntityRenderCache.cachePlayerSkin(profile); + + // Now retrieve the prepared player from cache + cachedData = EntityRenderCache.getCachedPlayerSkin(profile); + if (cachedData != null && cachedData.entity instanceof FakeClientPlayer cachedPlayer) { + renderPlayers.put(profile, cachedPlayer); + unlockedSkins.add(profile); } - } catch (Exception e) { - Remorphed.LOGGER.warn("Failed to create fake player for profile {}: {}", profile.getName(), e.getMessage()); - } - } else { - // Create fresh player but use cached render state - if (minecraft != null) { - FakeClientPlayer entity = new FakeClientPlayer(minecraft.level, profile); - renderPlayers.put(profile, entity); } } } - - // Add all valid unlocked skins - for (GameProfile profile : filteredUnlocked) { - if (CACHED_PLAYER_RENDER_STATES.containsKey(profile)) { - unlockedSkins.add(profile); - } - } } /** @@ -380,9 +318,7 @@ public synchronized void populateUnlockedRenderPlayers(Player player) { * or when you want to force a complete refresh of all entities. */ public static void clearCache() { - CACHED_ENTITY_RENDER_STATES.clear(); - CACHED_PLAYER_RENDER_STATES.clear(); - Remorphed.LOGGER.info("Cleared render state caches"); + EntityRenderCache.clearCache(); } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java b/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java index 66f84af..bc7d5a6 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java @@ -16,7 +16,6 @@ import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.renderer.entity.EntityRenderDispatcher; -import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.LivingEntity; import org.jetbrains.annotations.NotNull; @@ -34,20 +33,13 @@ public class EntityWidget extends ShapeWidget { private final T entity; private final int size; private final int id; - private final EntityRenderState cachedRenderState; - public EntityWidget(int id, int x, int y, int width, int height, ShapeType type, @NotNull T entity, Screen parent, boolean isFavorite, boolean current, int availability, @Nullable EntityRenderState cachedRenderState) { - super(x, y, width, height, parent, isFavorite, current, availability); // int x, int y, int width, int height, message - // Calculate size with cap for small entities like slimes and magma cubes - float entitySize = Math.max(entity.getBbHeight(), entity.getBbWidth()); - float scaleFactor = 1 / entitySize; - // Cap the scale factor to prevent slimes/magma cubes from being too big - scaleFactor = Math.min(scaleFactor, 2.0f); - this.size = (int) (Remorphed.CONFIG.entity_size * scaleFactor); + public EntityWidget(int id, int x, int y, int width, int height, ShapeType type, @NotNull T entity, Screen parent, boolean isFavorite, boolean current, int availability) { + super(x, y, width, height, parent, isFavorite, current, availability); + this.size = (int) (Remorphed.CONFIG.entity_size * (1 / (Math.max(entity.getBbHeight(), entity.getBbWidth())))); this.type = type; this.entity = entity; this.id = id; - this.cachedRenderState = cachedRenderState; // Use cached render state with proper scale entity.setGlowingTag(true); setTooltip(Tooltip.create(ShapeType.createTooltipText(entity))); } @@ -95,14 +87,15 @@ protected void renderShape(GuiGraphics guiGraphics) { // Some entities (namely Aether mobs) crash when rendered in a GUI. // Unsure as to the cause, but this try/catch should prevent the game from entirely dipping out. try { - // ARGH int leftPos = (int) (getX() + (float) this.getWidth() / 2); int topPos = (int) (getY() + this.getHeight() * .75f); int k = leftPos - 20; int l = topPos - 25; int m = leftPos + 20; int n = topPos + 35; - RemorphedClient.renderEntityInInventory(id, guiGraphics, k, l, m, n, size, new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), null, entity, cachedRenderState); + RemorphedClient.renderEntityInInventory(id, guiGraphics, k, l, m, n, (float) size, + new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), + null, entity); } catch (Exception e) { Remorphed.LOGGER.error("Error while rendering {}", ShapeType.createTooltipText(entity).getString(), e); setCrashed(); diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java b/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java index 7c97f98..e19c477 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java @@ -13,7 +13,6 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.NotNull; @@ -26,19 +25,12 @@ public class SkinWidget extends ShapeWidget { private final GameProfile skin; private final FakeClientPlayer fakePlayer; private final int size; - private final EntityRenderState cachedRenderState; - public SkinWidget(int x, int y, int width, int height, @NotNull GameProfile skin, @NotNull FakeClientPlayer fakePlayer, Screen parent, boolean isFavorite, boolean isCurrent, int availability, @Nullable EntityRenderState cachedRenderState) { + public SkinWidget(int x, int y, int width, int height, @NotNull GameProfile skin, @NotNull FakeClientPlayer fakePlayer, Screen parent, boolean isFavorite, boolean isCurrent, int availability) { super(x, y, width, height, parent, isFavorite, isCurrent, availability); - // Calculate size with cap for small entities like slimes and magma cubes - float entitySize = Math.max(fakePlayer.getBbHeight(), fakePlayer.getBbWidth()); - float scaleFactor = 1 / entitySize; - // Cap the scale factor to prevent slimes/magma cubes from being too big - scaleFactor = Math.min(scaleFactor, 2.0f); - this.size = (int) (Remorphed.CONFIG.entity_size * scaleFactor); + this.size = (int) (Remorphed.CONFIG.entity_size * (1 / (Math.max(fakePlayer.getBbHeight(), fakePlayer.getBbWidth())))); this.skin = skin; this.fakePlayer = fakePlayer; - this.cachedRenderState = cachedRenderState; // Use cached render state with proper scale setTooltip(Tooltip.create(Component.literal(skin.getName()))); } @@ -67,7 +59,9 @@ protected void renderShape(GuiGraphics guiGraphics) { int n = topPos + 35; // Use a unique ID for each skin widget (based on skin UUID hash) int id = skin.getId().hashCode(); - RemorphedClient.renderEntityInInventory(id, guiGraphics, k, l, m, n, size, new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), null, fakePlayer, cachedRenderState); + RemorphedClient.renderEntityInInventory(id, guiGraphics, k, l, m, n, (float) size, + new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), + null, fakePlayer); } } From 4363bff3a066156fb8cbd84733d57486facd4f39 Mon Sep 17 00:00:00 2001 From: Elijah Stephenson Date: Sun, 16 Nov 2025 18:06:15 -0600 Subject: [PATCH 5/7] Preloading logic working! --- .../client/EntityRenderCacheHandler.java | 58 +++++++++- .../remorphed/screen/EntityPreloadScreen.java | 107 ++++++++++++++++++ .../remorphed/screen/EntityRenderCache.java | 30 ++--- .../remorphed/screen/RemorphedMenu.java | 5 +- 4 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java diff --git a/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java b/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java index 0452a2d..770fd10 100644 --- a/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java +++ b/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java @@ -1,14 +1,23 @@ package dev.tocraft.remorphed.handler.client; +import com.mojang.authlib.GameProfile; import dev.tocraft.craftedcore.event.client.ClientTickEvents; import dev.tocraft.remorphed.Remorphed; +import dev.tocraft.remorphed.screen.EntityPreloadScreen; import dev.tocraft.remorphed.screen.EntityRenderCache; +import dev.tocraft.walkers.api.variant.ShapeType; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.Mob; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** - * Handles pre-loading entity render states when the player joins a world. - * This ensures the menu opens instantly without visual loading delays. + * Handles pre-loading entity instances when the player joins a world. + * Entity instances are cached so subsequent menu opens are instant. */ public class EntityRenderCacheHandler implements ClientTickEvents.Client { @@ -42,7 +51,7 @@ public void tick(Minecraft client) { } /** - * Pre-loads all entity and player skin render states for the given player. + * Pre-loads entity instances for the given player. * This runs on the main client thread. * * @param player The player to pre-load entities for @@ -53,11 +62,50 @@ private void preloadForPlayer(LocalPlayer player) { } try { - // Pre-load entities and player skins on main thread + // Cache entity instances EntityRenderCache.preloadEntities(player); EntityRenderCache.preloadPlayerSkins(player); + + // Start background pre-rendering + startPreRendering(player); } catch (Exception e) { - Remorphed.LOGGER.error("[Remorphed] Failed to pre-load entity render cache", e); + // Silent - don't spam logs + } + } + + private void startPreRendering(LocalPlayer player) { + List> currentUnlockedShapes = Remorphed.getUnlockedShapes(player); + List unlockedSkins = Remorphed.getUnlockedSkins(player); + + // Apply the SAME filtering logic as RemorphedMenu lines 118-126 + // This filters to one variant per entity type for the CURRENT mode (survival/creative) + List> currentFilteredShapes = new ArrayList<>(); + Set> seenTypes = new HashSet<>(); + for (ShapeType shapeType : currentUnlockedShapes) { + if (seenTypes.add(shapeType.getEntityType())) { + currentFilteredShapes.add(shapeType); + } + } + + // Gather entities for the current filtered list (for correct ID mapping) + List entitiesToRender = new ArrayList<>(); + List> shapesInOrder = new ArrayList<>(); + + for (ShapeType type : currentFilteredShapes) { + EntityRenderCache.CachedEntityData cached = EntityRenderCache.getCachedEntity(type); + if (cached != null && cached.entity instanceof Mob mob) { + entitiesToRender.add(mob); + shapesInOrder.add(type); + } + } + + if (!entitiesToRender.isEmpty()) { + // Open invisible pre-render screen with shape types for ID calculation + Minecraft.getInstance().setScreen(new EntityPreloadScreen( + entitiesToRender, + shapesInOrder, + unlockedSkins + )); } } } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java b/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java new file mode 100644 index 0000000..1391d04 --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java @@ -0,0 +1,107 @@ +package dev.tocraft.remorphed.screen; + +import com.mojang.authlib.GameProfile; +import dev.tocraft.remorphed.Remorphed; +import dev.tocraft.remorphed.RemorphedClient; +import dev.tocraft.walkers.api.variant.ShapeType; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.Mob; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.List; + +/** + * Invisible screen that pre-renders entities off-screen to force texture/model loading. + * Renders in batches to avoid lag. + * Uses the same ID calculation as RemorphedMenu for GuiShapeRenderer cache compatibility. + */ +@Environment(EnvType.CLIENT) +public class EntityPreloadScreen extends Screen { + private final List entitiesToRender; + private final List> shapesInOrder; + private final List skinsToRender; + private int currentIndex = 0; + private int finishedTicks = 0; // Ticks since finishing rendering + private static final int ENTITIES_PER_FRAME = 10; // Render 10 entities per frame + private static final int WAIT_AFTER_RENDER = 5; // Wait 5 ticks after rendering for textures to load + + public EntityPreloadScreen(List entities, List> shapes, List skins) { + super(Component.literal("Preloading")); + this.entitiesToRender = entities; + this.shapesInOrder = shapes; + this.skinsToRender = skins; + } + + @Override + protected void init() { + super.init(); + // Don't add any widgets - keep it completely empty + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + // Don't call super.render() - no background, no widgets, nothing visible + + // Render a batch of entities far off-screen + int rendered = 0; + while (currentIndex < entitiesToRender.size() && rendered < ENTITIES_PER_FRAME) { + Mob entity = entitiesToRender.get(currentIndex); + + // Calculate the list index (offset by skin count) + int listIndex = skinsToRender.size() + currentIndex; + + // Calculate grid position (same as RemorphedMenu) + int row = listIndex / Remorphed.CONFIG.shapes_per_row; + int col = listIndex % Remorphed.CONFIG.shapes_per_row; + + // Calculate ID using the SAME formula as RemorphedMenu + int id = row * Remorphed.CONFIG.shapes_per_row + col; + + try { + // Render at reasonable size but off-screen to force texture loading + int size = (int) (Remorphed.CONFIG.entity_size * (1 / (Math.max(entity.getBbHeight(), entity.getBbWidth())))); + + RemorphedClient.renderEntityInInventory( + id, // Use calculated grid ID + guiGraphics, + -10000, -10000, // Off-screen + -9900, -9900, + size, + new Vector3f(), + new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), + null, + entity + ); + } catch (Exception e) { + // Silent - entity will load on-demand if pre-render fails + } + + currentIndex++; + rendered++; + } + + // Wait a few ticks after rendering completes to let textures fully load + if (currentIndex >= entitiesToRender.size()) { + finishedTicks++; + if (finishedTicks >= WAIT_AFTER_RENDER) { + Minecraft.getInstance().setScreen(null); + } + } + } + + @Override + public boolean isPauseScreen() { + return false; // Don't pause the game + } + + @Override + public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + // Don't render ANY background - keep it completely transparent/invisible + } +} diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java b/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java index caad94c..bca69f8 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java @@ -47,10 +47,10 @@ public CachedEntityData(LivingEntity entity) { } /** - * Pre-loads all unlocked entity shapes for the given player. - * This should be called when the player joins a world. + * Pre-loads ALL possible entity shapes for the given player. + * This caches all entities regardless of unlock status to support creative mode switching. * - * @param player The player whose unlocked shapes to cache + * @param player The player context */ public static void preloadEntities(Player player) { Minecraft minecraft = Minecraft.getInstance(); @@ -58,10 +58,12 @@ public static void preloadEntities(Player player) { return; } - List> unlockedShapes = Remorphed.getUnlockedShapes(player); + // Cache ALL possible shapes, not just unlocked ones + // This ensures entities are ready when switching to creative mode + List> allShapes = ShapeType.getAllTypes(player.level()); - for (ShapeType type : unlockedShapes) { - // Skip if already cached + for (ShapeType type : allShapes) { + // Skip if already cached (thread-safe check) if (ENTITY_CACHE.containsKey(type)) { continue; } @@ -75,7 +77,8 @@ public static void preloadEntities(Player player) { prepareStaticEntity(mob); // Cache the ENTITY itself (not render state!) - ENTITY_CACHE.put(type, new CachedEntityData(mob)); + // Use putIfAbsent to avoid race conditions + ENTITY_CACHE.putIfAbsent(type, new CachedEntityData(mob)); } } catch (Exception e) { Remorphed.LOGGER.warn("[Remorphed] Failed to pre-load entity for type {}: {}", @@ -104,7 +107,7 @@ public static void preloadPlayerSkins(Player player) { continue; } - // Skip if already cached + // Skip if already cached (thread-safe check) if (PLAYER_CACHE.containsKey(profile)) { continue; } @@ -117,7 +120,8 @@ public static void preloadPlayerSkins(Player player) { fakePlayer.setInvulnerable(true); // Cache the ENTITY itself (not render state!) - PLAYER_CACHE.put(profile, new CachedEntityData(fakePlayer)); + // Use putIfAbsent to avoid race conditions + PLAYER_CACHE.putIfAbsent(profile, new CachedEntityData(fakePlayer)); } catch (Exception e) { Remorphed.LOGGER.warn("[Remorphed] Failed to pre-load player skin for profile {}: {}", profile.getName(), e.getMessage()); @@ -192,8 +196,8 @@ public static void cacheEntity(ShapeType type, Player player) { if (entity instanceof Mob mob) { prepareStaticEntity(mob); - ENTITY_CACHE.put(type, new CachedEntityData(mob)); - Remorphed.LOGGER.debug("[Remorphed] Cached entity on-demand: {}", type.getEntityType().getDescriptionId()); + // Use putIfAbsent to avoid race conditions + ENTITY_CACHE.putIfAbsent(type, new CachedEntityData(mob)); } } catch (Exception e) { Remorphed.LOGGER.warn("[Remorphed] Failed to cache entity on-demand: {}", type.getEntityType().getDescriptionId(), e); @@ -220,8 +224,8 @@ public static void cachePlayerSkin(GameProfile profile) { FakeClientPlayer fakePlayer = new FakeClientPlayer(minecraft.level, profile); fakePlayer.setInvulnerable(true); - PLAYER_CACHE.put(profile, new CachedEntityData(fakePlayer)); - Remorphed.LOGGER.debug("[Remorphed] Cached player skin on-demand: {}", profile.getName()); + // Use putIfAbsent to avoid race conditions + PLAYER_CACHE.putIfAbsent(profile, new CachedEntityData(fakePlayer)); } catch (Exception e) { Remorphed.LOGGER.warn("[Remorphed] Failed to cache player skin on-demand: {}", profile.getName(), e); } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java index 8dc02d9..1dc51d1 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java @@ -117,10 +117,12 @@ protected void addContents() { // filter unlocked List> newUnlocked = new ArrayList<>(); + Set> seenTypes = new HashSet<>(); for (ShapeType shapeType : unlockedShapes) { - if (!newUnlocked.stream().map(ShapeType::getEntityType).toList().contains(shapeType.getEntityType())) { + if (!seenTypes.contains(shapeType.getEntityType())) { if (currentShape == null || shapeType.equals(currentShape) || shapeType.getEntityType() != currentShape.getEntityType() || shapeType.getVariantData() == currentShape.getVariantData()) { // only add the current variant, NOT the default one (additionally) newUnlocked.add(shapeType); + seenTypes.add(shapeType.getEntityType()); } } } @@ -168,7 +170,6 @@ protected void addContents() { .toList(); populateShapeWidgets(filteredShapes, filteredSkins); - Remorphed.LOGGER.info("Loaded {} entities and {} skins for rendering", filteredShapes.size(), filteredSkins.size()); lastSearchContents = text; }); From b8d5639e80ab5ea90eb46ae839652d6aa355bd0f Mon Sep 17 00:00:00 2001 From: To_Craft Date: Mon, 1 Dec 2025 17:56:56 +0100 Subject: [PATCH 6/7] fix screen auto-closes --- .../client/EntityRenderCacheHandler.java | 9 +++---- .../remorphed/screen/EntityPreloadScreen.java | 25 ++++++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java b/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java index 770fd10..fca7e1d 100644 --- a/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java +++ b/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java @@ -89,22 +89,19 @@ private void startPreRendering(LocalPlayer player) { // Gather entities for the current filtered list (for correct ID mapping) List entitiesToRender = new ArrayList<>(); - List> shapesInOrder = new ArrayList<>(); for (ShapeType type : currentFilteredShapes) { EntityRenderCache.CachedEntityData cached = EntityRenderCache.getCachedEntity(type); - if (cached != null && cached.entity instanceof Mob mob) { + if (cached != null && cached.entity() instanceof Mob mob) { entitiesToRender.add(mob); - shapesInOrder.add(type); } } if (!entitiesToRender.isEmpty()) { // Open invisible pre-render screen with shape types for ID calculation Minecraft.getInstance().setScreen(new EntityPreloadScreen( - entitiesToRender, - shapesInOrder, - unlockedSkins + entitiesToRender, + unlockedSkins )); } } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java b/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java index 1391d04..5fbde47 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java @@ -3,7 +3,6 @@ import com.mojang.authlib.GameProfile; import dev.tocraft.remorphed.Remorphed; import dev.tocraft.remorphed.RemorphedClient; -import dev.tocraft.walkers.api.variant.ShapeType; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.Minecraft; @@ -24,17 +23,15 @@ @Environment(EnvType.CLIENT) public class EntityPreloadScreen extends Screen { private final List entitiesToRender; - private final List> shapesInOrder; private final List skinsToRender; private int currentIndex = 0; private int finishedTicks = 0; // Ticks since finishing rendering private static final int ENTITIES_PER_FRAME = 10; // Render 10 entities per frame private static final int WAIT_AFTER_RENDER = 5; // Wait 5 ticks after rendering for textures to load - public EntityPreloadScreen(List entities, List> shapes, List skins) { + public EntityPreloadScreen(List entities, List skins) { super(Component.literal("Preloading")); this.entitiesToRender = entities; - this.shapesInOrder = shapes; this.skinsToRender = skins; } @@ -68,15 +65,15 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia int size = (int) (Remorphed.CONFIG.entity_size * (1 / (Math.max(entity.getBbHeight(), entity.getBbWidth())))); RemorphedClient.renderEntityInInventory( - id, // Use calculated grid ID - guiGraphics, - -10000, -10000, // Off-screen - -9900, -9900, - size, - new Vector3f(), - new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), - null, - entity + id, // Use calculated grid ID + guiGraphics, + -10000, -10000, // Off-screen + -9900, -9900, + size, + new Vector3f(), + new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), + null, + entity ); } catch (Exception e) { // Silent - entity will load on-demand if pre-render fails @@ -90,7 +87,7 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia if (currentIndex >= entitiesToRender.size()) { finishedTicks++; if (finishedTicks >= WAIT_AFTER_RENDER) { - Minecraft.getInstance().setScreen(null); + Minecraft.getInstance().setScreen(new RemorphedMenu()); } } } From 76d941cf2d60489b085c7066b80517d4dc616f73 Mon Sep 17 00:00:00 2001 From: To_Craft Date: Mon, 1 Dec 2025 17:57:01 +0100 Subject: [PATCH 7/7] small cleanup --- .../dev/tocraft/remorphed/RemorphedClient.java | 1 - .../handler/client/ClientDisconnectHandler.java | 1 - .../remorphed/network/ClientPermissionCache.java | 1 + .../remorphed/network/PermissionCheckPacket.java | 1 + .../remorphed/screen/EntityRenderCache.java | 15 +++++---------- .../tocraft/remorphed/screen/RemorphedMenu.java | 11 ++++------- .../remorphed/screen/widget/EntityWidget.java | 1 - .../remorphed/screen/widget/ShapeWidget.java | 3 +-- .../remorphed/screen/widget/SkinWidget.java | 1 - 9 files changed, 12 insertions(+), 23 deletions(-) diff --git a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java index 61d0628..ea8f2a8 100644 --- a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java +++ b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java @@ -9,7 +9,6 @@ import dev.tocraft.remorphed.handler.client.EntityRenderCacheHandler; import dev.tocraft.remorphed.mixin.client.accessor.GuiGraphicsAccessor; import dev.tocraft.remorphed.network.ClientNetworking; -import dev.tocraft.remorphed.screen.RemorphedMenu; import dev.tocraft.remorphed.screen.render.GuiShapeRenderState; import dev.tocraft.remorphed.tick.KeyPressHandler; import net.fabricmc.api.EnvType; diff --git a/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java b/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java index 57c511c..dec4caf 100644 --- a/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java +++ b/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java @@ -1,7 +1,6 @@ package dev.tocraft.remorphed.handler.client; import dev.tocraft.craftedcore.event.client.ClientTickEvents; -import dev.tocraft.remorphed.Remorphed; import dev.tocraft.remorphed.screen.EntityRenderCache; import net.minecraft.client.Minecraft; diff --git a/common/src/main/java/dev/tocraft/remorphed/network/ClientPermissionCache.java b/common/src/main/java/dev/tocraft/remorphed/network/ClientPermissionCache.java index bdb7540..662143d 100644 --- a/common/src/main/java/dev/tocraft/remorphed/network/ClientPermissionCache.java +++ b/common/src/main/java/dev/tocraft/remorphed/network/ClientPermissionCache.java @@ -9,6 +9,7 @@ /** * Client-side cache for permission results to avoid repeated server requests */ +@SuppressWarnings("unused") @Environment(EnvType.CLIENT) public class ClientPermissionCache { diff --git a/common/src/main/java/dev/tocraft/remorphed/network/PermissionCheckPacket.java b/common/src/main/java/dev/tocraft/remorphed/network/PermissionCheckPacket.java index 035c2c6..6a6fa60 100644 --- a/common/src/main/java/dev/tocraft/remorphed/network/PermissionCheckPacket.java +++ b/common/src/main/java/dev/tocraft/remorphed/network/PermissionCheckPacket.java @@ -8,6 +8,7 @@ /** * Network packet for checking permissions - client-side methods only */ +@SuppressWarnings("unused") public class PermissionCheckPacket { /** diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java b/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java index bca69f8..020426c 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java @@ -22,10 +22,10 @@ /** * Manages pre-loading and caching of entity render states for the Remorphed menu. * This prevents the visual loading delay when opening the menu for the first time. - * * The cache creates isolated, static snapshots of entity render states that won't be * affected by real-world entities near the player. */ +@SuppressWarnings("unused") @Environment(EnvType.CLIENT) public class EntityRenderCache { @@ -38,12 +38,7 @@ public class EntityRenderCache { * NOTE: We store the actual entity, NOT the EntityRenderState, because * EntityRenderState objects are mutable and get updated by the rendering system. */ - public static class CachedEntityData { - public final LivingEntity entity; - - public CachedEntityData(LivingEntity entity) { - this.entity = entity; - } + public record CachedEntityData(LivingEntity entity) { } /** @@ -82,7 +77,7 @@ public static void preloadEntities(Player player) { } } catch (Exception e) { Remorphed.LOGGER.warn("[Remorphed] Failed to pre-load entity for type {}: {}", - type.getEntityType().getDescriptionId(), e.getMessage()); + type.getEntityType().getDescriptionId(), e.getMessage()); } } } @@ -124,7 +119,7 @@ public static void preloadPlayerSkins(Player player) { PLAYER_CACHE.putIfAbsent(profile, new CachedEntityData(fakePlayer)); } catch (Exception e) { Remorphed.LOGGER.warn("[Remorphed] Failed to pre-load player skin for profile {}: {}", - profile.getName(), e.getMessage()); + profile.getName(), e.getMessage()); } } } @@ -178,7 +173,7 @@ public static CachedEntityData getCachedPlayerSkin(GameProfile profile) { * Caches a single entity type on-demand. * Used when a player unlocks a new shape while in-world. * - * @param type The shape type to cache + * @param type The shape type to cache * @param player The player context */ public static void cacheEntity(ShapeType type, Player player) { diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java index 1dc51d1..1459a6e 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java @@ -7,7 +7,6 @@ import dev.tocraft.remorphed.impl.PlayerMorph; import dev.tocraft.remorphed.mixin.client.accessor.ScreenAccessor; import dev.tocraft.remorphed.screen.widget.*; -import dev.tocraft.remorphed.screen.EntityRenderCache; import dev.tocraft.skinshifter.SkinShifter; import dev.tocraft.walkers.Walkers; import dev.tocraft.walkers.api.PlayerShape; @@ -25,11 +24,9 @@ import net.minecraft.client.player.AbstractClientPlayer; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; -import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; -import net.minecraft.world.entity.monster.Slime; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -266,7 +263,7 @@ public synchronized void populateUnlockedRenderEntities(Player player) { // Try to get from global cache first EntityRenderCache.CachedEntityData cachedData = EntityRenderCache.getCachedEntity(type); - if (cachedData != null && cachedData.entity instanceof Mob cachedMob) { + if (cachedData != null && cachedData.entity() instanceof Mob cachedMob) { // Cache hit! Use the pre-loaded entity renderEntities.put(type, cachedMob); unlockedShapes.add(type); @@ -276,7 +273,7 @@ public synchronized void populateUnlockedRenderEntities(Player player) { // Now retrieve the prepared entity from cache cachedData = EntityRenderCache.getCachedEntity(type); - if (cachedData != null && cachedData.entity instanceof Mob cachedMob) { + if (cachedData != null && cachedData.entity() instanceof Mob cachedMob) { renderEntities.put(type, cachedMob); unlockedShapes.add(type); } @@ -295,7 +292,7 @@ public synchronized void populateUnlockedRenderPlayers(Player player) { // Try to get from global cache first EntityRenderCache.CachedEntityData cachedData = EntityRenderCache.getCachedPlayerSkin(profile); - if (cachedData != null && cachedData.entity instanceof FakeClientPlayer cachedPlayer) { + if (cachedData != null && cachedData.entity() instanceof FakeClientPlayer cachedPlayer) { // Cache hit! Use the pre-loaded player renderPlayers.put(profile, cachedPlayer); unlockedSkins.add(profile); @@ -305,7 +302,7 @@ public synchronized void populateUnlockedRenderPlayers(Player player) { // Now retrieve the prepared player from cache cachedData = EntityRenderCache.getCachedPlayerSkin(profile); - if (cachedData != null && cachedData.entity instanceof FakeClientPlayer cachedPlayer) { + if (cachedData != null && cachedData.entity() instanceof FakeClientPlayer cachedPlayer) { renderPlayers.put(profile, cachedPlayer); unlockedSkins.add(profile); } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java b/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java index bc7d5a6..c0d3297 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/widget/EntityWidget.java @@ -19,7 +19,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.LivingEntity; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f; diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/widget/ShapeWidget.java b/common/src/main/java/dev/tocraft/remorphed/screen/widget/ShapeWidget.java index d828d44..e2bcc33 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/widget/ShapeWidget.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/widget/ShapeWidget.java @@ -78,8 +78,7 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float String s = String.valueOf(availability); int w = parent.getFont().width(s); guiGraphics.drawString(parent.getFont(), s, getX() + getWidth() - w - getWidth() / 8, (int) (getY() + getHeight() * 0.125), -1, false); - } - else if (availability == 0) { + } else if (availability == 0) { guiGraphics.blit(RenderPipelines.GUI_TEXTURED, Remorphed.id("textures/gui/deleted.png"), getX(), getY(), 0, 0, getWidth(), getHeight(), 48, 32, 48, 32); } diff --git a/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java b/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java index e19c477..07b78e0 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/widget/SkinWidget.java @@ -16,7 +16,6 @@ import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f;