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 ================ diff --git a/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java b/common/src/main/java/dev/tocraft/remorphed/RemorphedClient.java index b3e6732..ea8f2a8 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.render.GuiShapeRenderState; @@ -33,6 +35,8 @@ 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()); 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/handler/client/ClientDisconnectHandler.java b/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java new file mode 100644 index 0000000..dec4caf --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/handler/client/ClientDisconnectHandler.java @@ -0,0 +1,25 @@ +package dev.tocraft.remorphed.handler.client; + +import dev.tocraft.craftedcore.event.client.ClientTickEvents; +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..fca7e1d --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/handler/client/EntityRenderCacheHandler.java @@ -0,0 +1,108 @@ +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 instances when the player joins a world. + * Entity instances are cached so subsequent menu opens are instant. + */ +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 entity instances 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 { + // Cache entity instances + EntityRenderCache.preloadEntities(player); + EntityRenderCache.preloadPlayerSkins(player); + + // Start background pre-rendering + startPreRendering(player); + } catch (Exception 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<>(); + + for (ShapeType type : currentFilteredShapes) { + EntityRenderCache.CachedEntityData cached = EntityRenderCache.getCachedEntity(type); + if (cached != null && cached.entity() instanceof Mob mob) { + entitiesToRender.add(mob); + } + } + + if (!entitiesToRender.isEmpty()) { + // Open invisible pre-render screen with shape types for ID calculation + Minecraft.getInstance().setScreen(new EntityPreloadScreen( + entitiesToRender, + unlockedSkins + )); + } + } +} 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/EntityPreloadScreen.java b/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java new file mode 100644 index 0000000..5fbde47 --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/screen/EntityPreloadScreen.java @@ -0,0 +1,104 @@ +package dev.tocraft.remorphed.screen; + +import com.mojang.authlib.GameProfile; +import dev.tocraft.remorphed.Remorphed; +import dev.tocraft.remorphed.RemorphedClient; +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 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 skins) { + super(Component.literal("Preloading")); + this.entitiesToRender = entities; + 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(new RemorphedMenu()); + } + } + } + + @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 new file mode 100644 index 0000000..020426c --- /dev/null +++ b/common/src/main/java/dev/tocraft/remorphed/screen/EntityRenderCache.java @@ -0,0 +1,255 @@ +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. + */ +@SuppressWarnings("unused") +@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 record CachedEntityData(LivingEntity entity) { + } + + /** + * 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 context + */ + public static void preloadEntities(Player player) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return; + } + + // 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 : allShapes) { + // Skip if already cached (thread-safe check) + 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!) + // 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 {}: {}", + 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 (thread-safe check) + 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!) + // 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()); + } + } + } + + /** + * 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); + // 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); + } + } + + /** + * 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); + + // 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); + } + } + + /** + * 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 71f8fe9..1459a6e 100644 --- a/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java +++ b/common/src/main/java/dev/tocraft/remorphed/screen/RemorphedMenu.java @@ -24,7 +24,6 @@ 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; @@ -49,6 +48,7 @@ public class RemorphedMenu extends Screen { private final Map, Mob> renderEntities = new ConcurrentHashMap<>(); private final Map renderPlayers = new ConcurrentHashMap<>(); + private final SearchWidget searchBar = createSearchBar(); private final Button helpButton = createHelpButton(); private final PlayerWidget playerButton = createPlayerButton(); @@ -114,10 +114,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()); } } } @@ -165,7 +167,6 @@ protected void addContents() { .toList(); populateShapeWidgets(filteredShapes, filteredSkins); - Remorphed.LOGGER.info("Loaded {} entities and {} skins for rendering", filteredShapes.size(), filteredSkins.size()); lastSearchContents = text; }); @@ -208,7 +209,7 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull 0, 0, skinProfile, - new FakeClientPlayer(minecraft.level, skinProfile), + (FakeClientPlayer) fakePlayer, this, PlayerMorph.getFavoriteSkins(minecraft.player).contains(skinProfile), bl, @@ -255,12 +256,27 @@ private void populateShapeWidgets(@NotNull List> rendered, @NotNull public synchronized void populateUnlockedRenderEntities(Player player) { unlockedShapes.clear(); renderEntities.clear(); + List> validUnlocked = Remorphed.getUnlockedShapes(player); + for (ShapeType type : validUnlocked) { - Entity entity = type.create(Minecraft.getInstance().level, player); - if (entity instanceof Mob living) { + // Try to get from global cache first + EntityRenderCache.CachedEntityData cachedData = EntityRenderCache.getCachedEntity(type); + + if (cachedData != null && cachedData.entity() instanceof Mob cachedMob) { + // Cache hit! Use the pre-loaded entity + renderEntities.put(type, cachedMob); unlockedShapes.add(type); - renderEntities.put(type, living); + } else { + // 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); + } } } } @@ -268,19 +284,42 @@ public synchronized void populateUnlockedRenderEntities(Player player) { 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; - if (minecraft != null) { - entity = new FakeClientPlayer(minecraft.level, profile); + // 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); + } } - 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() { + EntityRenderCache.clearCache(); + } + + 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..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 @@ -34,7 +34,7 @@ public class EntityWidget extends ShapeWidget { private final int id; 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); // int x, int y, int width, int height, message + 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; @@ -86,14 +86,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); + 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/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 b3e2bbe..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 @@ -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,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.gui.screens.inventory.InventoryScreen; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.NotNull; @@ -56,7 +56,11 @@ 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, (float) size, + new Vector3f(), new Quaternionf().rotationXYZ(0.43633232F, (float) Math.PI, (float) Math.PI), + null, fakePlayer); } }