Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f488a89
feat: add Minecraft capes and cape override handling to the wardrobe
Deftu Oct 14, 2026
577c935
fix: resync local equipment when another client changes it
Deftu Oct 14, 2026
c80da87
feat: rebuild the wardrobe layout and merge the store into it
Deftu Oct 14, 2026
cd81e0b
feat: add an emote wheel editor with multiple wheels
Deftu Oct 14, 2026
f9279d4
feat: rename the cosmetics screen to Wardrobe and add a keybind to op…
Deftu Oct 14, 2026
d4794a9
fix: hide the cape in legacy previews when none is selected
Deftu Oct 14, 2026
621844f
feat: let player previews share a persistent rotation
Deftu Oct 14, 2026
3db3c6c
feat: build variant swatch art from cosmetic textures
Deftu Oct 14, 2026
f37e896
feat: detect emote wheel keybind conflicts
Deftu Oct 14, 2026
68faf09
feat: add variant swatches, card menus and a popularity sort to the w…
Deftu Oct 14, 2026
fb041a7
fix: fall back to the offline UUID when the session has none
Deftu Oct 14, 2026
83456e9
feat: add outfit and skin libraries backed by the PolyPlus API
Deftu Oct 14, 2026
5ef63bb
feat: add outfits, skins and an outfit switcher to the wardrobe
Deftu Oct 14, 2026
7a121dc
feat: replace the wardrobe sidebar with header chip rows
Deftu Oct 14, 2026
29cd6c8
feat: wrap cosmetic variants as chips across the full width
Deftu Oct 14, 2026
959ae02
feat: show four cards per wardrobe row
Deftu Oct 14, 2026
178ff53
feat: rework the cape headings and force toggle
Deftu Oct 14, 2026
8722e02
feat: give players eight starter outfits
Deftu Oct 14, 2026
36d4810
fix: drop outfits the server no longer has
Deftu Oct 14, 2026
41beb77
fix: read store prices from the sale object
Deftu Oct 14, 2026
1d863f7
fix: address wardrobe review feedback
Deftu Oct 15, 2026
5bce3c3
fix: harden outfit adoption, cape state and store loading
Deftu Oct 15, 2026
6fcb58e
chore: bump OneConfig to 1.4.3
Deftu Oct 20, 2026
43bf6c0
fix: restore wardrobe side padding
Deftu Oct 20, 2026
43a342a
Merge remote-tracking branch 'origin/main' into feat/wardrobe
Deftu Oct 20, 2026
b5220a6
chore: bump OneConfig to 1.4.4
Deftu Oct 21, 2026
4c0909f
feat(wardrobe): preview the selected item on the player model
Deftu Oct 21, 2026
43b6464
chore: bump OneConfig to 1.5.0 and Kotlin to 2.4.20
Deftu Oct 21, 2026
10c0d80
refactor: use the public OneConfig theme and components
Deftu Oct 21, 2026
8da846a
refactor: register the wardrobe page and OneConfig UI hooks through t…
Deftu Oct 21, 2026
1c65150
fix: keep the mod order toggle inside the page gutter
Deftu Oct 21, 2026
2235279
fix(wardrobe): show the outfit switcher disabled when outfits are una…
Deftu Oct 21, 2026
66c351b
fix(wardrobe): retry skin thumbnails after a failed download
Deftu Oct 21, 2026
0467793
fix(outfits): clear the Minecraft cape when its state could not be read
Deftu Oct 21, 2026
304d0fa
feat(preview): support rendering a chosen skin and aura color
Deftu Oct 21, 2026
d9c1e33
fix(wardrobe): show a new Minecraft skin as soon as it is changed
Deftu Oct 21, 2026
9ae68f6
fix(wardrobe): preview the selected outfit or skin instead of the cur…
Deftu Oct 21, 2026
63006a4
fix(wardrobe): show each outfit's saved skin and aura color in its th…
Deftu Oct 21, 2026
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
6 changes: 3 additions & 3 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ pluginManagement {
}

plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.serialization") version "2.4.10"
kotlin("plugin.compose") version "2.4.10"
kotlin("jvm") version "2.4.20"
kotlin("plugin.serialization") version "2.4.20"
kotlin("plugin.compose") version "2.4.20"
id("com.gradleup.shadow") version "9.6.1"
id("me.modmuss50.mod-publish-plugin") version "2.2.0"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

//? if > 1.8.9 {
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.mojang.authlib.GameProfile;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.client.player.AbstractClientPlayer;
import org.polyfrost.polyplus.client.cosmetics.CosmeticAssetCache;
import org.polyfrost.polyplus.client.cosmetics.CapeResolver;
import org.polyfrost.polyplus.client.cosmetics.CosmeticEquipment;
import org.polyfrost.polyplus.client.cosmetics.LocalSkinOverride;
import org.polyfrost.polyplus.client.cosmetics.access.PlayerCosmeticsAccess;
import org.polyfrost.polyplus.client.cosmetics.access.PlayerEmotesAccess;
import org.polyfrost.polyplus.client.emotes.playback.EmoteController;
Expand All @@ -16,16 +18,17 @@
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import java.util.UUID;

//? if >= 1.21.10 {
import net.minecraft.core.ClientAsset;
import net.minecraft.world.entity.player.PlayerSkin;
//?}

//? if < 1.21.10 {
/*import net.minecraft.client.resources.PlayerSkin;
*///?}

@Mixin(AbstractClientPlayer.class)
@Mixin(value = AbstractClientPlayer.class, priority = 1500)
public abstract class MixinAbstractClientPlayer implements PlayerEmotesAccess, PlayerCosmeticsAccess {
@Shadow
private PlayerInfo playerInfo;
Expand Down Expand Up @@ -67,50 +70,40 @@ public abstract class MixinAbstractClientPlayer implements PlayerEmotesAccess, P
return original;
}

GameProfile profile = this.playerInfo.getProfile();
//~ if >= 1.21.10 'getId' -> 'id'
var capeLocation = CosmeticAssetCache.getCapeTexture(this.playerInfo.getProfile().id());
if (capeLocation == null) {
return original;
}

return new PlayerSkin(
// FIXME: how to simplify this
//? if >= 1.21.10 {
original.body(),
new ClientAsset.ResourceTexture(capeLocation),
original.elytra(),
original.model(),
original.secure()
//?} else {
/*original.texture(),
original.textureUrl(),
capeLocation,
original.elytraTexture(),
original.model(),
original.secure()
*///?}
);
UUID player = profile.id();
return LocalSkinOverride.apply(player, CapeResolver.applyCape(player, profile, original));
}
}
//?} else {
/*import com.mojang.authlib.GameProfile;
/*import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.mojang.authlib.GameProfile;
import net.minecraft.client.entity.living.player.ClientPlayerEntity;
import net.minecraft.client.network.PlayerInfo;
import net.minecraft.entity.living.player.PlayerEntity;
import net.minecraft.resources.Identifier;
import net.minecraft.world.World;
import org.polyfrost.polyplus.client.PolyPlusConfig;
import org.polyfrost.polyplus.client.cosmetics.CapeResolver;
import org.polyfrost.polyplus.client.cosmetics.CosmeticAssetCache;
import org.polyfrost.polyplus.client.cosmetics.CosmeticEquipment;
import org.polyfrost.polyplus.client.cosmetics.LocalSkinOverride;
import org.polyfrost.polyplus.client.cosmetics.MojangCapeService;
import org.polyfrost.polyplus.client.cosmetics.access.PlayerCosmeticsAccess;
import org.polyfrost.polyplus.client.cosmetics.access.PlayerEmotesAccess;
import org.polyfrost.polyplus.client.emotes.playback.EmoteController;
import org.polyfrost.polyplus.client.gui.preview.PreviewSkin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(ClientPlayerEntity.class)
import java.util.UUID;

@Mixin(value = ClientPlayerEntity.class, priority = 1500)
public abstract class MixinAbstractClientPlayer extends PlayerEntity implements PlayerEmotesAccess, PlayerCosmeticsAccess {
@Unique
private EmoteController polyplus$emoteController;
Expand All @@ -122,6 +115,9 @@ private MixinAbstractClientPlayer(World world, GameProfile profile) {
super(world, profile);
}

@Shadow
protected abstract PlayerInfo getInfo();

@Override
public EmoteController polyplus$emoteController() {
EmoteController controller = polyplus$emoteController;
Expand All @@ -148,10 +144,40 @@ public void tick() {
polyplus$emoteController().tick((ClientPlayerEntity) (Object) this);
}

@Inject(method = "getCapeTextureLocation", at = @At("HEAD"), cancellable = true)
private void polyplus$replaceCapeTexture(CallbackInfoReturnable<Identifier> cir) {
Identifier cape = CosmeticAssetCache.getCapeTexture(((ClientPlayerEntity) (Object) this).getUuid());
if (cape != null) cir.setReturnValue(cape);
@ModifyReturnValue(method = "getCapeTextureLocation", at = @At("RETURN"))
private Identifier polyplus$replaceCapeTexture(Identifier original) {
UUID playerId = ((ClientPlayerEntity) (Object) this).getUuid();
switch (CapeResolver.decide(playerId, original == null ? null : original.getNamespace())) {
case POLYPLUS:
return polyplus$orOriginal(CosmeticAssetCache.getCapeTexture(playerId), original);
case MOJANG:
return polyplus$orOriginal(MojangCapeService.localCapeTexture(), original);
case VANILLA:
PlayerInfo info = getInfo();
return info == null ? null : info.getCapeTexture();
case NONE:
return null;
default:
return original;
}
}

@ModifyReturnValue(method = "getSkinTextureLocation()Lnet/minecraft/resource/Identifier;", at = @At("RETURN"))
private Identifier polyplus$replaceSkinTexture(Identifier original) {
PreviewSkin skin = LocalSkinOverride.forPlayer(((ClientPlayerEntity) (Object) this).getUuid());
return skin != null ? skin.getTexture() : original;
}

@ModifyReturnValue(method = "getModelType", at = @At("RETURN"))
private String polyplus$replaceSkinModel(String original) {
PreviewSkin skin = LocalSkinOverride.forPlayer(((ClientPlayerEntity) (Object) this).getUuid());
if (skin == null) return original;
return skin.getSlim() ? "slim" : "default";
}

@Unique
private static Identifier polyplus$orOriginal(Identifier cape, Identifier original) {
return cape != null ? cape : original;
}

@Inject(method = "getFovModifier", at = @At("HEAD"), cancellable = true)
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.polyfrost.polyplus.client.features.DefaultSettings
import org.polyfrost.polyplus.client.features.JvmAdvisor
import org.polyfrost.polyplus.client.features.ModpackDiff
import org.polyfrost.polyplus.client.features.OnboardingFeatures
import org.polyfrost.polyplus.client.gui.PolyPlusOneConfigIntegration
import org.polyfrost.polyplus.client.gui.VanillaMenuButton
import org.polyfrost.polyplus.client.gui.preview.PlayerPreviewRenderer
import org.polyfrost.polyplus.client.host.HostWorldManager
Expand Down Expand Up @@ -164,6 +165,7 @@ object PolyPlusClient {
}
step("rich text privacy") { RichTextPrivacy.warmUp() }
step("default settings") { DefaultSettings.initialize() }
step("oneconfig ui") { PolyPlusOneConfigIntegration.register() }
step("default mod order") { DefaultModOrder.initialize() }
step("advanced mod cards") { AdvancedModCards.initialize() }
step("onboarding") { OnboardingFeatures.initialize() }
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/org/polyfrost/polyplus/client/PolyPlusConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.polyfrost.oneconfig.api.ui.v1.keybind.OneConfigKeybind
import org.polyfrost.polyplus.BackendUrl
import org.polyfrost.polyplus.PolyPlusConstants
import org.polyfrost.polyplus.client.emotes.EmoteWheelKeybind
import org.polyfrost.polyplus.client.gui.PolyPlusOneConfigIntegration
import org.polyfrost.polyplus.client.network.websocket.PolyConnection
import org.polyfrost.polyplus.client.privacy.PrivacyEnforcement
import org.polyfrost.polyplus.client.social.SocialOverlay
Expand Down Expand Up @@ -224,6 +225,18 @@ object PolyPlusConfig : Config("${PolyPlusConstants.ID}.json", "${PolyPlusConsta
true
}

@Keybind(
title = "Open Wardrobe",
subcategory = "Keybinds",
description = "Open the PolyPlus wardrobe",
)
var wardrobeKeybind = OneConfigKeybind(null, null, KeyModifiers.NONE, 0L) { state ->
if (state) {
PolyPlusOneConfigIntegration.openCosmeticsFromKeybind()
}
true
}

@Dropdown(title = "API URL", description = "The URL used for the PolyPlus API. Only change if you know what you're doing.")
var apiUrl: BackendUrl = BackendUrl.PRODUCTION
get() = if (PolyPlusConstants.IS_DEV_ENV) field else BackendUrl.PRODUCTION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ object PolyPlusCosmeticsConfig : Config(
)
var hideFeetCosmeticsWithBoots = true

@JvmStatic
@Switch(
title = "Force PolyPlus Capes",
description = "Show the capes picked in the wardrobe even when another mod sets its own cape.",
)
var forcePolyPlusCapes = false

@JvmStatic
@Dropdown(
title = "Capes",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ object PolyPlusMainMenuConfig : Config(

@JvmStatic
@Switch(
title = "Hide Cosmetics Button",
description = "Hide the Cosmetics button on the PolyPlus main menu.",
title = "Hide Wardrobe Button",
description = "Hide the Wardrobe button on the PolyPlus main menu.",
subcategory = "Elements",
)
var hideMainMenuCosmetics = false
Expand Down
Loading
Loading