From 76e801c944ef8c8917ad1564df349a13d643b7e1 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Sat, 10 Jan 2026 14:55:31 +0300 Subject: [PATCH 1/8] use parchment --- common/build.gradle | 2 +- fabric/build.gradle | 2 +- forge/build.gradle | 3 +-- gradle.properties | 4 ++-- neoforge/build.gradle | 8 ++++---- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/common/build.gradle b/common/build.gradle index 88b391d..c168537 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -8,7 +8,7 @@ dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" mappings loom.layered { officialMojangMappings() -// parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") + parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") } compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5' diff --git a/fabric/build.gradle b/fabric/build.gradle index 27c744a..62adc91 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -10,7 +10,7 @@ dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" mappings loom.layered { officialMojangMappings() -// parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") + parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") } modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" diff --git a/forge/build.gradle b/forge/build.gradle index 588a178..c52c1d9 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -24,8 +24,7 @@ tasks.named('jarJar') { jar.finalizedBy('jarJar') minecraft { - mappings channel: 'official', version: minecraft_version -// mappings channel: 'parchment', version: "${parchment_minecraft}-${parchment_version}-${minecraft_version}" + mappings channel: 'parchment', version: "${parchment_minecraft}-${parchment_version}-${minecraft_version}" copyIdeResources = true //Calls processResources when in dev diff --git a/gradle.properties b/gradle.properties index 7f59327..8b7bb95 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,8 +21,8 @@ description=A server-side mod for managing skins. mineskin_client_version=3.0.6-SNAPSHOT # ParchmentMC mappings, see https://parchmentmc.org/docs/getting-started#choose-a-version for new versions -parchment_minecraft=1.21.8 -parchment_version=2025.09.14 +parchment_minecraft=1.21.11 +parchment_version=2025.12.20 # Publishing curseforge_id=443823 diff --git a/neoforge/build.gradle b/neoforge/build.gradle index 7caa74f..0c9aca9 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -17,10 +17,10 @@ neoForge { accessTransformers.from(at.absolutePath) } -// parchment { -// minecraftVersion = parchment_minecraft -// mappingsVersion = parchment_version -// } + parchment { + minecraftVersion = parchment_minecraft + mappingsVersion = parchment_version + } runs { configureEach { From ffd2a0dba741877cb8844f5e373a55dfccb95788 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Mon, 12 Jan 2026 23:28:32 +0300 Subject: [PATCH 2/8] update mineskin --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8b7bb95..8785f81 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ credits= description=A server-side mod for managing skins. # Dependencies -mineskin_client_version=3.0.6-SNAPSHOT +mineskin_client_version=3.2.1-SNAPSHOT # ParchmentMC mappings, see https://parchmentmc.org/docs/getting-started#choose-a-version for new versions parchment_minecraft=1.21.11 From e832e02fd4c37421929bec15a6400f2017a0a060 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Tue, 13 Jan 2026 02:04:21 +0300 Subject: [PATCH 3/8] add collection provider --- .../lionarius/skinrestorer/SkinRestorer.java | 2 + .../config/FirstJoinSkinProvider.java | 5 +- .../provider/BuiltInProviderConfig.java | 7 +- .../config/provider/ProvidersConfig.java | 17 +++- .../collection/CollectionProviderConfig.java | 32 +++++++ .../collection/CollectionSkinFile.java | 60 ++++++++++++ .../collection/CollectionSkinSource.java | 12 +++ .../CollectionSkinSourceListDeserializer.java | 30 ++++++ .../collection/CollectionSkinUrl.java | 41 +++++++++ .../ServerLoginPacketListenerImplMixin.java | 11 ++- .../skinrestorer/skin/SkinVariant.java | 5 +- .../skin/provider/CollectionSkinProvider.java | 91 +++++++++++++++++++ .../skin/provider/MineskinSkinProvider.java | 11 ++- .../skin/provider/SkinProvider.java | 2 + 14 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionProviderConfig.java create mode 100644 common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinFile.java create mode 100644 common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSource.java create mode 100644 common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java create mode 100644 common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinUrl.java create mode 100644 common/src/main/java/net/lionarius/skinrestorer/skin/provider/CollectionSkinProvider.java diff --git a/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java b/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java index 1220221..43e2ef1 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java +++ b/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java @@ -88,6 +88,7 @@ public final class SkinRestorer { SkinRestorer.registerDefaultSkinProvider(MojangSkinProvider.PROVIDER_NAME, SkinProvider.MOJANG, SkinRestorer.getConfig().providersConfig().mojang()); SkinRestorer.registerDefaultSkinProvider(ElyBySkinProvider.PROVIDER_NAME, SkinProvider.ELY_BY, SkinRestorer.getConfig().providersConfig().ely_by()); SkinRestorer.registerDefaultSkinProvider(MineskinSkinProvider.PROVIDER_NAME, SkinProvider.MINESKIN, SkinRestorer.getConfig().providersConfig().mineskin()); + SkinRestorer.registerDefaultSkinProvider(CollectionSkinProvider.PROVIDER_NAME, SkinProvider.COLLECTION, SkinRestorer.getConfig().providersConfig().collection()); } private static void registerDefaultSkinProvider(String defaultName, SkinProvider provider, BuiltInProviderConfig config) { @@ -106,6 +107,7 @@ public final class SkinRestorer { MojangSkinProvider.reload(); ElyBySkinProvider.reload(); MineskinSkinProvider.reload(); + CollectionSkinProvider.reload(); } public static Collection applySkin(MinecraftServer server, Iterable targets, SkinValue value, boolean save) { diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/FirstJoinSkinProvider.java b/common/src/main/java/net/lionarius/skinrestorer/config/FirstJoinSkinProvider.java index 120f8de..64dc581 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/config/FirstJoinSkinProvider.java +++ b/common/src/main/java/net/lionarius/skinrestorer/config/FirstJoinSkinProvider.java @@ -1,6 +1,7 @@ package net.lionarius.skinrestorer.config; import com.google.gson.annotations.SerializedName; +import net.lionarius.skinrestorer.skin.provider.CollectionSkinProvider; import net.lionarius.skinrestorer.skin.provider.ElyBySkinProvider; import net.lionarius.skinrestorer.skin.provider.MojangSkinProvider; @@ -8,7 +9,9 @@ public enum FirstJoinSkinProvider { @SerializedName(value = "MOJANG", alternate = {"mojang"}) MOJANG(MojangSkinProvider.PROVIDER_NAME), @SerializedName(value = "ELY.BY", alternate = {"ely.by", "ELY_BY", "ely_by"}) - ELY_BY(ElyBySkinProvider.PROVIDER_NAME); + ELY_BY(ElyBySkinProvider.PROVIDER_NAME), + @SerializedName(value = "COLLECTION", alternate = {"collection"}) + COLLECTION(CollectionSkinProvider.PROVIDER_NAME); private final String name; diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/BuiltInProviderConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/BuiltInProviderConfig.java index c27e84c..3ea7a7c 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/config/provider/BuiltInProviderConfig.java +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/BuiltInProviderConfig.java @@ -8,8 +8,13 @@ public abstract class BuiltInProviderConfig implements GsonPostProcessable { protected String name; protected CacheConfig cache; + public BuiltInProviderConfig(String name, CacheConfig cache) { - this.enabled = true; + this(name, cache, true); + } + + public BuiltInProviderConfig(String name, CacheConfig cache, boolean enabled) { + this.enabled = enabled; this.name = name; this.cache = cache; } diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/ProvidersConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/ProvidersConfig.java index 4e4f69b..e1de7f1 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/config/provider/ProvidersConfig.java +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/ProvidersConfig.java @@ -1,23 +1,27 @@ package net.lionarius.skinrestorer.config.provider; import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.config.provider.collection.CollectionProviderConfig; import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; public final class ProvidersConfig implements GsonPostProcessable { public static final ProvidersConfig DEFAULT = new ProvidersConfig( new MojangProviderConfig(), new ElyByProviderConfig(), - new MineskinProviderConfig() + new MineskinProviderConfig(), + new CollectionProviderConfig() ); private MojangProviderConfig mojang; private ElyByProviderConfig ely_by; private MineskinProviderConfig mineskin; + private CollectionProviderConfig collection; - public ProvidersConfig(MojangProviderConfig mojang, ElyByProviderConfig ely_by, MineskinProviderConfig mineskin) { + public ProvidersConfig(MojangProviderConfig mojang, ElyByProviderConfig ely_by, MineskinProviderConfig mineskin, CollectionProviderConfig collection) { this.mojang = mojang; this.ely_by = ely_by; this.mineskin = mineskin; + this.collection = collection; } public MojangProviderConfig mojang() { @@ -32,6 +36,10 @@ public final class ProvidersConfig implements GsonPostProcessable { return this.mineskin; } + public CollectionProviderConfig collection() { + return this.collection; + } + @Override public void gsonPostProcess() { if (this.mojang == null) { @@ -48,5 +56,10 @@ public final class ProvidersConfig implements GsonPostProcessable { SkinRestorer.LOGGER.warn("Mineskin provider config is null, using default"); this.mineskin = ProvidersConfig.DEFAULT.mineskin(); } + + if (this.collection == null) { + SkinRestorer.LOGGER.warn("Collection provider config is null, using default"); + this.collection = ProvidersConfig.DEFAULT.collection(); + } } } diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionProviderConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionProviderConfig.java new file mode 100644 index 0000000..45d5c95 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionProviderConfig.java @@ -0,0 +1,32 @@ +package net.lionarius.skinrestorer.config.provider.collection; + +import com.google.gson.annotations.JsonAdapter; +import net.lionarius.skinrestorer.config.provider.BuiltInProviderConfig; +import net.lionarius.skinrestorer.config.provider.CacheConfig; +import net.lionarius.skinrestorer.skin.provider.CollectionSkinProvider; +import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; + +import java.util.ArrayList; +import java.util.List; + +public final class CollectionProviderConfig extends BuiltInProviderConfig implements GsonPostProcessable { + private static final CacheConfig DEFAULT_CACHE_VALUE = new CacheConfig(true, 604800); + + @JsonAdapter(CollectionSkinSourceListDeserializer.class) + private List sources = new ArrayList<>(); + + public CollectionProviderConfig() { + super(CollectionSkinProvider.PROVIDER_NAME, DEFAULT_CACHE_VALUE, false); + } + + public List sources() { + return this.sources; + } + + @Override + public void gsonPostProcess() { + if (this.sources == null) { + this.sources = new ArrayList<>(); + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinFile.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinFile.java new file mode 100644 index 0000000..535e3a2 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinFile.java @@ -0,0 +1,60 @@ +package net.lionarius.skinrestorer.config.provider.collection; + +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.skin.SkinVariant; +import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class CollectionSkinFile implements CollectionSkinSource, GsonPostProcessable { + private String path = ""; + private SkinVariant variant = SkinVariant.CLASSIC; + + @Override + public @Nullable URI uri() { + if (this.path.isEmpty()) + return null; + + try { + var filePath = SkinRestorer.getConfigDir().resolve(this.path); + + if (!Files.exists(filePath)) { + SkinRestorer.LOGGER.warn("Skin file does not exist: {}", this.path); + return null; + } + + if (!Files.isRegularFile(filePath)) { + SkinRestorer.LOGGER.warn("Skin path is not a file: {}", this.path); + return null; + } + + if (!this.path.toLowerCase().endsWith(".png")) { + SkinRestorer.LOGGER.warn("Skin file is not a PNG file: {}", this.path); + return null; + } + + return filePath.toUri(); + } catch (Exception e) { + SkinRestorer.LOGGER.warn("Invalid file path: {}", this.path, e); + return null; + } + } + + @Override + public SkinVariant variant() { + return this.variant; + } + + @Override + public void gsonPostProcess() { + if (this.path == null) { + this.path = ""; + } + if (this.variant == null) { + this.variant = SkinVariant.CLASSIC; + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSource.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSource.java new file mode 100644 index 0000000..1a8579b --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSource.java @@ -0,0 +1,12 @@ +package net.lionarius.skinrestorer.config.provider.collection; + +import net.lionarius.skinrestorer.skin.SkinVariant; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; + +public interface CollectionSkinSource { + @Nullable URI uri(); + + SkinVariant variant(); +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java new file mode 100644 index 0000000..9535c66 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java @@ -0,0 +1,30 @@ +package net.lionarius.skinrestorer.config.provider.collection; + +import com.google.gson.*; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class CollectionSkinSourceListDeserializer implements JsonDeserializer> { + @Override + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + List sources = new ArrayList<>(); + + if (json.isJsonArray()) { + for (JsonElement element : json.getAsJsonArray()) { + if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + if (obj.has("url")) { + sources.add(context.deserialize(obj, CollectionSkinUrl.class)); + } else if (obj.has("path")) { + sources.add(context.deserialize(obj, CollectionSkinFile.class)); + } + } + } + } + + return sources; + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinUrl.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinUrl.java new file mode 100644 index 0000000..6607219 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinUrl.java @@ -0,0 +1,41 @@ +package net.lionarius.skinrestorer.config.provider.collection; + +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.skin.SkinVariant; +import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; + +public final class CollectionSkinUrl implements CollectionSkinSource, GsonPostProcessable { + private String url = ""; + private SkinVariant variant = SkinVariant.CLASSIC; + + @Override + public @Nullable URI uri() { + try { + if (this.url.isEmpty()) + return null; + + return new URI(this.url); + } catch (Exception e) { + SkinRestorer.LOGGER.warn("Invalid URI: {}", this.url, e); + return null; + } + } + + @Override + public SkinVariant variant() { + return this.variant; + } + + @Override + public void gsonPostProcess() { + if (this.url == null) { + this.url = ""; + } + if (this.variant == null) { + this.variant = SkinVariant.CLASSIC; + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/mixin/ServerLoginPacketListenerImplMixin.java b/common/src/main/java/net/lionarius/skinrestorer/mixin/ServerLoginPacketListenerImplMixin.java index 09a97dc..bda6c42 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/mixin/ServerLoginPacketListenerImplMixin.java +++ b/common/src/main/java/net/lionarius/skinrestorer/mixin/ServerLoginPacketListenerImplMixin.java @@ -2,6 +2,7 @@ package net.lionarius.skinrestorer.mixin; import com.mojang.authlib.GameProfile; import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.config.FirstJoinSkinProvider; import net.lionarius.skinrestorer.skin.SkinValue; import net.lionarius.skinrestorer.skin.provider.SkinProviderContext; import net.lionarius.skinrestorer.util.PlayerUtils; @@ -53,9 +54,15 @@ public abstract class ServerLoginPacketListenerImplMixin { return null; } - if (originalSkin == null && SkinRestorer.getConfig().fetchSkinOnFirstJoin()) { + var config = SkinRestorer.getConfig(); + var provider = config.firstJoinSkinProvider(); + + var shouldFetch = (originalSkin == null && config.fetchSkinOnFirstJoin()) || + (originalSkin != null && config.forceFirstJoinSkinFetch() && provider != FirstJoinSkinProvider.MOJANG); + + if (shouldFetch) { var context = new SkinProviderContext( - SkinRestorer.getConfig().firstJoinSkinProvider().getName(), + provider.getName(), profile.name(), null ); diff --git a/common/src/main/java/net/lionarius/skinrestorer/skin/SkinVariant.java b/common/src/main/java/net/lionarius/skinrestorer/skin/SkinVariant.java index d30d5a6..e0494ce 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/skin/SkinVariant.java +++ b/common/src/main/java/net/lionarius/skinrestorer/skin/SkinVariant.java @@ -1,8 +1,11 @@ package net.lionarius.skinrestorer.skin; +import com.google.gson.annotations.SerializedName; + public enum SkinVariant { - + @SerializedName(value = "classic", alternate = {"CLASSIC"}) CLASSIC("classic"), + @SerializedName(value = "slim", alternate = {"SLIM"}) SLIM("slim"); private final String name; diff --git a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/CollectionSkinProvider.java b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/CollectionSkinProvider.java new file mode 100644 index 0000000..89970c2 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/CollectionSkinProvider.java @@ -0,0 +1,91 @@ +package net.lionarius.skinrestorer.skin.provider; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.mojang.authlib.properties.Property; +import it.unimi.dsi.fastutil.Pair; +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.config.provider.collection.CollectionSkinSource; +import net.lionarius.skinrestorer.skin.SkinVariant; +import net.lionarius.skinrestorer.util.Result; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public final class CollectionSkinProvider implements SkinProvider { + + public static final String PROVIDER_NAME = "collection"; + + private static LoadingCache> SKIN_CACHE; + + private static List> COLLECTION_SKINS; + + public static void reload() { + COLLECTION_SKINS = loadCollectionSkins(); + + createCache(); + } + + private static List> loadCollectionSkins() { + List> skins = new ArrayList<>(); + + var config = SkinRestorer.getConfig().providersConfig().collection(); + + for (CollectionSkinSource source : config.sources()) { + var uri = source.uri(); + if (uri != null) { + skins.add(Pair.of(uri, source.variant())); + } + } + + return skins; + } + + private static void createCache() { + var config = SkinRestorer.getConfig().providersConfig().collection(); + var time = config.cache().enabled() ? config.cache().duration() : 0; + + SKIN_CACHE = CacheBuilder.newBuilder() + .expireAfterWrite(time, TimeUnit.SECONDS) + .build(new CacheLoader<>() { + @Override + public @NotNull Optional load(@NotNull Integer key) throws Exception { + var skinEntry = COLLECTION_SKINS.get(key); + return MineskinSkinProvider.loadSkin(skinEntry.first(), skinEntry.second()); + } + }); + } + + @Override + public String getArgumentName() { + return "seed"; + } + + @Override + public boolean hasVariantSupport() { + return false; + } + + @Override + public Result, Exception> fetchSkin(String argument, SkinVariant variant) { + if (COLLECTION_SKINS.isEmpty()) { + return Result.error(new IllegalStateException("No collection skins configured")); + } + + var skinIndex = Math.abs(argument.hashCode()) % COLLECTION_SKINS.size(); + + try { + return Result.success(SKIN_CACHE.get(skinIndex)); + } catch (UncheckedExecutionException e) { + return Result.error((Exception) e.getCause()); + } catch (Exception e) { + return Result.error(e); + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MineskinSkinProvider.java b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MineskinSkinProvider.java index 0386927..e254a95 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MineskinSkinProvider.java +++ b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MineskinSkinProvider.java @@ -22,6 +22,8 @@ import org.mineskin.response.QueueResponse; import java.net.InetSocketAddress; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -94,13 +96,18 @@ public final class MineskinSkinProvider implements SkinProvider { } } - private static Optional loadSkin(URI uri, SkinVariant variant) throws Exception { + static Optional loadSkin(URI uri, SkinVariant variant) throws Exception { var mineskinVariant = switch (variant) { case CLASSIC -> Variant.CLASSIC; case SLIM -> Variant.SLIM; }; - var request = GenerateRequest.url(uri) + var request = "file".equals(uri.getScheme()) + ? GenerateRequest.upload(Files.newInputStream(Path.of(uri))) + .variant(mineskinVariant) + .name("skinrestorer-skin") + .visibility(Visibility.UNLISTED) + : GenerateRequest.url(uri) .variant(mineskinVariant) .name("skinrestorer-skin") .visibility(Visibility.UNLISTED); diff --git a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/SkinProvider.java b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/SkinProvider.java index 8b5aad5..c588882 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/SkinProvider.java +++ b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/SkinProvider.java @@ -13,6 +13,7 @@ public interface SkinProvider { MojangSkinProvider MOJANG = new MojangSkinProvider(); ElyBySkinProvider ELY_BY = new ElyBySkinProvider(); MineskinSkinProvider MINESKIN = new MineskinSkinProvider(); + CollectionSkinProvider COLLECTION = new CollectionSkinProvider(); SkinShuffleSkinProvider SKIN_SHUFFLE = new SkinShuffleSkinProvider(); Set BUILTIN_PROVIDER_NAMES = ImmutableSet.of( @@ -20,6 +21,7 @@ public interface SkinProvider { MojangSkinProvider.PROVIDER_NAME, ElyBySkinProvider.PROVIDER_NAME, MineskinSkinProvider.PROVIDER_NAME, + CollectionSkinProvider.PROVIDER_NAME, SkinShuffleSkinProvider.PROVIDER_NAME ); From 1aa606038eeca6f3fefb686114ba734801de444b Mon Sep 17 00:00:00 2001 From: Lionarius Date: Tue, 13 Jan 2026 02:33:31 +0300 Subject: [PATCH 4/8] add `forceFirstJoinSkinFetch` config --- .../main/java/net/lionarius/skinrestorer/config/Config.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/Config.java b/common/src/main/java/net/lionarius/skinrestorer/config/Config.java index d8acb49..4f405a6 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/config/Config.java +++ b/common/src/main/java/net/lionarius/skinrestorer/config/Config.java @@ -22,6 +22,8 @@ public final class Config implements GsonPostProcessable { private boolean fetchSkinOnFirstJoin = true; + private boolean forceFirstJoinSkinFetch = false; + private FirstJoinSkinProvider firstJoinSkinProvider = FirstJoinSkinProvider.MOJANG; private String proxy = ""; @@ -47,6 +49,10 @@ public final class Config implements GsonPostProcessable { return this.fetchSkinOnFirstJoin; } + public boolean forceFirstJoinSkinFetch() { + return this.forceFirstJoinSkinFetch; + } + public FirstJoinSkinProvider firstJoinSkinProvider() { return this.firstJoinSkinProvider; } From 7dcd1d4e81d8815b34be22aa5c81af7d2e649ee6 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Tue, 13 Jan 2026 19:57:33 +0300 Subject: [PATCH 5/8] reorder mixins --- common/src/main/resources/skinrestorer.mixins.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/resources/skinrestorer.mixins.json b/common/src/main/resources/skinrestorer.mixins.json index 741efe1..db8f62e 100644 --- a/common/src/main/resources/skinrestorer.mixins.json +++ b/common/src/main/resources/skinrestorer.mixins.json @@ -7,10 +7,10 @@ "refmap": "${mod_id}.refmap.json", "mixins": [ "ChunkMapAccessor", + "PlayerAccessor", "PlayerListMixin", "ServerLoginPacketListenerImplMixin", - "TrackedEntityAccessorInvoker", - "PlayerAccessor" + "TrackedEntityAccessorInvoker" ], "injectors": { "defaultRequire": 1 From 2e655ce1f3cbd1ff52e571f385418c07a5903422 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Wed, 14 Jan 2026 19:06:02 +0300 Subject: [PATCH 6/8] bump version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8785f81..2b6f914 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ minecraft_version_list=1.21.11 minecraft_version_range=[1.21.11, 1.22) mod_id=skinrestorer mod_name=SkinRestorer -mod_version=2.4.3 +mod_version=2.5.0 mod_author=Lionarius mod_homepage=https://modrinth.com/mod/skinrestorer mod_sources=https://github.com/Suiranoil/SkinRestorer From 301428147db84dcc495b3a2675c489b9c5531fc2 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Wed, 14 Jan 2026 21:53:09 +0300 Subject: [PATCH 7/8] update CHANGELOG --- CHANGELOG.md | 5 +++++ CHANGELOG_LATEST.md | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ff975..03007f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.5.0] - 2026-01-14 +### Added +- Added collection skin provider (allows assigning random skins from a predefined set) +- Added `forceFirstJoinSkinFetch` config option to force skin fetch on first join even if player already has a skin + ## [2.4.3] - 2025-07-25 ### Fixed - Fixed crash on client when loading player head skin (fixes [#63](https://github.com/Suiranoil/SkinRestorer/issues/63) and [#64](https://github.com/Suiranoil/SkinRestorer/issues/64)) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 21da2bd..9f04717 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -1,3 +1,3 @@ -### Fixed -- Fixed crash on client when loading player head skin (fixes [#63](https://github.com/Suiranoil/SkinRestorer/issues/63) and [#64](https://github.com/Suiranoil/SkinRestorer/issues/64)) -- Fixed server freeze when loading player head skin +### Added +- Added collection skin provider (allows assigning random skins from a predefined set) (see [wiki](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration#providerscollection)) +- Added `forceFirstJoinSkinFetch` config option to force skin fetch on first join even if player already has a skin (see [wiki](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration#forcefirstjoinskinfetch)) From 0524b5dab8e3792f39c0cd6c882e76f2f6ea5c57 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Wed, 14 Jan 2026 23:39:53 +0300 Subject: [PATCH 8/8] the worst bug I encountered in gson 2.10.1 --- .../collection/CollectionSkinSourceListDeserializer.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java index 9535c66..7dff675 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/collection/CollectionSkinSourceListDeserializer.java @@ -6,7 +6,12 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -public class CollectionSkinSourceListDeserializer implements JsonDeserializer> { +public class CollectionSkinSourceListDeserializer implements JsonSerializer>, JsonDeserializer> { + @Override + public JsonElement serialize(List src, Type typeOfT, JsonSerializationContext context) { + return context.serialize(src, List.class); + } + @Override public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {