Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ public void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBu
rootNode.addChild(hasSkin);
}


dispatcher.getRoot().addChild(rootNode);

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<ShapeType<?>> currentUnlockedShapes = Remorphed.getUnlockedShapes(player);
List<GameProfile> 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<ShapeType<?>> currentFilteredShapes = new ArrayList<>();
Set<net.minecraft.world.entity.EntityType<?>> 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<Mob> 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
));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/**
* Client-side cache for permission results to avoid repeated server requests
*/
@SuppressWarnings("unused")
@Environment(EnvType.CLIENT)
public class ClientPermissionCache {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/**
* Network packet for checking permissions - client-side methods only
*/
@SuppressWarnings("unused")
public class PermissionCheckPacket {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Mob> entitiesToRender;
private final List<GameProfile> 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<Mob> entities, List<GameProfile> 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
}
}
Loading