package com.razz.dfashion.client.screen; import com.razz.dfashion.block.ClosetBlockEntity; import com.razz.dfashion.client.ClientSharedCosmeticCache; import com.razz.dfashion.client.ClientSharedCosmeticUploader; import com.razz.dfashion.client.ClientSkinCache; import com.razz.dfashion.client.CosmeticCache; import com.razz.dfashion.client.CosmeticRenderLayer; import com.razz.dfashion.client.SkinInfoOverride; import com.razz.dfashion.cosmetic.CosmeticAttachments; import com.razz.dfashion.cosmetic.CosmeticDefinition; import com.razz.dfashion.cosmetic.CosmeticRef; import com.razz.dfashion.cosmetic.share.BoneExtraction; import com.razz.dfashion.cosmetic.share.CosmeticLibrary; import com.razz.dfashion.cosmetic.share.CosmeticLibraryEntry; import com.razz.dfashion.cosmetic.share.packet.AssignSharedCosmetic; import com.razz.dfashion.cosmetic.share.packet.DeleteCosmetic; import com.razz.dfashion.skin.SkinAttachments; import com.razz.dfashion.skin.SkinData; import com.razz.dfashion.skin.SkinLibrary; import com.razz.dfashion.skin.SkinLibraryEntry; import com.razz.dfashion.skin.SkinModel; import com.razz.dfashion.skin.packet.AssignSkin; import com.razz.dfashion.skin.packet.DeleteSkin; import com.razz.dfashion.skin.packet.RequestSkin; import net.minecraft.core.ClientAsset; import net.minecraft.world.entity.player.PlayerModelType; import net.minecraft.world.entity.player.PlayerSkin; import net.minecraft.client.Camera; import net.minecraft.client.CameraType; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.renderer.entity.EntityRenderDispatcher; import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.client.renderer.entity.state.AvatarRenderState; import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.phys.Vec3; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.common.NeoForge; import org.lwjgl.PointerBuffer; import org.lwjgl.system.MemoryStack; import org.lwjgl.util.tinyfd.TinyFileDialogs; import org.jspecify.annotations.Nullable; import org.joml.Matrix4f; import org.joml.Quaternionf; import org.joml.Vector3f; import org.lwjgl.glfw.GLFW; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public class ClosetScreen extends Screen { // Tab vocabulary — matches category memory. "skin" is a pseudo-category that drives the // per-player skin override UI (upload/reset) rather than the usual cosmetic row. // "shared" is a pseudo-category for the shared-cosmetic library (vertical scroll grid). private static final String[] CATEGORIES = { "hat", "head", "torso", "arms", "legs", "wrist", "feet", "wings", "particle", "skin", "shared" }; private static final String SKIN_TAB = "skin"; private static final String SHARED_TAB = "shared"; /** Fallback equip category used when a shared cosmetic has no canonical bones. */ private static final String FALLBACK_SHARED_CATEGORY = "particle"; /** * Bone-based filters on the shared tab. {@code null} = "All"; others are canonical * bone group names used by {@link BoneExtraction}. Labels are user-facing ({@code Torso} * rather than the canonical {@code Body}). */ private static final String[] SHARED_FILTER_KEYS = { null, "Head", "Body", "Arm", "Leg" }; private static final String[] SHARED_FILTER_LABELS = { "All", "Head", "Torso", "Arms", "Legs" }; // Per-tick rotation rate while an arrow is held (20 tps → 120°/sec). private static final float ROTATE_PER_TICK = 6f; // Layout constants — tabs run horizontally across the top. private static final int TAB_X_START = 10; private static final int TAB_Y = 10; private static final int TAB_W = 20; private static final int TAB_H = 20; private static final int TAB_SPACING = 2; private static final int COSMETIC_ROW_LEFT = 10; // x where row starts (full-width now) private static final int COSMETIC_ROW_BOTTOM_MARGIN = 82; // y from bottom — just above Done private static final int COSMETIC_SIZE = 36; private static final int COSMETIC_SPACING = 4; private static final int COSMETIC_INNER_PADDING = 3; private static final int COSMETIC_BORDER_COLOR = 0xFFFFFFFF; // normal frame around each preview private static final int COSMETIC_BORDER_EQUIPPED_COLOR = 0xFF55FF55; // green frame when this item is currently worn private static final int COSMETIC_BORDER_SHARED_COLOR = 0xFFFFB060; // amber frame — entry came from the user's shared library private static final float COSMETIC_PREVIEW_SIZE = 11f; // entity render scale — small enough to fit cosmetics that extend the silhouette private static final float COSMETIC_PREVIEW_YAW = -20f; // degrees — angled for better cosmetic visibility private final @Nullable ClosetBlockEntity closet; private CameraType previousCamera; private @Nullable Button leftArrow, rightArrow; // Body/head yaw applied every tick. yRot (camera-facing yaw) is left untouched so // rotating doesn't pan the camera — only the player's visible body/head turn. private float displayYaw = 0f; private @Nullable String selectedCategory; private int cosmeticScroll = 0; private final List