|
|
|
@ -1,10 +1,12 @@
|
|
|
|
package com.razz.dfashion.client.screen;
|
|
|
|
package com.razz.dfashion.client.screen;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import com.razz.dfashion.DecoFashionConfig;
|
|
|
|
import com.razz.dfashion.block.ClosetBlockEntity;
|
|
|
|
import com.razz.dfashion.block.ClosetBlockEntity;
|
|
|
|
import com.razz.dfashion.client.ClientCosmeticInfoCache;
|
|
|
|
import com.razz.dfashion.client.ClientCosmeticInfoCache;
|
|
|
|
import com.razz.dfashion.client.ClientSharedCosmeticCache;
|
|
|
|
import com.razz.dfashion.client.ClientSharedCosmeticCache;
|
|
|
|
import com.razz.dfashion.client.ClientSharedCosmeticUploader;
|
|
|
|
import com.razz.dfashion.client.ClientSharedCosmeticUploader;
|
|
|
|
import com.razz.dfashion.client.ClientSkinCache;
|
|
|
|
import com.razz.dfashion.client.ClientSkinCache;
|
|
|
|
|
|
|
|
import com.razz.dfashion.client.ClosetPreferences;
|
|
|
|
import com.razz.dfashion.client.CosmeticCache;
|
|
|
|
import com.razz.dfashion.client.CosmeticCache;
|
|
|
|
import com.razz.dfashion.client.CosmeticRenderLayer;
|
|
|
|
import com.razz.dfashion.client.CosmeticRenderLayer;
|
|
|
|
import com.razz.dfashion.client.SkinInfoOverride;
|
|
|
|
import com.razz.dfashion.client.SkinInfoOverride;
|
|
|
|
@ -43,6 +45,7 @@ import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
|
|
|
|
import net.minecraft.client.renderer.entity.EntityRenderer;
|
|
|
|
import net.minecraft.client.renderer.entity.EntityRenderer;
|
|
|
|
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
|
|
|
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
|
|
|
import net.minecraft.client.renderer.entity.state.EntityRenderState;
|
|
|
|
import net.minecraft.client.renderer.entity.state.EntityRenderState;
|
|
|
|
|
|
|
|
import net.minecraft.ChatFormatting;
|
|
|
|
import net.minecraft.network.chat.Component;
|
|
|
|
import net.minecraft.network.chat.Component;
|
|
|
|
import net.minecraft.resources.Identifier;
|
|
|
|
import net.minecraft.resources.Identifier;
|
|
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
|
|
@ -75,15 +78,28 @@ import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
public class ClosetScreen extends Screen {
|
|
|
|
public class ClosetScreen extends Screen {
|
|
|
|
|
|
|
|
|
|
|
|
// Tab vocabulary — matches category memory. "skin" is a pseudo-category that drives the
|
|
|
|
// Tab vocabulary — matches category memory. Top-level tabs collapse the old per-category
|
|
|
|
// per-player skin override UI (upload/reset) rather than the usual cosmetic row.
|
|
|
|
// list into bone-group tabs + three standalones ("particle", "skin", "shared"). Selecting
|
|
|
|
// "shared" is a pseudo-category for the shared-cosmetic library (vertical scroll grid).
|
|
|
|
// a bone-group tab exposes a sub-filter pill row so the author can narrow within the group.
|
|
|
|
private static final String[] CATEGORIES = {
|
|
|
|
// "skin" is a pseudo-category that drives the per-player skin override UI rather than the
|
|
|
|
"hat", "head", "torso", "arms", "legs", "wrist", "feet", "wings", "particle", "skin", "shared"
|
|
|
|
// usual cosmetic row; "shared" is the shared-cosmetic library (vertical scroll grid).
|
|
|
|
};
|
|
|
|
|
|
|
|
private static final String SKIN_TAB = "skin";
|
|
|
|
private static final String SKIN_TAB = "skin";
|
|
|
|
private static final String SHARED_TAB = "shared";
|
|
|
|
private static final String SHARED_TAB = "shared";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Bone-group tab key → sub-categories it shows. Display order = insertion order. */
|
|
|
|
|
|
|
|
private static final java.util.LinkedHashMap<String, String[]> BONE_GROUPS = new java.util.LinkedHashMap<>();
|
|
|
|
|
|
|
|
static {
|
|
|
|
|
|
|
|
BONE_GROUPS.put("head", new String[]{"hat", "head"});
|
|
|
|
|
|
|
|
BONE_GROUPS.put("torso", new String[]{"torso", "wings"});
|
|
|
|
|
|
|
|
BONE_GROUPS.put("arms", new String[]{"arms", "wrist"});
|
|
|
|
|
|
|
|
BONE_GROUPS.put("legs", new String[]{"legs", "feet"});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Top-level tabs in render order: 4 bone groups + particle + skin + shared. */
|
|
|
|
|
|
|
|
private static final String[] TOP_LEVEL_TABS = {
|
|
|
|
|
|
|
|
"head", "torso", "arms", "legs", "particle", SKIN_TAB, SHARED_TAB
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** Fallback equip category used when a shared cosmetic has no canonical bones. */
|
|
|
|
/** Fallback equip category used when a shared cosmetic has no canonical bones. */
|
|
|
|
private static final String FALLBACK_SHARED_CATEGORY = "particle";
|
|
|
|
private static final String FALLBACK_SHARED_CATEGORY = "particle";
|
|
|
|
|
|
|
|
|
|
|
|
@ -121,6 +137,7 @@ public class ClosetScreen extends Screen {
|
|
|
|
private CameraType previousCamera;
|
|
|
|
private CameraType previousCamera;
|
|
|
|
private @Nullable Button leftArrow, rightArrow;
|
|
|
|
private @Nullable Button leftArrow, rightArrow;
|
|
|
|
private @Nullable Button shareButton;
|
|
|
|
private @Nullable Button shareButton;
|
|
|
|
|
|
|
|
private @Nullable Button hideHeadBtn, hideBodyBtn, hideArmsBtn, hideLegsBtn;
|
|
|
|
// Body/head yaw applied every tick. yRot (camera-facing yaw) is left untouched so
|
|
|
|
// 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.
|
|
|
|
// rotating doesn't pan the camera — only the player's visible body/head turn.
|
|
|
|
private float displayYaw = 0f;
|
|
|
|
private float displayYaw = 0f;
|
|
|
|
@ -128,6 +145,10 @@ public class ClosetScreen extends Screen {
|
|
|
|
private @Nullable String selectedCategory;
|
|
|
|
private @Nullable String selectedCategory;
|
|
|
|
private int cosmeticScroll = 0;
|
|
|
|
private int cosmeticScroll = 0;
|
|
|
|
private final List<Button> cosmeticButtons = new ArrayList<>();
|
|
|
|
private final List<Button> cosmeticButtons = new ArrayList<>();
|
|
|
|
|
|
|
|
/** Bone-group sub-filter pill buttons. Tracked separately from cosmeticButtons so the
|
|
|
|
|
|
|
|
* preview renderer, which pairs cosmeticButtons with catalog entries by index, never
|
|
|
|
|
|
|
|
* accidentally draws a 3D mini-player on top of a pill. Cleaned up on every rebuild. */
|
|
|
|
|
|
|
|
private final List<Button> filterPillButtons = new ArrayList<>();
|
|
|
|
private boolean showPreviews = true; // toggled by the bottom-right button
|
|
|
|
private boolean showPreviews = true; // toggled by the bottom-right button
|
|
|
|
|
|
|
|
|
|
|
|
// Current skin-model selection on the skin tab. Drives the Upload button's model arg
|
|
|
|
// Current skin-model selection on the skin tab. Drives the Upload button's model arg
|
|
|
|
@ -175,6 +196,12 @@ public class ClosetScreen extends Screen {
|
|
|
|
// ---- Shared-library tab state ----
|
|
|
|
// ---- Shared-library tab state ----
|
|
|
|
/** {@code null} = no filter (show all); else one of {@link #SHARED_FILTER_KEYS}. */
|
|
|
|
/** {@code null} = no filter (show all); else one of {@link #SHARED_FILTER_KEYS}. */
|
|
|
|
private @Nullable String sharedBoneFilter = null;
|
|
|
|
private @Nullable String sharedBoneFilter = null;
|
|
|
|
|
|
|
|
/** Sub-category pill inside the selected bone-group tab. {@code null} = show all in group. */
|
|
|
|
|
|
|
|
private @Nullable String selectedSubFilter = null;
|
|
|
|
|
|
|
|
/** Last observed values of server-synced feature flags; used by {@link #tick()} to
|
|
|
|
|
|
|
|
* detect runtime config changes and rebuild the closet UI in response. */
|
|
|
|
|
|
|
|
private boolean lastSharedEnabled;
|
|
|
|
|
|
|
|
private boolean lastBodyHideEnabled;
|
|
|
|
private int sharedVerticalScroll = 0;
|
|
|
|
private int sharedVerticalScroll = 0;
|
|
|
|
private final Map<Button, String> sharedButtonHashes = new HashMap<>();
|
|
|
|
private final Map<Button, String> sharedButtonHashes = new HashMap<>();
|
|
|
|
private int lastKnownSharedCount = -1;
|
|
|
|
private int lastKnownSharedCount = -1;
|
|
|
|
@ -193,7 +220,9 @@ public class ClosetScreen extends Screen {
|
|
|
|
private static final int SHARED_FILTER_BTN_GAP = 4;
|
|
|
|
private static final int SHARED_FILTER_BTN_GAP = 4;
|
|
|
|
|
|
|
|
|
|
|
|
/** Top of the vertical-scroll grid on the Shared tab (below the filter bar). */
|
|
|
|
/** Top of the vertical-scroll grid on the Shared tab (below the filter bar). */
|
|
|
|
private static final int SHARED_GRID_TOP = SHARED_FILTER_Y + SHARED_FILTER_H + 10;
|
|
|
|
/** +25 below the filter bar (was +10) so the per-cosmetic {@code ?}/{@code x} overlay
|
|
|
|
|
|
|
|
* buttons at {@code boxY - 12} don't crowd the filter pills. */
|
|
|
|
|
|
|
|
private static final int SHARED_GRID_TOP = SHARED_FILTER_Y + SHARED_FILTER_H + 25;
|
|
|
|
/** Height reserved for bottom buttons; the grid stops above this. */
|
|
|
|
/** Height reserved for bottom buttons; the grid stops above this. */
|
|
|
|
private static final int SHARED_GRID_BOTTOM_PAD = 50;
|
|
|
|
private static final int SHARED_GRID_BOTTOM_PAD = 50;
|
|
|
|
|
|
|
|
|
|
|
|
@ -220,7 +249,7 @@ public class ClosetScreen extends Screen {
|
|
|
|
|
|
|
|
|
|
|
|
NeoForge.EVENT_BUS.register(this);
|
|
|
|
NeoForge.EVENT_BUS.register(this);
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedCategory == null) selectedCategory = CATEGORIES[0];
|
|
|
|
if (selectedCategory == null) selectedCategory = TOP_LEVEL_TABS[0];
|
|
|
|
|
|
|
|
|
|
|
|
// Catch any PNGs added to the cache directory since last client setup —
|
|
|
|
// Catch any PNGs added to the cache directory since last client setup —
|
|
|
|
// e.g. user dropped files in manually, or the folder was just populated.
|
|
|
|
// e.g. user dropped files in manually, or the folder was just populated.
|
|
|
|
@ -232,14 +261,25 @@ public class ClosetScreen extends Screen {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lastSharedEnabled = DecoFashionConfig.SHARED_LIBRARY_ENABLED.get();
|
|
|
|
|
|
|
|
lastBodyHideEnabled = DecoFashionConfig.BODY_HIDE_ENABLED.get();
|
|
|
|
buildTabs();
|
|
|
|
buildTabs();
|
|
|
|
|
|
|
|
buildPartToggles();
|
|
|
|
buildBottomControls();
|
|
|
|
buildBottomControls();
|
|
|
|
selectCategory(selectedCategory != null ? selectedCategory : CATEGORIES[0]);
|
|
|
|
// If the server disabled shared library mid-session and the player was viewing the
|
|
|
|
|
|
|
|
// Library tab, fall back to the first available tab.
|
|
|
|
|
|
|
|
String target = selectedCategory != null ? selectedCategory : TOP_LEVEL_TABS[0];
|
|
|
|
|
|
|
|
if (SHARED_TAB.equals(target) && !DecoFashionConfig.SHARED_LIBRARY_ENABLED.get()) {
|
|
|
|
|
|
|
|
target = TOP_LEVEL_TABS[0];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
selectCategory(target);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void buildTabs() {
|
|
|
|
private void buildTabs() {
|
|
|
|
int x = TAB_X_START;
|
|
|
|
int x = TAB_X_START;
|
|
|
|
for (String cat : CATEGORIES) {
|
|
|
|
for (String cat : TOP_LEVEL_TABS) {
|
|
|
|
|
|
|
|
// Server disabled shared library → hide the Library tab entirely.
|
|
|
|
|
|
|
|
if (SHARED_TAB.equals(cat) && !DecoFashionConfig.SHARED_LIBRARY_ENABLED.get()) continue;
|
|
|
|
String label = tabLabel(cat);
|
|
|
|
String label = tabLabel(cat);
|
|
|
|
addRenderableWidget(Button.builder(
|
|
|
|
addRenderableWidget(Button.builder(
|
|
|
|
Component.literal(label),
|
|
|
|
Component.literal(label),
|
|
|
|
@ -249,6 +289,53 @@ public class ClosetScreen extends Screen {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Body-part-hide toggles, placed to the right of the tab row. Each button cycles the
|
|
|
|
|
|
|
|
* corresponding {@link ClosetPreferences} flag and persists to disk. A struck-through
|
|
|
|
|
|
|
|
* red label indicates the part is currently hidden. Skipped entirely when the server
|
|
|
|
|
|
|
|
* disables body-hide via the mod config. */
|
|
|
|
|
|
|
|
private void buildPartToggles() {
|
|
|
|
|
|
|
|
if (!DecoFashionConfig.BODY_HIDE_ENABLED.get()) return;
|
|
|
|
|
|
|
|
int startX = TAB_X_START + TOP_LEVEL_TABS.length * (TAB_W + TAB_SPACING) + 10;
|
|
|
|
|
|
|
|
int step = TAB_W + TAB_SPACING;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hideHeadBtn = makeToggle("H", "Head", startX, ClosetPreferences.hideHead,
|
|
|
|
|
|
|
|
b -> { ClosetPreferences.hideHead = !ClosetPreferences.hideHead;
|
|
|
|
|
|
|
|
refreshToggle(b, "H", ClosetPreferences.hideHead); });
|
|
|
|
|
|
|
|
hideBodyBtn = makeToggle("B", "Body", startX + step, ClosetPreferences.hideBody,
|
|
|
|
|
|
|
|
b -> { ClosetPreferences.hideBody = !ClosetPreferences.hideBody;
|
|
|
|
|
|
|
|
refreshToggle(b, "B", ClosetPreferences.hideBody); });
|
|
|
|
|
|
|
|
hideArmsBtn = makeToggle("A", "Arms", startX + 2*step, ClosetPreferences.hideArms,
|
|
|
|
|
|
|
|
b -> { ClosetPreferences.hideArms = !ClosetPreferences.hideArms;
|
|
|
|
|
|
|
|
refreshToggle(b, "A", ClosetPreferences.hideArms); });
|
|
|
|
|
|
|
|
hideLegsBtn = makeToggle("L", "Legs", startX + 3*step, ClosetPreferences.hideLegs,
|
|
|
|
|
|
|
|
b -> { ClosetPreferences.hideLegs = !ClosetPreferences.hideLegs;
|
|
|
|
|
|
|
|
refreshToggle(b, "L", ClosetPreferences.hideLegs); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addRenderableWidget(hideHeadBtn);
|
|
|
|
|
|
|
|
addRenderableWidget(hideBodyBtn);
|
|
|
|
|
|
|
|
addRenderableWidget(hideArmsBtn);
|
|
|
|
|
|
|
|
addRenderableWidget(hideLegsBtn);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private Button makeToggle(String letter, String tooltip, int x, boolean hidden, Button.OnPress onPress) {
|
|
|
|
|
|
|
|
Button b = Button.builder(partToggleLabel(letter, hidden), onPress)
|
|
|
|
|
|
|
|
.bounds(x, TAB_Y, TAB_W, TAB_H)
|
|
|
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
b.setTooltip(Tooltip.create(Component.literal("Hide " + tooltip)));
|
|
|
|
|
|
|
|
return b;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void refreshToggle(Button btn, String letter, boolean hidden) {
|
|
|
|
|
|
|
|
btn.setMessage(partToggleLabel(letter, hidden));
|
|
|
|
|
|
|
|
ClosetPreferences.save();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static Component partToggleLabel(String letter, boolean hidden) {
|
|
|
|
|
|
|
|
return hidden
|
|
|
|
|
|
|
|
? Component.literal(letter).withStyle(ChatFormatting.STRIKETHROUGH, ChatFormatting.RED)
|
|
|
|
|
|
|
|
: Component.literal(letter);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Single-char tab labels; {@code shared} overridden to "L" so it doesn't collide with {@code skin}. */
|
|
|
|
/** Single-char tab labels; {@code shared} overridden to "L" so it doesn't collide with {@code skin}. */
|
|
|
|
private static String tabLabel(String cat) {
|
|
|
|
private static String tabLabel(String cat) {
|
|
|
|
return switch (cat) {
|
|
|
|
return switch (cat) {
|
|
|
|
@ -297,15 +384,54 @@ public class ClosetScreen extends Screen {
|
|
|
|
|
|
|
|
|
|
|
|
private void selectCategory(String cat) {
|
|
|
|
private void selectCategory(String cat) {
|
|
|
|
selectedCategory = cat;
|
|
|
|
selectedCategory = cat;
|
|
|
|
|
|
|
|
selectedSubFilter = null; // reset sub-filter on top-level tab change
|
|
|
|
cosmeticScroll = 0;
|
|
|
|
cosmeticScroll = 0;
|
|
|
|
lastKnownSkinCount = -1; // force the next skin-tab tick to re-evaluate the grid
|
|
|
|
lastKnownSkinCount = -1; // force the next skin-tab tick to re-evaluate the grid
|
|
|
|
if (shareButton != null) shareButton.visible = SHARED_TAB.equals(cat);
|
|
|
|
if (shareButton != null) shareButton.visible = SHARED_TAB.equals(cat);
|
|
|
|
rebuildCosmeticRow();
|
|
|
|
rebuildCosmeticRow();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void setBoneGroupSubFilter(@Nullable String sub) {
|
|
|
|
|
|
|
|
selectedSubFilter = sub;
|
|
|
|
|
|
|
|
rebuildCosmeticRow();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Sub-category pill row for the currently selected bone-group tab. Reuses the
|
|
|
|
|
|
|
|
* shared-tab filter bar layout constants so the row lines up visually. */
|
|
|
|
|
|
|
|
private void buildBoneGroupFilterBar(String[] subs) {
|
|
|
|
|
|
|
|
int fx = COSMETIC_ROW_LEFT;
|
|
|
|
|
|
|
|
// "All" first — null subFilter means show every sub-category in the group.
|
|
|
|
|
|
|
|
Button allBtn = Button.builder(
|
|
|
|
|
|
|
|
Component.literal("All"),
|
|
|
|
|
|
|
|
b -> setBoneGroupSubFilter(null)
|
|
|
|
|
|
|
|
).bounds(fx, SHARED_FILTER_Y, SHARED_FILTER_BTN_W, SHARED_FILTER_H).build();
|
|
|
|
|
|
|
|
allBtn.active = selectedSubFilter != null;
|
|
|
|
|
|
|
|
addRenderableWidget(allBtn);
|
|
|
|
|
|
|
|
filterPillButtons.add(allBtn);
|
|
|
|
|
|
|
|
fx += SHARED_FILTER_BTN_W + SHARED_FILTER_BTN_GAP;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (String sub : subs) {
|
|
|
|
|
|
|
|
final String key = sub;
|
|
|
|
|
|
|
|
Button pill = Button.builder(
|
|
|
|
|
|
|
|
Component.literal(capitalize(sub)),
|
|
|
|
|
|
|
|
b -> setBoneGroupSubFilter(key)
|
|
|
|
|
|
|
|
).bounds(fx, SHARED_FILTER_Y, SHARED_FILTER_BTN_W, SHARED_FILTER_H).build();
|
|
|
|
|
|
|
|
pill.active = !sub.equals(selectedSubFilter);
|
|
|
|
|
|
|
|
addRenderableWidget(pill);
|
|
|
|
|
|
|
|
filterPillButtons.add(pill);
|
|
|
|
|
|
|
|
fx += SHARED_FILTER_BTN_W + SHARED_FILTER_BTN_GAP;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static String capitalize(String s) {
|
|
|
|
|
|
|
|
return s.isEmpty() ? s : Character.toUpperCase(s.charAt(0)) + s.substring(1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void rebuildCosmeticRow() {
|
|
|
|
private void rebuildCosmeticRow() {
|
|
|
|
for (Button btn : cosmeticButtons) removeWidget(btn);
|
|
|
|
for (Button btn : cosmeticButtons) removeWidget(btn);
|
|
|
|
cosmeticButtons.clear();
|
|
|
|
cosmeticButtons.clear();
|
|
|
|
|
|
|
|
for (Button btn : filterPillButtons) removeWidget(btn);
|
|
|
|
|
|
|
|
filterPillButtons.clear();
|
|
|
|
sharedButtonHashes.clear();
|
|
|
|
sharedButtonHashes.clear();
|
|
|
|
infoButtonHashes.clear();
|
|
|
|
infoButtonHashes.clear();
|
|
|
|
|
|
|
|
|
|
|
|
@ -319,13 +445,30 @@ public class ClosetScreen extends Screen {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Bone-group tab? Show sub-filter pills and compute the allowed category set.
|
|
|
|
|
|
|
|
// Standalone tabs (particle) use a single-category set matching the tab name.
|
|
|
|
|
|
|
|
String[] groupSubs = BONE_GROUPS.get(selectedCategory);
|
|
|
|
|
|
|
|
if (groupSubs != null) {
|
|
|
|
|
|
|
|
buildBoneGroupFilterBar(groupSubs);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
java.util.Set<String> allowed = new java.util.HashSet<>();
|
|
|
|
|
|
|
|
if (groupSubs != null) {
|
|
|
|
|
|
|
|
if (selectedSubFilter != null) {
|
|
|
|
|
|
|
|
allowed.add(selectedSubFilter);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
for (String s : groupSubs) allowed.add(s);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
allowed.add(selectedCategory);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int x = COSMETIC_ROW_LEFT - cosmeticScroll;
|
|
|
|
int x = COSMETIC_ROW_LEFT - cosmeticScroll;
|
|
|
|
int y = this.height - COSMETIC_ROW_BOTTOM_MARGIN;
|
|
|
|
int y = this.height - COSMETIC_ROW_BOTTOM_MARGIN;
|
|
|
|
|
|
|
|
|
|
|
|
// Authored cosmetics for this category.
|
|
|
|
// Authored cosmetics whose category falls in the allowed set.
|
|
|
|
for (Map.Entry<Identifier, CosmeticDefinition> entry : CosmeticCache.catalog.entrySet()) {
|
|
|
|
for (Map.Entry<Identifier, CosmeticDefinition> entry : CosmeticCache.catalog.entrySet()) {
|
|
|
|
CosmeticDefinition def = entry.getValue();
|
|
|
|
CosmeticDefinition def = entry.getValue();
|
|
|
|
if (!selectedCategory.equals(def.category())) continue;
|
|
|
|
if (!allowed.contains(def.category())) continue;
|
|
|
|
|
|
|
|
|
|
|
|
Identifier id = entry.getKey();
|
|
|
|
Identifier id = entry.getKey();
|
|
|
|
// No text label — the mini-player preview + green-border-when-equipped already
|
|
|
|
// No text label — the mini-player preview + green-border-when-equipped already
|
|
|
|
@ -355,7 +498,10 @@ public class ClosetScreen extends Screen {
|
|
|
|
: CosmeticLibrary.EMPTY;
|
|
|
|
: CosmeticLibrary.EMPTY;
|
|
|
|
|
|
|
|
|
|
|
|
for (CosmeticLibraryEntry libEntry : lib.entries()) {
|
|
|
|
for (CosmeticLibraryEntry libEntry : lib.entries()) {
|
|
|
|
if (!BoneExtraction.categoriesFor(libEntry.bones()).contains(selectedCategory)) continue;
|
|
|
|
java.util.Set<String> entryCats = BoneExtraction.categoriesFor(libEntry.bones());
|
|
|
|
|
|
|
|
boolean overlap = false;
|
|
|
|
|
|
|
|
for (String c : entryCats) { if (allowed.contains(c)) { overlap = true; break; } }
|
|
|
|
|
|
|
|
if (!overlap) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Pull from server if the blob hasn't materialized locally yet.
|
|
|
|
// Pull from server if the blob hasn't materialized locally yet.
|
|
|
|
ClientSharedCosmeticCache.requestIfMissing(libEntry.hash());
|
|
|
|
ClientSharedCosmeticCache.requestIfMissing(libEntry.hash());
|
|
|
|
@ -597,7 +743,7 @@ public class ClosetScreen extends Screen {
|
|
|
|
* Derive the equip category from the cosmetic's primary bone and toggle equip in
|
|
|
|
* Derive the equip category from the cosmetic's primary bone and toggle equip in
|
|
|
|
* that slot — matches the authored path's click-to-unequip behavior. An empty hash
|
|
|
|
* that slot — matches the authored path's click-to-unequip behavior. An empty hash
|
|
|
|
* on the wire tells the server to clear the slot (see
|
|
|
|
* on the wire tells the server to clear the slot (see
|
|
|
|
* {@link com.razz.dfashion.cosmetic.share.CosmeticShareNetwork#onAssignShared}).
|
|
|
|
* {@code CosmeticShareNetwork.onAssignShared}).
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
private void equipShared(CosmeticLibraryEntry entry) {
|
|
|
|
private void equipShared(CosmeticLibraryEntry entry) {
|
|
|
|
String category = BoneExtraction.primaryCategory(entry.bones());
|
|
|
|
String category = BoneExtraction.primaryCategory(entry.bones());
|
|
|
|
@ -1090,6 +1236,15 @@ public class ClosetScreen extends Screen {
|
|
|
|
LocalPlayer player = mc.player;
|
|
|
|
LocalPlayer player = mc.player;
|
|
|
|
if (player == null) return;
|
|
|
|
if (player == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Server-synced config may have flipped mid-session (op edited the TOML). Rebuild
|
|
|
|
|
|
|
|
// the entire screen so tabs/toggles appear or disappear to match.
|
|
|
|
|
|
|
|
boolean sharedNow = DecoFashionConfig.SHARED_LIBRARY_ENABLED.get();
|
|
|
|
|
|
|
|
boolean hideNow = DecoFashionConfig.BODY_HIDE_ENABLED.get();
|
|
|
|
|
|
|
|
if (sharedNow != lastSharedEnabled || hideNow != lastBodyHideEnabled) {
|
|
|
|
|
|
|
|
this.rebuildWidgets();
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// When on the skin tab, rebuild the grid when the cached-skin count changes — this
|
|
|
|
// When on the skin tab, rebuild the grid when the cached-skin count changes — this
|
|
|
|
// covers uploads (which add a new entry asynchronously after the server echo arrives)
|
|
|
|
// covers uploads (which add a new entry asynchronously after the server echo arrives)
|
|
|
|
// and any new skin received from another player.
|
|
|
|
// and any new skin received from another player.
|
|
|
|
@ -1375,12 +1530,25 @@ public class ClosetScreen extends Screen {
|
|
|
|
Map<String, com.razz.dfashion.cosmetic.CosmeticRef> liveEquipped =
|
|
|
|
Map<String, com.razz.dfashion.cosmetic.CosmeticRef> liveEquipped =
|
|
|
|
player.getData(CosmeticAttachments.EQUIPPED.get());
|
|
|
|
player.getData(CosmeticAttachments.EQUIPPED.get());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Must match the allowed-set logic in rebuildCosmeticRow so button indices line up.
|
|
|
|
|
|
|
|
String[] groupSubs = BONE_GROUPS.get(selectedCategory);
|
|
|
|
|
|
|
|
java.util.Set<String> allowedCats = new java.util.HashSet<>();
|
|
|
|
|
|
|
|
if (groupSubs != null) {
|
|
|
|
|
|
|
|
if (selectedSubFilter != null) {
|
|
|
|
|
|
|
|
allowedCats.add(selectedSubFilter);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
for (String s : groupSubs) allowedCats.add(s);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
allowedCats.add(selectedCategory);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Authored loop first. Shared entries appended to this category tab render via
|
|
|
|
// Authored loop first. Shared entries appended to this category tab render via
|
|
|
|
// renderSharedPreviews below — they're not matched by the catalog iteration.
|
|
|
|
// renderSharedPreviews below — they're not matched by the catalog iteration.
|
|
|
|
int idx = 0;
|
|
|
|
int idx = 0;
|
|
|
|
for (Map.Entry<Identifier, CosmeticDefinition> entry : CosmeticCache.catalog.entrySet()) {
|
|
|
|
for (Map.Entry<Identifier, CosmeticDefinition> entry : CosmeticCache.catalog.entrySet()) {
|
|
|
|
CosmeticDefinition def = entry.getValue();
|
|
|
|
CosmeticDefinition def = entry.getValue();
|
|
|
|
if (!selectedCategory.equals(def.category())) continue;
|
|
|
|
if (!allowedCats.contains(def.category())) continue;
|
|
|
|
if (idx >= cosmeticButtons.size()) break;
|
|
|
|
if (idx >= cosmeticButtons.size()) break;
|
|
|
|
|
|
|
|
|
|
|
|
Button btn = cosmeticButtons.get(idx);
|
|
|
|
Button btn = cosmeticButtons.get(idx);
|
|
|
|
@ -1514,7 +1682,7 @@ public class ClosetScreen extends Screen {
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Same 3D-mini-player preview pattern as cosmetics, but per-box we swap the render state's
|
|
|
|
* Same 3D-mini-player preview pattern as cosmetics, but per-box we swap the render state's
|
|
|
|
* {@code skin} to a specific cached texture so each box shows a different skin variant.
|
|
|
|
* {@code skin} to a specific cached texture so each box shows a different skin variant.
|
|
|
|
* {@link SkinInfoOverride} runs at tick-level on {@link PlayerInfo}, not on these
|
|
|
|
* {@link SkinInfoOverride} runs at tick-level on {@link net.minecraft.client.multiplayer.PlayerInfo}, not on these
|
|
|
|
* preview states, so our per-box swap here isn't clobbered.
|
|
|
|
* preview states, so our per-box swap here isn't clobbered.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
private void renderSkinPreviews(GuiGraphicsExtractor graphics, LocalPlayer player) {
|
|
|
|
private void renderSkinPreviews(GuiGraphicsExtractor graphics, LocalPlayer player) {
|
|
|
|
|