diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a92f3a..5737561 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,5 @@ name: release on: - push: - tags: - - "v**" workflow_dispatch: jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c55ab..0ad418f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ 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.2.0] - 2024-11-28 +### Added +- Added support for [SkinShuffle](https://modrinth.com/mod/skinshuffle) clients (requires FabricAPI on Fabric) + (closes [#34](https://github.com/Suiranoil/SkinRestorer/issues/34)) +- Added `providers.mineskin.apiKey` config option (see [wiki](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration#providersmineskin)) +### Changed +- Migrated to MineSkin's new API V2 +### Fixed +- Fixed `providers` config validation + ## [2.1.0] - 2024-09-26 ### Added - Added `config reload` command for dynamic configuration updates diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 9143cea..ce46783 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -1,9 +1,8 @@ ### Added -- Added `config reload` command for dynamic configuration updates -- Added `refreshSkinOnJoin` config option (see [wiki](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration#refreshskinonjoin)) -- Implemented caching for skin providers -- Added provider configurations (see [wiki](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration#providers)) -- Added username and url validation for requests +- Added support for [SkinShuffle](https://modrinth.com/mod/skinshuffle) clients (requires FabricAPI on Fabric) + (closes [#34](https://github.com/Suiranoil/SkinRestorer/issues/34)) +- Added `providers.mineskin.apiKey` config option (see [wiki](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration##providersmineskin)) +### Changed +- Migrated to MineSkin's new API V2 ### Fixed -- Fixed old skin directory migration not working -- Prevented overwriting existing skin files during migration +- Fixed `providers` config validation diff --git a/README.md b/README.md index e1e902e..50063e2 100644 --- a/README.md +++ b/README.md @@ -9,26 +9,34 @@ A server-side mod for managing and restoring player skins. - **Set skins from Mojang Account**: Fetch and apply skins using a valid Minecraft username. - **Set skins from Ely.by**: Fetch and apply skins using a valid [Ely.by](https://ely.by/) username. -- **Set skins from URL**: Fetch and apply skins from any image URL, supporting both classic (Steve) and slim (Alex) skin models. -- **Automatic skin fetching**: Automatically fetch skin from Mojang/Ely.by when a player joins the server running in offline/insecure mode ([configurable](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration)). +- **Set skins from URL**: Fetch and apply skins from any image URL, supporting both classic (Steve) and slim (Alex) skin + models. +- **Automatic skin fetching**: Automatically fetch skin from Mojang/Ely.by when a player joins the server running in + offline/insecure mode ([configurable](https://github.com/Suiranoil/SkinRestorer/wiki/Configuration)). - **Singleplayer support**: Apply skins individually for each world. - **Permissions API support** +- **SkinShuffle support**: Allow players with [SkinShuffle](https://modrinth.com/mod/skinshuffle) installed to apply skins using GUI. ## 📜 Command Usage Guide -For a detailed list of available commands and their usage, please visit the [commands wiki page](https://github.com/Suiranoil/SkinRestorer/wiki/Commands). +For a detailed list of available commands and their usage, please visit +the [commands wiki page](https://github.com/Suiranoil/SkinRestorer/wiki/Commands). ## ❌ Known Incompatibilities -- **[Arclight](https://github.com/IzzelAliz/Arclight) (<=1.20.1)**: Trials or older versions are not compatible due to mixin conflicts. +- **[Arclight](https://github.com/IzzelAliz/Arclight) (<=1.20.1)**: Trials or older versions are not compatible due to + mixin conflicts. As an alternative, consider using the [SkinsRestorer](https://www.spigotmc.org/resources/skinsrestorer.2124/) plugin. ## 🪙 Donation -If you enjoy using **SkinRestorer** and would like to support its development, you can contribute through cryptocurrency donations. +If you enjoy using **SkinRestorer** and would like to support its development, you can contribute through the following +platforms: -Bitcoin (BTC): `1Ndbwny8pxdnWXFgadp95fp97y5JqMJKTX`\ -USDT (TRC20): `TGXn8wrqku5KLzwPWQAeH7wgnV4UzwHEae`\ +[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/lionarius) + +Bitcoin (BTC): `1Ndbwny8pxdnWXFgadp95fp97y5JqMJKTX` +USDT (TRC20): `TGXn8wrqku5KLzwPWQAeH7wgnV4UzwHEae` USDT (TON): `UQAQF18Xlmx-V1oZ90C2YOju5qI7q1LgrCP5QaIUhqIELmDS` Thank you for your generosity! diff --git a/build.gradle b/build.gradle index e040975..d986858 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,29 @@ plugins { // see https://fabricmc.net/develop/ for new versions - id 'fabric-loom' version '1.7-SNAPSHOT' apply false + id 'fabric-loom' version '1.8-SNAPSHOT' apply false // see https://projects.neoforged.net/neoforged/neogradle for new versions id 'net.neoforged.gradle.userdev' version '7.0.153' apply false // see https://files.minecraftforge.net/net/minecraftforge/gradle/ForgeGradle/ for new versions - id 'net.minecraftforge.gradle' version '6.0.26' apply false + id 'net.minecraftforge.gradle' version '6.0.29' apply false id 'org.parchmentmc.librarian.forgegradle' version '1.+' apply false id 'org.spongepowered.mixin' version '0.7-SNAPSHOT' apply false - id 'me.modmuss50.mod-publish-plugin' version '0.6.3' apply false + id 'me.modmuss50.mod-publish-plugin' version '0.8.1' apply false +} + +allprojects { + repositories { + exclusiveContent { + forRepository { + maven { + name = 'MineSkin' + url = 'https://repo.inventivetalent.org/repository/public' + } + } + filter { + includeGroupAndSubgroups('org.mineskin') + } + } + } } diff --git a/buildSrc/src/main/groovy/multiloader-loader.gradle b/buildSrc/src/main/groovy/multiloader-loader.gradle index ca19b7f..b9dfd60 100644 --- a/buildSrc/src/main/groovy/multiloader-loader.gradle +++ b/buildSrc/src/main/groovy/multiloader-loader.gradle @@ -3,10 +3,10 @@ plugins { } configurations { - commonJava{ + commonJava { canBeResolved = true } - commonResources{ + commonResources { canBeResolved = true } } diff --git a/buildSrc/src/main/groovy/multiloader-publish.gradle b/buildSrc/src/main/groovy/multiloader-publish.gradle index abbf530..f759713 100644 --- a/buildSrc/src/main/groovy/multiloader-publish.gradle +++ b/buildSrc/src/main/groovy/multiloader-publish.gradle @@ -6,7 +6,7 @@ publishMods { if (project.name == 'fabric') file = remapJar.archiveFile else - file = jar.archiveFile + file = tasks.named('jarJar').get().archiveFile modLoaders.add(project.name) type = STABLE @@ -22,6 +22,9 @@ publishMods { minecraftVersions.addAll(minecraft_version_list.split(',')) serverRequired = true + + if (project.name == 'fabric') + optional(fabric_optional_dependencies.split(',')) } modrinth { @@ -31,5 +34,8 @@ publishMods { accessToken = providers.environmentVariable("MODRINTH_TOKEN") minecraftVersions.addAll(minecraft_version_list.split(',')) + + if (project.name == 'fabric') + optional(fabric_optional_dependencies.split(',')) } } diff --git a/common/build.gradle b/common/build.gradle index 63e1da1..c168537 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,6 +1,6 @@ plugins { id 'multiloader-common' - + id 'fabric-loom' } @@ -11,6 +11,8 @@ dependencies { parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") } compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5' + + implementation("org.mineskin:java-client:${mineskin_client_version}") } loom { @@ -18,7 +20,7 @@ loom { if (aw.exists()) { accessWidenerPath.set(aw) } - + mixin { useLegacyMixinAp = false } diff --git a/common/gradle.properties b/common/gradle.properties index 76b0f70..95dca56 100644 --- a/common/gradle.properties +++ b/common/gradle.properties @@ -1 +1 @@ -fabric.loom.dontRemap = true +fabric.loom.dontRemap=true diff --git a/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java b/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java index a324c84..553ee5d 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java +++ b/common/src/main/java/net/lionarius/skinrestorer/SkinRestorer.java @@ -1,8 +1,10 @@ package net.lionarius.skinrestorer; import com.mojang.authlib.GameProfile; -import net.lionarius.skinrestorer.config.BuiltInProviderConfig; +import com.mojang.brigadier.CommandDispatcher; +import net.lionarius.skinrestorer.command.SkinCommand; import net.lionarius.skinrestorer.config.Config; +import net.lionarius.skinrestorer.config.provider.BuiltInProviderConfig; import net.lionarius.skinrestorer.platform.Services; import net.lionarius.skinrestorer.skin.SkinIO; import net.lionarius.skinrestorer.skin.SkinStorage; @@ -13,6 +15,8 @@ import net.lionarius.skinrestorer.util.FileUtils; import net.lionarius.skinrestorer.util.PlayerUtils; import net.lionarius.skinrestorer.util.Result; import net.lionarius.skinrestorer.util.WebUtils; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.storage.LevelResource; @@ -20,7 +24,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.file.Path; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Optional; @@ -57,11 +60,20 @@ public final class SkinRestorer { return Optional.ofNullable(SkinRestorer.providersRegistry.get(name)); } + public static ResourceLocation resourceLocation(String name) { + return new ResourceLocation(SkinRestorer.MOD_ID, name); + } + + public static String assetPath(String name) { + return String.format("/assets/%s/%s", SkinRestorer.MOD_ID, name); + } + public static void onInitialize() { SkinRestorer.configDir = Services.PLATFORM.getConfigDirectory().resolve(SkinRestorer.MOD_ID); SkinRestorer.reloadConfig(); SkinRestorer.providersRegistry.register(EmptySkinProvider.PROVIDER_NAME, SkinProvider.EMPTY, false); + SkinRestorer.providersRegistry.register(SkinShuffleSkinProvider.PROVIDER_NAME, SkinProvider.SKIN_SHUFFLE, false); SkinRestorer.registerDefaultSkinProvider(MojangSkinProvider.PROVIDER_NAME, SkinProvider.MOJANG, SkinRestorer.getConfig().providersConfig().mojang()); SkinRestorer.registerDefaultSkinProvider(ElyBySkinProvider.PROVIDER_NAME, SkinProvider.ELY_BY, SkinRestorer.getConfig().providersConfig().ely_by()); @@ -72,29 +84,18 @@ public final class SkinRestorer { var isDefaultName = config.name().equals(defaultName); SkinRestorer.providersRegistry.register(defaultName, provider, config.enabled() && isDefaultName); - if (!isDefaultName && Arrays.stream(SkinProvider.BUILTIN_PROVIDER_NAMES).noneMatch(name -> name.equals(config.name()))) + if (!isDefaultName && !SkinProvider.BUILTIN_PROVIDER_NAMES.contains(config.name())) SkinRestorer.providersRegistry.register(config.name(), provider, config.enabled()); } - public static void onServerStarted(MinecraftServer server) { - Path worldSkinDirectory = server.getWorldPath(LevelResource.ROOT).resolve(SkinRestorer.MOD_ID); - FileUtils.tryMigrateOldSkinDirectory(SkinRestorer.getConfigDir(), worldSkinDirectory); - - SkinRestorer.skinStorage = new SkinStorage(new SkinIO(worldSkinDirectory)); - } - public static void reloadConfig() { SkinRestorer.config = Config.load(SkinRestorer.getConfigDir()); Translation.reloadTranslations(); WebUtils.recreateHttpClient(); - MojangSkinProvider.createCache(); - ElyBySkinProvider.createCache(); - MineskinSkinProvider.createCache(); - } - - public static String assetPath(String name) { - return String.format("/assets/%s/%s", SkinRestorer.MOD_ID, name); + MojangSkinProvider.reload(); + ElyBySkinProvider.reload(); + MineskinSkinProvider.reload(); } public static Collection applySkin(MinecraftServer server, Iterable targets, SkinValue value, boolean save) { @@ -104,12 +105,12 @@ public final class SkinRestorer { if (!SkinRestorer.getSkinStorage().hasSavedSkin(profile.getId())) value = value.setOriginalValue(PlayerUtils.getPlayerSkin(profile)); - if (save) - SkinRestorer.getSkinStorage().setSkin(profile.getId(), value); - if (PlayerUtils.areSkinPropertiesEquals(value.value(), PlayerUtils.getPlayerSkin(profile))) continue; + if (save) + SkinRestorer.getSkinStorage().setSkin(profile.getId(), value); + PlayerUtils.applyRestoredSkin(profile, value.value()); var player = server.getPlayerList().getPlayer(profile.getId()); @@ -155,4 +156,23 @@ public final class SkinRestorer { return Result.error(e.getMessage()); }); } + + public static class Events { + private Events() {} + + public static void onServerStarted(MinecraftServer server) { + Path worldSkinDirectory = server.getWorldPath(LevelResource.ROOT).resolve(SkinRestorer.MOD_ID); + FileUtils.tryMigrateOldSkinDirectory(SkinRestorer.getConfigDir(), worldSkinDirectory); + + SkinRestorer.skinStorage = new SkinStorage(new SkinIO(worldSkinDirectory)); + } + + public static void onCommandRegister(CommandDispatcher dispatcher) { + SkinCommand.register(dispatcher); + } + + public static void onPlayerDisconnect(ServerPlayer player) { + SkinRestorer.getSkinStorage().removeSkin(player.getUUID()); + } + } } diff --git a/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleCompatibility.java b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleCompatibility.java new file mode 100644 index 0000000..b535f0f --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleCompatibility.java @@ -0,0 +1,55 @@ +package net.lionarius.skinrestorer.compat.skinshuffle; + +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.platform.Services; +import net.lionarius.skinrestorer.skin.SkinValue; +import net.lionarius.skinrestorer.skin.provider.SkinShuffleSkinProvider; +import net.lionarius.skinrestorer.util.PlayerUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Collections; + +public class SkinShuffleCompatibility { + + public static final String MOD_ID = "skinshuffle"; + + private static final boolean SHOULD_APPLY = !Services.PLATFORM.isModLoaded(SkinShuffleCompatibility.MOD_ID); + + private SkinShuffleCompatibility() {} + + public static boolean shouldApply() { + return SkinShuffleCompatibility.SHOULD_APPLY; + } + + public static ResourceLocation resourceLocation(String name) { + return new ResourceLocation(SkinShuffleCompatibility.MOD_ID, name); + } + + public static void onPlayerJoin(ServerPlayer player) { + Services.COMPATIBILITY.skinShuffle_sendHandshake(player); + } + + public static void handleSkinRefresh(MinecraftServer server, ServerPlayer player, SkinShuffleSkinRefreshPayload payload) { + var property = payload.textureProperty(); + + if (!property.name().equals(PlayerUtils.TEXTURES_KEY)) + return; + + if (!property.hasSignature()) + return; + + server.execute(() -> { + SkinRestorer.applySkin( + server, + Collections.singleton(player.getGameProfile()), + new SkinValue(SkinShuffleSkinProvider.PROVIDER_NAME, null, null, property), + !server.usesAuthentication() + ); + + if (server.usesAuthentication() && SkinRestorer.getSkinStorage().hasSavedSkin(player.getUUID())) + SkinRestorer.getSkinStorage().deleteSkin(player.getUUID()); + }); + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleHandshakePayload.java b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleHandshakePayload.java new file mode 100644 index 0000000..798ed92 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleHandshakePayload.java @@ -0,0 +1,19 @@ +package net.lionarius.skinrestorer.compat.skinshuffle; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record SkinShuffleHandshakePayload() implements CustomPacketPayload { + + public static final SkinShuffleHandshakePayload INSTANCE = new SkinShuffleHandshakePayload(); + + public static final CustomPacketPayload.Type PACKET_ID = new CustomPacketPayload.Type<>(SkinShuffleCompatibility.resourceLocation("handshake")); + public static final StreamCodec PACKET_CODEC = StreamCodec.unit(INSTANCE); + + @Override + public CustomPacketPayload.@NotNull Type type() { + return PACKET_ID; + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshPayload.java b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshPayload.java new file mode 100644 index 0000000..f8c6030 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshPayload.java @@ -0,0 +1,7 @@ +package net.lionarius.skinrestorer.compat.skinshuffle; + +import com.mojang.authlib.properties.Property; + +public interface SkinShuffleSkinRefreshPayload { + Property textureProperty(); +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshV1Payload.java b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshV1Payload.java new file mode 100644 index 0000000..8892357 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshV1Payload.java @@ -0,0 +1,34 @@ +package net.lionarius.skinrestorer.compat.skinshuffle; + +import com.mojang.authlib.properties.Property; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record SkinShuffleSkinRefreshV1Payload( + Property textureProperty) implements CustomPacketPayload, SkinShuffleSkinRefreshPayload { + + public static final CustomPacketPayload.Type PACKET_ID = new CustomPacketPayload.Type<>(SkinShuffleCompatibility.resourceLocation("refresh")); + public static final StreamCodec PACKET_CODEC = StreamCodec.of( + SkinShuffleSkinRefreshV1Payload::encode, + SkinShuffleSkinRefreshV1Payload::decode + ); + + public static void encode(FriendlyByteBuf buf, SkinShuffleSkinRefreshV1Payload value) { + var textureProperty = value.textureProperty(); + + buf.writeUtf(textureProperty.name()); + buf.writeUtf(textureProperty.value()); + buf.writeNullable(textureProperty.signature(), FriendlyByteBuf::writeUtf); + } + + public static SkinShuffleSkinRefreshV1Payload decode(FriendlyByteBuf buf) { + return new SkinShuffleSkinRefreshV1Payload(new Property(buf.readUtf(), buf.readUtf(), buf.readNullable(FriendlyByteBuf::readUtf))); + } + + @Override + public @NotNull CustomPacketPayload.Type type() { + return PACKET_ID; + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshV2Payload.java b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshV2Payload.java new file mode 100644 index 0000000..177fef9 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/compat/skinshuffle/SkinShuffleSkinRefreshV2Payload.java @@ -0,0 +1,42 @@ +package net.lionarius.skinrestorer.compat.skinshuffle; + +import com.mojang.authlib.properties.Property; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.jetbrains.annotations.NotNull; + +public record SkinShuffleSkinRefreshV2Payload( + Property textureProperty) implements CustomPacketPayload, SkinShuffleSkinRefreshPayload { + + public static final CustomPacketPayload.Type PACKET_ID = new CustomPacketPayload.Type<>(SkinShuffleCompatibility.resourceLocation("skin_refresh")); + public static final StreamCodec PACKET_CODEC = StreamCodec.of( + SkinShuffleSkinRefreshV2Payload::encode, + SkinShuffleSkinRefreshV2Payload::decode + ); + + public static void encode(FriendlyByteBuf buf, SkinShuffleSkinRefreshV2Payload value) { + var textureProperty = value.textureProperty(); + + buf.writeBoolean(textureProperty.hasSignature()); + buf.writeUtf(textureProperty.name()); + buf.writeUtf(textureProperty.value()); + if (textureProperty.hasSignature()) { + assert textureProperty.signature() != null; + + buf.writeUtf(textureProperty.signature()); + } + } + + public static SkinShuffleSkinRefreshV2Payload decode(FriendlyByteBuf buf) { + if (buf.readBoolean()) { + return new SkinShuffleSkinRefreshV2Payload(new Property(buf.readUtf(), buf.readUtf(), buf.readUtf())); + } + return new SkinShuffleSkinRefreshV2Payload(new Property(buf.readUtf(), buf.readUtf(), null)); + } + + @Override + public @NotNull CustomPacketPayload.Type type() { + return PACKET_ID; + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/BuiltInProviderConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/BuiltInProviderConfig.java deleted file mode 100644 index a4d197e..0000000 --- a/common/src/main/java/net/lionarius/skinrestorer/config/BuiltInProviderConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.lionarius.skinrestorer.config; - -public record BuiltInProviderConfig(boolean enabled, String name, CacheConfig cache) { - public boolean isValid() { - return this.name != null && this.cache != null && !this.name.isEmpty() && this.cache.isValid(); - } -} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/CacheConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/CacheConfig.java deleted file mode 100644 index 90c0089..0000000 --- a/common/src/main/java/net/lionarius/skinrestorer/config/CacheConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.lionarius.skinrestorer.config; - -public record CacheConfig(boolean enabled, long duration) { - public boolean isValid() { - return this.duration > 0; - } -} 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 59e1017..c22f6bc 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/config/Config.java +++ b/common/src/main/java/net/lionarius/skinrestorer/config/Config.java @@ -1,13 +1,15 @@ package net.lionarius.skinrestorer.config; import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.config.provider.ProvidersConfig; import net.lionarius.skinrestorer.util.FileUtils; import net.lionarius.skinrestorer.util.JsonUtils; +import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; import java.nio.file.Path; import java.util.Optional; -public final class Config { +public final class Config implements GsonPostProcessable { public static final String CONFIG_FILENAME = "config.json"; @@ -68,14 +70,13 @@ public final class Config { if (config == null) config = new Config(); - config.verifyAndFix(); - FileUtils.writeFile(path.resolve(Config.CONFIG_FILENAME), JsonUtils.toJson(config)); return config; } - private void verifyAndFix() { + @Override + public void gsonPostProcess() { if (this.language == null || this.language.isEmpty()) { SkinRestorer.LOGGER.warn("Language config is null or empty, defaulting to 'en_us'"); this.language = "en_us"; @@ -95,7 +96,7 @@ public final class Config { try { this.parsedProxy = Proxy.parse(this.proxy); } catch (Exception e) { - SkinRestorer.LOGGER.warn("Could not parse proxy config", e); + SkinRestorer.LOGGER.warn("Could not parse proxy config: {}", e.getMessage()); this.parsedProxy = null; } } @@ -109,8 +110,5 @@ public final class Config { SkinRestorer.LOGGER.warn("Providers config is null, using default"); this.providers = ProvidersConfig.DEFAULT; } - - if (!this.providers.isValid()) - this.providers.fix(); } } diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/ProvidersConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/ProvidersConfig.java deleted file mode 100644 index da22b99..0000000 --- a/common/src/main/java/net/lionarius/skinrestorer/config/ProvidersConfig.java +++ /dev/null @@ -1,65 +0,0 @@ -package net.lionarius.skinrestorer.config; - -import net.lionarius.skinrestorer.SkinRestorer; -import net.lionarius.skinrestorer.skin.provider.ElyBySkinProvider; -import net.lionarius.skinrestorer.skin.provider.MineskinSkinProvider; -import net.lionarius.skinrestorer.skin.provider.MojangSkinProvider; - -public final class ProvidersConfig { - public static final ProvidersConfig DEFAULT = new ProvidersConfig( - new BuiltInProviderConfig(true, MojangSkinProvider.PROVIDER_NAME, new CacheConfig(true, 60)), - new BuiltInProviderConfig(true, ElyBySkinProvider.PROVIDER_NAME, new CacheConfig(true, 60)), - new BuiltInProviderConfig(true, MineskinSkinProvider.PROVIDER_NAME, new CacheConfig(true, 300)) - ); - - private BuiltInProviderConfig mojang; - private BuiltInProviderConfig ely_by; - private BuiltInProviderConfig mineskin; - - public ProvidersConfig(BuiltInProviderConfig mojang, BuiltInProviderConfig ely_by, BuiltInProviderConfig mineskin) { - this.mojang = mojang; - this.ely_by = ely_by; - this.mineskin = mineskin; - } - - public boolean isValid() { - if (this == ProvidersConfig.DEFAULT) - return true; - - return this.mojang == null || !this.mojang.isValid() - || this.ely_by == null || !this.ely_by.isValid() - || this.mineskin == null || !this.mineskin.isValid(); - } - - public void fix() { - if (this == ProvidersConfig.DEFAULT) - return; - - if (this.mojang == null || !this.mojang.isValid()) { - SkinRestorer.LOGGER.warn("Mojang provider config is invalid, using default"); - this.mojang = ProvidersConfig.DEFAULT.mojang(); - } - - if (this.ely_by == null || !this.ely_by.isValid()) { - SkinRestorer.LOGGER.warn("Ely.By provider config is invalid, using default"); - this.ely_by = ProvidersConfig.DEFAULT.ely_by(); - } - - if (this.mineskin == null || !this.mineskin.isValid()) { - SkinRestorer.LOGGER.warn("Mineskin provider config is invalid, using default"); - this.mineskin = ProvidersConfig.DEFAULT.mineskin(); - } - } - - public BuiltInProviderConfig mojang() { - return this.mojang; - } - - public BuiltInProviderConfig ely_by() { - return this.ely_by; - } - - public BuiltInProviderConfig mineskin() { - return this.mineskin; - } -} 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 new file mode 100644 index 0000000..198e9f1 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/BuiltInProviderConfig.java @@ -0,0 +1,10 @@ +package net.lionarius.skinrestorer.config.provider; + +public interface BuiltInProviderConfig { + + boolean enabled(); + + String name(); + + CacheConfig cache(); +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/CacheConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/CacheConfig.java new file mode 100644 index 0000000..6daf190 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/CacheConfig.java @@ -0,0 +1,28 @@ +package net.lionarius.skinrestorer.config.provider; + +import net.lionarius.skinrestorer.SkinRestorer; + +public final class CacheConfig { + private boolean enabled; + private long duration; + + public CacheConfig(boolean enabled, long duration) { + this.enabled = enabled; + this.duration = duration; + } + + public boolean enabled() { + return enabled; + } + + public long duration() { + return duration; + } + + void validate(CacheConfig defaultValue) { + if (this.duration <= 0) { + SkinRestorer.LOGGER.warn("Cache duration is less than or equal to zero, defaulting to {}", defaultValue.duration()); + this.duration = defaultValue.duration(); + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/ElyByProviderConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/ElyByProviderConfig.java new file mode 100644 index 0000000..c36caf8 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/ElyByProviderConfig.java @@ -0,0 +1,46 @@ +package net.lionarius.skinrestorer.config.provider; + +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.skin.provider.ElyBySkinProvider; +import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; + +public class ElyByProviderConfig implements BuiltInProviderConfig, GsonPostProcessable { + private static final CacheConfig DEFAULT_CACHE_VALUE = new CacheConfig(true, 60); + + private boolean enabled; + private String name; + private CacheConfig cache; + + public ElyByProviderConfig() { + this.enabled = true; + this.name = ElyBySkinProvider.PROVIDER_NAME; + this.cache = DEFAULT_CACHE_VALUE; + } + + public boolean enabled() { + return enabled; + } + + public String name() { + return name; + } + + public CacheConfig cache() { + return cache; + } + + @Override + public void gsonPostProcess() { + if (this.name == null || this.name.isEmpty()) { + SkinRestorer.LOGGER.warn("Ely.By provider name is null or empty, defaulting to '{}'", ElyBySkinProvider.PROVIDER_NAME); + this.name = ElyBySkinProvider.PROVIDER_NAME; + } + + if (this.cache == null) { + SkinRestorer.LOGGER.warn("Ely.By provider cache is null, using default"); + this.cache = DEFAULT_CACHE_VALUE; + } else { + this.cache.validate(DEFAULT_CACHE_VALUE); + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/MineskinProviderConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/MineskinProviderConfig.java new file mode 100644 index 0000000..3dff272 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/MineskinProviderConfig.java @@ -0,0 +1,56 @@ +package net.lionarius.skinrestorer.config.provider; + +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.skin.provider.MineskinSkinProvider; +import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; + +public class MineskinProviderConfig implements BuiltInProviderConfig, GsonPostProcessable { + private static final CacheConfig DEFAULT_CACHE_VALUE = new CacheConfig(true, 300); + + private boolean enabled; + private String name; + private CacheConfig cache; + private String apiKey; + + public MineskinProviderConfig() { + this.enabled = true; + this.name = MineskinSkinProvider.PROVIDER_NAME; + this.cache = DEFAULT_CACHE_VALUE; + } + + public boolean enabled() { + return enabled; + } + + public String name() { + return name; + } + + public CacheConfig cache() { + return cache; + } + + public String apiKey() { + return apiKey; + } + + @Override + public void gsonPostProcess() { + if (this.name == null || this.name.isEmpty()) { + SkinRestorer.LOGGER.warn("Mineskin provider name is null or empty, defaulting to '{}'", MineskinSkinProvider.PROVIDER_NAME); + this.name = MineskinSkinProvider.PROVIDER_NAME; + } + + if (this.cache == null) { + SkinRestorer.LOGGER.warn("Mineskin cache is null, using default"); + this.cache = DEFAULT_CACHE_VALUE; + } else { + this.cache.validate(DEFAULT_CACHE_VALUE); + } + + if (this.apiKey == null) { + SkinRestorer.LOGGER.warn("Mineskin API key is null, defaulting to an empty string"); + this.apiKey = ""; + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/config/provider/MojangProviderConfig.java b/common/src/main/java/net/lionarius/skinrestorer/config/provider/MojangProviderConfig.java new file mode 100644 index 0000000..625a55e --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/MojangProviderConfig.java @@ -0,0 +1,46 @@ +package net.lionarius.skinrestorer.config.provider; + +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.skin.provider.MojangSkinProvider; +import net.lionarius.skinrestorer.util.gson.GsonPostProcessable; + +public class MojangProviderConfig implements BuiltInProviderConfig, GsonPostProcessable { + private static final CacheConfig DEFAULT_CACHE_VALUE = new CacheConfig(true, 60); + + private boolean enabled; + private String name; + private CacheConfig cache; + + public MojangProviderConfig() { + this.enabled = true; + this.name = MojangSkinProvider.PROVIDER_NAME; + this.cache = DEFAULT_CACHE_VALUE; + } + + public boolean enabled() { + return enabled; + } + + public String name() { + return name; + } + + public CacheConfig cache() { + return cache; + } + + @Override + public void gsonPostProcess() { + if (this.name == null || this.name.isEmpty()) { + SkinRestorer.LOGGER.warn("Mojang provider name is null or empty, defaulting to '{}'", MojangSkinProvider.PROVIDER_NAME); + this.name = MojangSkinProvider.PROVIDER_NAME; + } + + if (this.cache == null) { + SkinRestorer.LOGGER.warn("Mojang provider cache is null, using default"); + this.cache = DEFAULT_CACHE_VALUE; + } else { + this.cache.validate(DEFAULT_CACHE_VALUE); + } + } +} 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 new file mode 100644 index 0000000..c8a793e --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/config/provider/ProvidersConfig.java @@ -0,0 +1,52 @@ +package net.lionarius.skinrestorer.config.provider; + +import net.lionarius.skinrestorer.SkinRestorer; +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() + ); + + private MojangProviderConfig mojang; + private ElyByProviderConfig ely_by; + private MineskinProviderConfig mineskin; + + public ProvidersConfig(MojangProviderConfig mojang, ElyByProviderConfig ely_by, MineskinProviderConfig mineskin) { + this.mojang = mojang; + this.ely_by = ely_by; + this.mineskin = mineskin; + } + + public MojangProviderConfig mojang() { + return this.mojang; + } + + public ElyByProviderConfig ely_by() { + return this.ely_by; + } + + public MineskinProviderConfig mineskin() { + return this.mineskin; + } + + @Override + public void gsonPostProcess() { + if (this.mojang == null) { + SkinRestorer.LOGGER.warn("Mojang provider config is null, using default"); + this.mojang = ProvidersConfig.DEFAULT.mojang(); + } + + if (this.ely_by == null) { + SkinRestorer.LOGGER.warn("Ely.By provider config is null, using default"); + this.ely_by = ProvidersConfig.DEFAULT.ely_by(); + } + + if (this.mineskin == null) { + SkinRestorer.LOGGER.warn("Mineskin provider config is null, using default"); + this.mineskin = ProvidersConfig.DEFAULT.mineskin(); + } + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/mineskin/Java11RequestHandler.java b/common/src/main/java/net/lionarius/skinrestorer/mineskin/Java11RequestHandler.java new file mode 100644 index 0000000..b69832a --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/mineskin/Java11RequestHandler.java @@ -0,0 +1,187 @@ +package net.lionarius.skinrestorer.mineskin; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import org.mineskin.MineSkinClientImpl; +import org.mineskin.data.CodeAndMessage; +import org.mineskin.exception.MineSkinRequestException; +import org.mineskin.exception.MineskinException; +import org.mineskin.request.RequestHandler; +import org.mineskin.response.MineSkinResponse; +import org.mineskin.response.ResponseConstructor; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Map; +import java.util.logging.Level; +import java.util.stream.Collectors; + +// copy-pasted from https://github.com/InventivetalentDev/MineskinClient/blob/master/java11/src/main/java/org/mineskin/Java11RequestHandler.java +// with some modifications to support proxy +public class Java11RequestHandler extends RequestHandler { + + private final Gson gson; + private final HttpClient httpClient; + + public Java11RequestHandler(String userAgent, String apiKey, int timeout, Gson gson, InetSocketAddress proxy) { + super(userAgent, apiKey, timeout, gson); + this.gson = gson; + + HttpClient.Builder clientBuilder = HttpClient.newBuilder() + .connectTimeout(java.time.Duration.ofMillis(timeout)); + + if (userAgent != null) { + clientBuilder.followRedirects(HttpClient.Redirect.NORMAL); + } + + if (proxy != null) { + clientBuilder.proxy(ProxySelector.of(proxy)); + } + + this.httpClient = clientBuilder.build(); + } + + private > R wrapResponse(HttpResponse response, Class clazz, ResponseConstructor constructor) + throws IOException { + String rawBody = response.body(); + try { + JsonObject jsonBody = gson.fromJson(rawBody, JsonObject.class); + R wrapped = constructor.construct( + response.statusCode(), + lowercaseHeaders(response.headers().map()), + jsonBody, + gson, clazz + ); + if (!wrapped.isSuccess()) { + throw new MineSkinRequestException( + wrapped.getFirstError().map(CodeAndMessage::code).orElse("request_failed"), + wrapped.getFirstError().map(CodeAndMessage::message).orElse("Request Failed"), + wrapped + ); + } + return wrapped; + } catch (JsonParseException e) { + MineSkinClientImpl.LOGGER.log(Level.WARNING, "Failed to parse response body: " + rawBody, e); + throw new MineskinException("Failed to parse response", e); + } + } + + private Map lowercaseHeaders(Map> headers) { + return headers.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().toLowerCase(), + entry -> String.join(", ", entry.getValue()) + )); + } + + public > R getJson(String url, Class clazz, ResponseConstructor constructor) + throws IOException { + MineSkinClientImpl.LOGGER.fine("GET " + url); + + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .header("User-Agent", this.userAgent); + HttpRequest request; + if (apiKey != null) { + request = requestBuilder + .header("Authorization", "Bearer " + apiKey) + .header("Accept", "application/json").build(); + } else { + request = requestBuilder.build(); + } + HttpResponse response; + try { + response = this.httpClient.send(request, BodyHandlers.ofString()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return wrapResponse(response, clazz, constructor); + } + + public > R postJson(String url, JsonObject data, Class clazz, ResponseConstructor constructor) + throws IOException { + MineSkinClientImpl.LOGGER.fine("POST " + url); + + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .POST(BodyPublishers.ofString(gson.toJson(data))) + .header("Content-Type", "application/json") + .header("User-Agent", this.userAgent); + HttpRequest request; + if (apiKey != null) { + request = requestBuilder + .header("Authorization", "Bearer " + apiKey) + .header("Accept", "application/json").build(); + } else { + request = requestBuilder.build(); + } + + HttpResponse response; + try { + response = this.httpClient.send(request, BodyHandlers.ofString()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return wrapResponse(response, clazz, constructor); + } + + public > R postFormDataFile(String url, String key, String filename, InputStream in, Map data, Class clazz, ResponseConstructor constructor) + throws IOException { + MineSkinClientImpl.LOGGER.fine("POST " + url); + + String boundary = "mineskin-" + System.currentTimeMillis(); + StringBuilder bodyBuilder = new StringBuilder(); + + // add form fields + for (Map.Entry entry : data.entrySet()) { + bodyBuilder.append("--").append(boundary).append("\r\n") + .append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"\r\n\r\n") + .append(entry.getValue()).append("\r\n"); + } + + // add file + byte[] fileContent = in.readAllBytes(); + bodyBuilder.append("--").append(boundary).append("\r\n") + .append("Content-Disposition: form-data; name=\"").append(key) + .append("\"; filename=\"").append(filename).append("\"\r\n") + .append("Content-Type: image/png\r\n\r\n"); + byte[] bodyStart = bodyBuilder.toString().getBytes(); + byte[] boundaryEnd = ("\r\n--" + boundary + "--\r\n").getBytes(); + byte[] bodyString = new byte[bodyStart.length + fileContent.length + boundaryEnd.length]; + System.arraycopy(bodyStart, 0, bodyString, 0, bodyStart.length); + System.arraycopy(fileContent, 0, bodyString, bodyStart.length, fileContent.length); + System.arraycopy(boundaryEnd, 0, bodyString, bodyStart.length + fileContent.length, boundaryEnd.length); + + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .POST(HttpRequest.BodyPublishers.ofByteArray(bodyString)) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .header("User-Agent", this.userAgent); + HttpRequest request; + if (apiKey != null) { + request = requestBuilder + .header("Authorization", "Bearer " + apiKey) + .header("Accept", "application/json").build(); + } else { + request = requestBuilder.build(); + } + + HttpResponse response; + try { + response = this.httpClient.send(request, BodyHandlers.ofString()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return wrapResponse(response, clazz, constructor); + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/mixin/PlayerListMixin.java b/common/src/main/java/net/lionarius/skinrestorer/mixin/PlayerListMixin.java index b73691c..ec1f02e 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/mixin/PlayerListMixin.java +++ b/common/src/main/java/net/lionarius/skinrestorer/mixin/PlayerListMixin.java @@ -27,13 +27,13 @@ public abstract class PlayerListMixin { @Inject(method = "remove", at = @At("TAIL")) private void remove(ServerPlayer player, CallbackInfo ci) { - SkinRestorer.getSkinStorage().removeSkin(player.getUUID()); + SkinRestorer.Events.onPlayerDisconnect(player); } @Inject(method = "removeAll", at = @At("HEAD")) private void removeAll(CallbackInfo ci) { - for (ServerPlayer player : getPlayers()) { - SkinRestorer.getSkinStorage().removeSkin(player.getUUID()); + for (var player : getPlayers()) { + SkinRestorer.Events.onPlayerDisconnect(player); } } 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 90d2210..28e588e 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/mixin/ServerLoginPacketListenerImplMixin.java +++ b/common/src/main/java/net/lionarius/skinrestorer/mixin/ServerLoginPacketListenerImplMixin.java @@ -72,17 +72,17 @@ public abstract class ServerLoginPacketListenerImplMixin { @Unique private static void skinrestorer$fetchSkin(GameProfile profile, SkinProviderContext context) { - SkinRestorer.LOGGER.debug("fetching {}'s skin", profile.getName()); + SkinRestorer.LOGGER.debug("Fetching {}'s skin", profile.getName()); var result = SkinRestorer.getProvider(context.name()).map( provider -> provider.fetchSkin(context.argument(), context.variant()) - ).orElseGet(() -> Result.error(new IllegalArgumentException("skin provider is not registered: " + context.name()))); + ).orElseGet(() -> Result.error(new IllegalArgumentException("Skin provider is not registered: " + context.name()))); if (!result.isError()) { var value = SkinValue.fromProviderContextWithValue(context, result.getSuccessValue().orElse(null)); SkinRestorer.getSkinStorage().setSkin(profile.getId(), value); } else { - SkinRestorer.LOGGER.warn("failed to fetch skin", result.getErrorValue()); + SkinRestorer.LOGGER.warn("Failed to fetch skin: {}", result.getErrorValue().getMessage()); } } } diff --git a/common/src/main/java/net/lionarius/skinrestorer/platform/Services.java b/common/src/main/java/net/lionarius/skinrestorer/platform/Services.java index e160259..7b3c00f 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/platform/Services.java +++ b/common/src/main/java/net/lionarius/skinrestorer/platform/Services.java @@ -1,6 +1,7 @@ package net.lionarius.skinrestorer.platform; import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.platform.services.CompatibilityHelper; import net.lionarius.skinrestorer.platform.services.PlatformHelper; import java.util.ServiceLoader; @@ -8,8 +9,9 @@ import java.util.ServiceLoader; public final class Services { private Services() {} - + public final static PlatformHelper PLATFORM = load(PlatformHelper.class); + public final static CompatibilityHelper COMPATIBILITY = load(CompatibilityHelper.class); private static T load(Class clazz) { final T loadedService = ServiceLoader.load(clazz) diff --git a/common/src/main/java/net/lionarius/skinrestorer/platform/services/CompatibilityHelper.java b/common/src/main/java/net/lionarius/skinrestorer/platform/services/CompatibilityHelper.java new file mode 100644 index 0000000..a372f49 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/platform/services/CompatibilityHelper.java @@ -0,0 +1,8 @@ +package net.lionarius.skinrestorer.platform.services; + +import net.minecraft.server.level.ServerPlayer; + +public interface CompatibilityHelper { + + void skinShuffle_sendHandshake(ServerPlayer player); +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/ElyBySkinProvider.java b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/ElyBySkinProvider.java index ecdfac9..dcba9e1 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/ElyBySkinProvider.java +++ b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/ElyBySkinProvider.java @@ -36,7 +36,11 @@ public final class ElyBySkinProvider implements SkinProvider { } } - public static void createCache() { + public static void reload() { + createCache(); + } + + private static void createCache() { var config = SkinRestorer.getConfig().providersConfig().ely_by(); var time = config.cache().enabled() ? config.cache().duration() : 0; 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 6fd0e91..5e79fc5 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 @@ -4,21 +4,26 @@ 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.google.gson.JsonObject; import com.mojang.authlib.properties.Property; import it.unimi.dsi.fastutil.Pair; import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.mineskin.Java11RequestHandler; import net.lionarius.skinrestorer.skin.SkinVariant; import net.lionarius.skinrestorer.util.JsonUtils; import net.lionarius.skinrestorer.util.PlayerUtils; import net.lionarius.skinrestorer.util.Result; import net.lionarius.skinrestorer.util.WebUtils; import org.jetbrains.annotations.NotNull; +import org.mineskin.MineSkinClient; +import org.mineskin.data.Variant; +import org.mineskin.data.Visibility; +import org.mineskin.request.GenerateRequest; +import org.mineskin.response.QueueResponse; -import java.io.IOException; +import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; -import java.net.http.HttpRequest; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -27,6 +32,7 @@ public final class MineskinSkinProvider implements SkinProvider { public static final String PROVIDER_NAME = "web"; private static final URI API_URI; + private static MineSkinClient MINESKIN_CLIENT; private static LoadingCache, Optional> SKIN_CACHE; @@ -38,7 +44,29 @@ public final class MineskinSkinProvider implements SkinProvider { } } - public static void createCache() { + public static void reload() { + var config = SkinRestorer.getConfig(); + var configApiKey = config.providersConfig().mineskin().apiKey(); + + MINESKIN_CLIENT = MineSkinClient + .builder() + .userAgent(WebUtils.USER_AGENT) + .gson(JsonUtils.GSON) + .timeout((int) Duration.ofSeconds(config.requestTimeout()).toMillis()) + .requestHandler((userAgent, apiKey, timeout, gson) -> new Java11RequestHandler( + userAgent, + apiKey, + timeout, + gson, + SkinRestorer.getConfig().proxy().map(proxy -> new InetSocketAddress(proxy.host(), proxy.port())).orElse(null) + )) + .apiKey(configApiKey.isEmpty() ? null : configApiKey) + .build(); + + createCache(); + } + + private static void createCache() { var config = SkinRestorer.getConfig().providersConfig().mineskin(); var time = config.cache().enabled() ? config.cache().duration() : 0; @@ -76,29 +104,26 @@ public final class MineskinSkinProvider implements SkinProvider { } private static Optional loadSkin(URI uri, SkinVariant variant) throws Exception { - var result = MineskinSkinProvider.uploadToMineskin(uri, variant); - var texture = result.getAsJsonObject("data").getAsJsonObject("texture"); - var textures = new Property(PlayerUtils.TEXTURES_KEY, texture.get("value").getAsString(), texture.get("signature").getAsString()); + var mineskinVariant = switch (variant) { + case CLASSIC -> Variant.CLASSIC; + case SLIM -> Variant.SLIM; + }; - return Optional.of(textures); - } - - private static JsonObject uploadToMineskin(URI url, SkinVariant variant) throws IOException { - var body = ("{\"variant\":\"%s\",\"name\":\"%s\",\"visibility\":%d,\"url\":\"%s\"}") - .formatted(variant.toString(), "none", 0, url); + var request = GenerateRequest.url(uri) + .variant(mineskinVariant) + .name("skinrestorer-skin") + .visibility(Visibility.UNLISTED); - var request = HttpRequest.newBuilder() - .uri(MineskinSkinProvider.API_URI.resolve("/generate/url")) - .POST(HttpRequest.BodyPublishers.ofString(body)) - .header("Content-Type", "application/json") - .build(); + var skin = MINESKIN_CLIENT.queue().submit(request) + .thenApply(QueueResponse::getJob) + .thenCompose(jobInfo -> jobInfo.waitForCompletion(MINESKIN_CLIENT)) + .thenCompose(jobReference -> jobReference.getOrLoadSkin(MINESKIN_CLIENT)) + .join(); - var response = WebUtils.executeRequest(request); - WebUtils.throwOnClientErrors(response); - - if (response.statusCode() != 200) - throw new IllegalArgumentException("could not get mineskin skin"); - - return JsonUtils.parseJson(response.body()); + return Optional.of(new Property( + PlayerUtils.TEXTURES_KEY, + skin.texture().data().value(), + skin.texture().data().signature() + )); } } diff --git a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MojangSkinProvider.java b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MojangSkinProvider.java index 1523f86..6f68982 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MojangSkinProvider.java +++ b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/MojangSkinProvider.java @@ -52,11 +52,13 @@ public final class MojangSkinProvider implements SkinProvider { } } }, SkinRestorer.getConfigDir().resolve(PROFILE_CACHE_FILENAME).toFile()); - - } - public static void createCache() { + public static void reload() { + createCache(); + } + + private static void createCache() { var config = SkinRestorer.getConfig().providersConfig().mojang(); var time = config.cache().enabled() ? config.cache().duration() : 0; 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 86a5815..8b5aad5 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 @@ -1,18 +1,27 @@ package net.lionarius.skinrestorer.skin.provider; +import com.google.common.collect.ImmutableSet; import com.mojang.authlib.properties.Property; import net.lionarius.skinrestorer.skin.SkinVariant; import net.lionarius.skinrestorer.util.Result; import java.util.Optional; +import java.util.Set; public interface SkinProvider { EmptySkinProvider EMPTY = new EmptySkinProvider(); MojangSkinProvider MOJANG = new MojangSkinProvider(); ElyBySkinProvider ELY_BY = new ElyBySkinProvider(); MineskinSkinProvider MINESKIN = new MineskinSkinProvider(); + SkinShuffleSkinProvider SKIN_SHUFFLE = new SkinShuffleSkinProvider(); - String[] BUILTIN_PROVIDER_NAMES = new String[]{EmptySkinProvider.PROVIDER_NAME, MojangSkinProvider.PROVIDER_NAME, ElyBySkinProvider.PROVIDER_NAME, MineskinSkinProvider.PROVIDER_NAME}; + Set BUILTIN_PROVIDER_NAMES = ImmutableSet.of( + EmptySkinProvider.PROVIDER_NAME, + MojangSkinProvider.PROVIDER_NAME, + ElyBySkinProvider.PROVIDER_NAME, + MineskinSkinProvider.PROVIDER_NAME, + SkinShuffleSkinProvider.PROVIDER_NAME + ); String getArgumentName(); diff --git a/common/src/main/java/net/lionarius/skinrestorer/skin/provider/SkinShuffleSkinProvider.java b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/SkinShuffleSkinProvider.java new file mode 100644 index 0000000..e8642ce --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/skin/provider/SkinShuffleSkinProvider.java @@ -0,0 +1,27 @@ +package net.lionarius.skinrestorer.skin.provider; + +import com.mojang.authlib.properties.Property; +import net.lionarius.skinrestorer.skin.SkinVariant; +import net.lionarius.skinrestorer.util.Result; + +import java.util.Optional; + +public final class SkinShuffleSkinProvider implements SkinProvider { + + public static final String PROVIDER_NAME = "skinshuffle"; + + @Override + public String getArgumentName() { + return "unsupported"; + } + + @Override + public boolean hasVariantSupport() { + return false; + } + + @Override + public Result, Exception> fetchSkin(String argument, SkinVariant variant) { + return Result.error(new UnsupportedOperationException("SkinShuffle Provider does not support fetching skins")); + } +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/util/FileUtils.java b/common/src/main/java/net/lionarius/skinrestorer/util/FileUtils.java index d0aeb0b..f1b7de9 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/util/FileUtils.java +++ b/common/src/main/java/net/lionarius/skinrestorer/util/FileUtils.java @@ -42,7 +42,7 @@ public final class FileUtils { } } } catch (Exception e) { - SkinRestorer.LOGGER.error("could not migrate skin directory", e); + SkinRestorer.LOGGER.error("Could not migrate skin directory", e); } } @@ -55,7 +55,7 @@ public final class FileUtils { return StringUtils.readString(reader); } } catch (IOException e) { - SkinRestorer.LOGGER.error("failed to read resource", e); + SkinRestorer.LOGGER.error("Failed to read resource", e); return null; } } @@ -67,7 +67,7 @@ public final class FileUtils { return Files.readString(file); } catch (Exception e) { - SkinRestorer.LOGGER.error("failed to read file", e); + SkinRestorer.LOGGER.error("Failed to read file", e); return null; } } @@ -83,7 +83,7 @@ public final class FileUtils { Files.writeString(file, content); } catch (IOException e) { - SkinRestorer.LOGGER.error("failed to write file", e); + SkinRestorer.LOGGER.error("Failed to write file", e); } } @@ -92,7 +92,7 @@ public final class FileUtils { if (Files.exists(file)) Files.delete(file); } catch (IOException e) { - SkinRestorer.LOGGER.error("failed to delete file", e); + SkinRestorer.LOGGER.error("Failed to delete file", e); } } } diff --git a/common/src/main/java/net/lionarius/skinrestorer/util/JsonUtils.java b/common/src/main/java/net/lionarius/skinrestorer/util/JsonUtils.java index 82e47b1..7eef4e1 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/util/JsonUtils.java +++ b/common/src/main/java/net/lionarius/skinrestorer/util/JsonUtils.java @@ -9,6 +9,7 @@ import com.mojang.authlib.properties.Property; import com.mojang.authlib.properties.PropertyMap; import com.mojang.util.UUIDTypeAdapter; import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.util.gson.PostProcessingEnabler; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; @@ -17,7 +18,8 @@ import java.util.UUID; public final class JsonUtils { - private static final Gson GSON = new GsonBuilder() + public static final Gson GSON = new GsonBuilder() + .registerTypeAdapterFactory(new PostProcessingEnabler()) .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) .registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()) .registerTypeAdapter(GameProfile.class, new GameProfile.Serializer()) @@ -54,7 +56,7 @@ public final class JsonUtils { return json; } catch (Exception e) { - SkinRestorer.LOGGER.error(e.toString()); + SkinRestorer.LOGGER.error("Could not parse skin property", e); return null; } } diff --git a/common/src/main/java/net/lionarius/skinrestorer/util/WebUtils.java b/common/src/main/java/net/lionarius/skinrestorer/util/WebUtils.java index c877bf2..1f6946a 100644 --- a/common/src/main/java/net/lionarius/skinrestorer/util/WebUtils.java +++ b/common/src/main/java/net/lionarius/skinrestorer/util/WebUtils.java @@ -36,7 +36,7 @@ public final class WebUtils { try { builder.connectTimeout(Duration.of(SkinRestorer.getConfig().requestTimeout(), ChronoUnit.SECONDS)); } catch (IllegalArgumentException e) { - SkinRestorer.LOGGER.error("failed to set request timeout", e); + SkinRestorer.LOGGER.error("Failed to set request timeout", e); builder.connectTimeout(Duration.of(10, ChronoUnit.SECONDS)); } diff --git a/common/src/main/java/net/lionarius/skinrestorer/util/gson/GsonPostProcessable.java b/common/src/main/java/net/lionarius/skinrestorer/util/gson/GsonPostProcessable.java new file mode 100644 index 0000000..b02128a --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/util/gson/GsonPostProcessable.java @@ -0,0 +1,5 @@ +package net.lionarius.skinrestorer.util.gson; + +public interface GsonPostProcessable { + void gsonPostProcess(); +} diff --git a/common/src/main/java/net/lionarius/skinrestorer/util/gson/PostProcessingEnabler.java b/common/src/main/java/net/lionarius/skinrestorer/util/gson/PostProcessingEnabler.java new file mode 100644 index 0000000..b6a2181 --- /dev/null +++ b/common/src/main/java/net/lionarius/skinrestorer/util/gson/PostProcessingEnabler.java @@ -0,0 +1,34 @@ +package net.lionarius.skinrestorer.util.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class PostProcessingEnabler implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + var adapter = gson.getDelegateAdapter(this, type); + + return new TypeAdapter<>() { + @Override + public void write(JsonWriter out, T value) throws IOException { + adapter.write(out, value); + } + + @Override + public T read(JsonReader in) throws IOException { + var value = adapter.read(in); + + if (value instanceof GsonPostProcessable postProcessable) + postProcessable.gsonPostProcess(); + + return value; + } + }; + } +} diff --git a/common/src/main/resources/skinrestorer.mixins.json b/common/src/main/resources/skinrestorer.mixins.json index a452f3b..084b56c 100644 --- a/common/src/main/resources/skinrestorer.mixins.json +++ b/common/src/main/resources/skinrestorer.mixins.json @@ -5,9 +5,9 @@ "compatibilityLevel": "JAVA_8", "refmap": "${mod_id}.refmap.json", "mixins": [ + "ChunkMapAccessor", "PlayerListMixin", "ServerLoginPacketListenerImplMixin", - "ChunkMapAccessor", "TrackedEntityAccessorInvoker" ], "injectors": { diff --git a/fabric/build.gradle b/fabric/build.gradle index ffdbf57..62adc91 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -1,10 +1,11 @@ plugins { id 'multiloader-loader' - + id 'fabric-loom' id 'multiloader-publish' } + dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" mappings loom.layered { @@ -12,6 +13,10 @@ dependencies { parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") } modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" + + modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" + + include implementation("org.mineskin:java-client:${mineskin_client_version}") } loom { @@ -19,11 +24,11 @@ loom { if (aw.exists()) { accessWidenerPath.set(aw) } - + mixin { defaultRefmapName.set("${mod_id}.refmap.json") } - + runs { client { client() diff --git a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/SkinRestorerFabric.java b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/SkinRestorerFabric.java index 906896e..ae7a70b 100644 --- a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/SkinRestorerFabric.java +++ b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/SkinRestorerFabric.java @@ -2,10 +2,18 @@ package net.lionarius.skinrestorer.fabric; import net.fabricmc.api.ModInitializer; import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility; +import net.lionarius.skinrestorer.platform.Services; public final class SkinRestorerFabric implements ModInitializer { + + public static final boolean FABRIC_API_LOADED = Services.PLATFORM.isModLoaded("fabric-api"); + @Override public void onInitialize() { SkinRestorer.onInitialize(); + + if (SkinShuffleCompatibility.shouldApply()) + net.lionarius.skinrestorer.fabric.compat.skinshuffle.SkinShuffleCompatibility.initialize(); } } diff --git a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/compat/skinshuffle/SkinShuffleCompatibility.java b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/compat/skinshuffle/SkinShuffleCompatibility.java new file mode 100644 index 0000000..662ae14 --- /dev/null +++ b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/compat/skinshuffle/SkinShuffleCompatibility.java @@ -0,0 +1,37 @@ +package net.lionarius.skinrestorer.fabric.compat.skinshuffle; + +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.lionarius.skinrestorer.SkinRestorer; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleHandshakePayload; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleSkinRefreshPayload; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleSkinRefreshV1Payload; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleSkinRefreshV2Payload; +import net.lionarius.skinrestorer.fabric.SkinRestorerFabric; + +public final class SkinShuffleCompatibility { + + private SkinShuffleCompatibility() {} + + public static void initialize() { + if (!SkinRestorerFabric.FABRIC_API_LOADED) { + SkinRestorer.LOGGER.warn("fabric-api is not loaded, SkinShuffle compatibility will not be available"); + return; + } + + PayloadTypeRegistry.playS2C().register(SkinShuffleHandshakePayload.PACKET_ID, SkinShuffleHandshakePayload.PACKET_CODEC); + PayloadTypeRegistry.playC2S().register(SkinShuffleSkinRefreshV1Payload.PACKET_ID, SkinShuffleSkinRefreshV1Payload.PACKET_CODEC); + PayloadTypeRegistry.playC2S().register(SkinShuffleSkinRefreshV2Payload.PACKET_ID, SkinShuffleSkinRefreshV2Payload.PACKET_CODEC); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility.onPlayerJoin(handler.getPlayer())); + + ServerPlayNetworking.registerGlobalReceiver(SkinShuffleSkinRefreshV1Payload.PACKET_ID, SkinShuffleCompatibility::handleSkinRefreshPacket); + + ServerPlayNetworking.registerGlobalReceiver(SkinShuffleSkinRefreshV2Payload.PACKET_ID, SkinShuffleCompatibility::handleSkinRefreshPacket); + } + + private static void handleSkinRefreshPacket(SkinShuffleSkinRefreshPayload payload, ServerPlayNetworking.Context context) { + net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility.handleSkinRefresh(context.player().getServer(), context.player(), payload); + } +} diff --git a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/CommandsMixin.java b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/CommandsMixin.java index b071501..cd10fcd 100644 --- a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/CommandsMixin.java +++ b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/CommandsMixin.java @@ -1,7 +1,7 @@ package net.lionarius.skinrestorer.fabric.mixin; import com.mojang.brigadier.CommandDispatcher; -import net.lionarius.skinrestorer.command.SkinCommand; +import net.lionarius.skinrestorer.SkinRestorer; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; @@ -21,6 +21,6 @@ public abstract class CommandsMixin { @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/commands/AdvancementCommands;register(Lcom/mojang/brigadier/CommandDispatcher;)V")) private void init(Commands.CommandSelection environment, CommandBuildContext commandRegistryAccess, CallbackInfo ci) { - SkinCommand.register(dispatcher); + SkinRestorer.Events.onCommandRegister(dispatcher); } } diff --git a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/MinecraftServerMixin.java b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/MinecraftServerMixin.java index ac6cdc2..f14d1ce 100644 --- a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/MinecraftServerMixin.java +++ b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/mixin/MinecraftServerMixin.java @@ -13,6 +13,6 @@ public abstract class MinecraftServerMixin { @Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;getNanos()J", ordinal = 0)) private void onServerStarted(CallbackInfo ci) { - SkinRestorer.onServerStarted((MinecraftServer) (Object) this); + SkinRestorer.Events.onServerStarted((MinecraftServer) (Object) this); } } diff --git a/fabric/src/main/java/net/lionarius/skinrestorer/fabric/platform/FabricCompatibilityHelper.java b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/platform/FabricCompatibilityHelper.java new file mode 100644 index 0000000..c8d7588 --- /dev/null +++ b/fabric/src/main/java/net/lionarius/skinrestorer/fabric/platform/FabricCompatibilityHelper.java @@ -0,0 +1,13 @@ +package net.lionarius.skinrestorer.fabric.platform; + +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleHandshakePayload; +import net.lionarius.skinrestorer.platform.services.CompatibilityHelper; +import net.minecraft.server.level.ServerPlayer; + +public final class FabricCompatibilityHelper implements CompatibilityHelper { + @Override + public void skinShuffle_sendHandshake(ServerPlayer player) { + ServerPlayNetworking.send(player, SkinShuffleHandshakePayload.INSTANCE); + } +} diff --git a/fabric/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper b/fabric/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper new file mode 100644 index 0000000..a325afc --- /dev/null +++ b/fabric/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper @@ -0,0 +1 @@ +net.lionarius.skinrestorer.fabric.platform.FabricCompatibilityHelper diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 89d74b6..85c219d 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -28,5 +28,8 @@ "fabricloader": ">=${fabric_loader_version}", "minecraft": ">=${minecraft_version}", "java": ">=${java_version}" + }, + "suggests": { + "fabric-api": "*" } } diff --git a/forge/build.gradle b/forge/build.gradle index 2b2e2e7..e3b12c2 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -2,7 +2,7 @@ plugins { id 'multiloader-loader' id 'idea' id 'eclipse' - + id 'net.minecraftforge.gradle' id 'org.parchmentmc.librarian.forgegradle' id 'org.spongepowered.mixin' @@ -15,6 +15,14 @@ mixin { config "${mod_id}.mixins.json" } +jarJar.enable() + +tasks.named('jarJar') { + archiveClassifier = '' +} + +jar.finalizedBy('jarJar') + minecraft { mappings channel: 'parchment', version: "${parchment_version}-${parchment_minecraft}" @@ -58,6 +66,11 @@ dependencies { annotationProcessor('org.spongepowered:mixin:0.8.5-SNAPSHOT:processor') implementation('net.sf.jopt-simple:jopt-simple:5.0.4') { version { strictly '5.0.4' } } + + // TODO: somehow fix forge gradle including dependencies of java-client + minecraftLibrary(jarJar(group: 'org.mineskin', name: 'java-client', version: mineskin_client_version)) { + jarJar.ranged(it, "[${mineskin_client_version},)") + } } // for some reason mixin plugin does not add 'MixinConfigs' to MANIFEST.MF so we do it manually diff --git a/forge/src/main/java/net/lionarius/skinrestorer/forge/SkinRestorerForge.java b/forge/src/main/java/net/lionarius/skinrestorer/forge/SkinRestorerForge.java index 5a0d03d..ef34aa0 100644 --- a/forge/src/main/java/net/lionarius/skinrestorer/forge/SkinRestorerForge.java +++ b/forge/src/main/java/net/lionarius/skinrestorer/forge/SkinRestorerForge.java @@ -1,8 +1,7 @@ package net.lionarius.skinrestorer.forge; import net.lionarius.skinrestorer.SkinRestorer; -import net.lionarius.skinrestorer.command.SkinCommand; -import net.minecraftforge.common.MinecraftForge; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.server.ServerStartedEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -13,18 +12,19 @@ import net.minecraftforge.fml.common.Mod; public final class SkinRestorerForge { public SkinRestorerForge() { - MinecraftForge.EVENT_BUS.register(SkinRestorerForge.class); - SkinRestorer.onInitialize(); + + if (SkinShuffleCompatibility.shouldApply()) + net.lionarius.skinrestorer.forge.compat.skinshuffle.SkinShuffleCompatibility.initialize(); } @SubscribeEvent public static void onCommandRegister(RegisterCommandsEvent event) { - SkinCommand.register(event.getDispatcher()); + SkinRestorer.Events.onCommandRegister(event.getDispatcher()); } @SubscribeEvent public static void onServerStarted(ServerStartedEvent event) { - SkinRestorer.onServerStarted(event.getServer()); + SkinRestorer.Events.onServerStarted(event.getServer()); } } diff --git a/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShuffleCompatibility.java b/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShuffleCompatibility.java new file mode 100644 index 0000000..a433843 --- /dev/null +++ b/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShuffleCompatibility.java @@ -0,0 +1,14 @@ +package net.lionarius.skinrestorer.forge.compat.skinshuffle; + +import net.minecraftforge.common.MinecraftForge; + +public final class SkinShuffleCompatibility { + + private SkinShuffleCompatibility() {} + + public static void initialize() { + MinecraftForge.EVENT_BUS.register(SkinShuffleGameEventHandler.class); + + SkinShufflePacketHandler.initialize(); + } +} diff --git a/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShuffleGameEventHandler.java b/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShuffleGameEventHandler.java new file mode 100644 index 0000000..70daee9 --- /dev/null +++ b/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShuffleGameEventHandler.java @@ -0,0 +1,16 @@ +package net.lionarius.skinrestorer.forge.compat.skinshuffle; + +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public final class SkinShuffleGameEventHandler { + + private SkinShuffleGameEventHandler() {} + + @SubscribeEvent + public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + SkinShuffleCompatibility.onPlayerJoin((ServerPlayer) event.getEntity()); + } +} diff --git a/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShufflePacketHandler.java b/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShufflePacketHandler.java new file mode 100644 index 0000000..9f6ba68 --- /dev/null +++ b/forge/src/main/java/net/lionarius/skinrestorer/forge/compat/skinshuffle/SkinShufflePacketHandler.java @@ -0,0 +1,59 @@ +package net.lionarius.skinrestorer.forge.compat.skinshuffle; + +import io.netty.buffer.Unpooled; +import net.lionarius.skinrestorer.compat.skinshuffle.*; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.event.network.CustomPayloadEvent; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.EventNetworkChannel; + +public class SkinShufflePacketHandler { + private SkinShufflePacketHandler() { + } + + private static final EventNetworkChannel HANDSHAKE_INSTANCE = ChannelBuilder + .named(SkinShuffleHandshakePayload.PACKET_ID.id()) + .optional() + .eventNetworkChannel(); + + private static final EventNetworkChannel SKIN_REFRESH_V1_INSTANCE = ChannelBuilder + .named(SkinShuffleSkinRefreshV1Payload.PACKET_ID.id()) + .optional() + .eventNetworkChannel() + .addListener(SkinShufflePacketHandler::skinRefreshV1Listener); + + private static final EventNetworkChannel SKIN_REFRESH_V2_INSTANCE = ChannelBuilder + .named(SkinShuffleSkinRefreshV2Payload.PACKET_ID.id()) + .optional() + .eventNetworkChannel() + .addListener(SkinShufflePacketHandler::skinRefreshV2Listener); + + protected static void initialize() { + // NO-OP + } + + public static void sendHandshake(Connection connection) { + HANDSHAKE_INSTANCE.send(new FriendlyByteBuf(Unpooled.buffer(0, 0)), connection); + } + + private static void skinRefreshV1Listener(CustomPayloadEvent event) { + var payload = SkinShuffleSkinRefreshV1Payload.PACKET_CODEC.decode(event.getPayload()); + handleSkinRefreshPacket(payload, event.getSource()); + } + + private static void skinRefreshV2Listener(CustomPayloadEvent event) { + var payload = SkinShuffleSkinRefreshV2Payload.PACKET_CODEC.decode(event.getPayload()); + handleSkinRefreshPacket(payload, event.getSource()); + } + + private static void handleSkinRefreshPacket(SkinShuffleSkinRefreshPayload payload, CustomPayloadEvent.Context context) { + var sender = context.getSender(); + + if (!context.isServerSide() || sender == null) + return; + + SkinShuffleCompatibility.handleSkinRefresh(sender.getServer(), sender, payload); + } +} diff --git a/forge/src/main/java/net/lionarius/skinrestorer/forge/platform/ForgeCompatibilityHelper.java b/forge/src/main/java/net/lionarius/skinrestorer/forge/platform/ForgeCompatibilityHelper.java new file mode 100644 index 0000000..f613a00 --- /dev/null +++ b/forge/src/main/java/net/lionarius/skinrestorer/forge/platform/ForgeCompatibilityHelper.java @@ -0,0 +1,13 @@ +package net.lionarius.skinrestorer.forge.platform; + +import net.lionarius.skinrestorer.forge.compat.skinshuffle.SkinShufflePacketHandler; +import net.lionarius.skinrestorer.platform.services.CompatibilityHelper; +import net.minecraft.server.level.ServerPlayer; + +public final class ForgeCompatibilityHelper implements CompatibilityHelper { + + @Override + public void skinShuffle_sendHandshake(ServerPlayer player) { + SkinShufflePacketHandler.sendHandshake(player.connection.getConnection()); + } +} diff --git a/forge/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper b/forge/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper new file mode 100644 index 0000000..73cddf5 --- /dev/null +++ b/forge/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper @@ -0,0 +1 @@ +net.lionarius.skinrestorer.forge.platform.ForgeCompatibilityHelper diff --git a/gradle.properties b/gradle.properties index 9e98084..9a5e78d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Project -group=net.lionarius.skinrestorer +group=net.lionarius java_version=17 # Common @@ -8,7 +8,7 @@ minecraft_version_list=1.20.3,1.20.4 minecraft_version_range=[1.20.3,1.20.4] mod_id=skinrestorer mod_name=SkinRestorer -mod_version=2.1.0 +mod_version=2.2.0 mod_author=Lionarius mod_homepage=https://modrinth.com/mod/skinrestorer mod_sources=https://github.com/Suiranoil/SkinRestorer @@ -17,12 +17,16 @@ license=MIT credits= description=A server-side mod for managing skins. +mineskin_client_version=3.0.1-SNAPSHOT + # ParchmentMC mappings, see https://parchmentmc.org/docs/getting-started#choose-a-version for new versions parchment_minecraft=1.20.3 parchment_version=2023.12.31 # Fabric, see https://fabricmc.net/develop/ for new versions fabric_loader_version=0.15.0 +fabric_api_version=0.91.1+1.20.3 +fabric_optional_dependencies=fabric-api # Forge, see https://files.minecraftforge.net/net/minecraftforge/forge/ for new versions forge_version=49.0.1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..9355b41 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf1..f5feea6 100644 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30d..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/neoforge/build.gradle b/neoforge/build.gradle index 7082cf5..872c927 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -14,6 +14,26 @@ if (at.exists()) { minecraft.accessTransformers.file(at) } +jarJar.enable() + +tasks.named('jar') { + archiveClassifier = 'thin' +} + +tasks.named('jarJar') { + archiveClassifier = '' +} + +jar.finalizedBy('jarJar') + +dependencies { + implementation "net.neoforged:neoforge:${neoforge_version}" + + implementation(jarJar(group: 'org.mineskin', name: 'java-client', version: mineskin_client_version)) { + jarJar.ranged(it, "[${mineskin_client_version},)") + } +} + subsystems { parchment { minecraftVersion = parchment_minecraft @@ -36,7 +56,3 @@ runs { workingDirectory = file('../run/server') } } - -dependencies { - implementation "net.neoforged:neoforge:${neoforge_version}" -} diff --git a/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/SkinRestorerNeoForge.java b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/SkinRestorerNeoForge.java index 54bd1ba..d0c8703 100644 --- a/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/SkinRestorerNeoForge.java +++ b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/SkinRestorerNeoForge.java @@ -1,10 +1,9 @@ package net.lionarius.skinrestorer.neoforge; import net.lionarius.skinrestorer.SkinRestorer; -import net.lionarius.skinrestorer.command.SkinCommand; +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.Mod; -import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.RegisterCommandsEvent; import net.neoforged.neoforge.event.server.ServerStartedEvent; @@ -13,18 +12,19 @@ import net.neoforged.neoforge.event.server.ServerStartedEvent; public final class SkinRestorerNeoForge { public SkinRestorerNeoForge() { - NeoForge.EVENT_BUS.register(SkinRestorerNeoForge.class); - SkinRestorer.onInitialize(); + + if (SkinShuffleCompatibility.shouldApply()) + net.lionarius.skinrestorer.neoforge.compat.skinshuffle.SkinShuffleCompatibility.initialize(); } @SubscribeEvent public static void onCommandRegister(RegisterCommandsEvent event) { - SkinCommand.register(event.getDispatcher()); + SkinRestorer.Events.onCommandRegister(event.getDispatcher()); } @SubscribeEvent public static void onServerStarted(ServerStartedEvent event) { - SkinRestorer.onServerStarted(event.getServer()); + SkinRestorer.Events.onServerStarted(event.getServer()); } } diff --git a/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleCompatibility.java b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleCompatibility.java new file mode 100644 index 0000000..702ea67 --- /dev/null +++ b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleCompatibility.java @@ -0,0 +1,17 @@ +package net.lionarius.skinrestorer.neoforge.compat.skinshuffle; + +import net.lionarius.skinrestorer.SkinRestorer; +import net.neoforged.fml.ModList; +import net.neoforged.neoforge.common.NeoForge; + +public final class SkinShuffleCompatibility { + + private SkinShuffleCompatibility() {} + + public static void initialize() { + NeoForge.EVENT_BUS.register(SkinShuffleGameEventHandler.class); + + final var mod = ModList.get().getModContainerById(SkinRestorer.MOD_ID).get(); + mod.getEventBus().register(SkinShuffleModEventHandler.class); + } +} diff --git a/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleGameEventHandler.java b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleGameEventHandler.java new file mode 100644 index 0000000..9f4b260 --- /dev/null +++ b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleGameEventHandler.java @@ -0,0 +1,16 @@ +package net.lionarius.skinrestorer.neoforge.compat.skinshuffle; + +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +public final class SkinShuffleGameEventHandler { + + private SkinShuffleGameEventHandler() {} + + @SubscribeEvent + public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + SkinShuffleCompatibility.onPlayerJoin((ServerPlayer) event.getEntity()); + } +} diff --git a/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleModEventHandler.java b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleModEventHandler.java new file mode 100644 index 0000000..6036393 --- /dev/null +++ b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/compat/skinshuffle/SkinShuffleModEventHandler.java @@ -0,0 +1,31 @@ +package net.lionarius.skinrestorer.neoforge.compat.skinshuffle; + +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleCompatibility; +import net.lionarius.skinrestorer.compat.skinshuffle.*; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.neoforged.neoforge.network.registration.HandlerThread; + +public final class SkinShuffleModEventHandler { + private SkinShuffleModEventHandler() {} + + @SubscribeEvent + public static void onRegisterPayloadHandlers(RegisterPayloadHandlersEvent event) { + final var registrar = event.registrar("1").optional().executesOn(HandlerThread.NETWORK); + + registrar + .playToClient(SkinShuffleHandshakePayload.PACKET_ID, SkinShuffleHandshakePayload.PACKET_CODEC, + (payload, context) -> {}) + .playToServer(SkinShuffleSkinRefreshV1Payload.PACKET_ID, SkinShuffleSkinRefreshV1Payload.PACKET_CODEC, + SkinShuffleModEventHandler::handleSkinRefreshPacket) + .playToServer(SkinShuffleSkinRefreshV2Payload.PACKET_ID, SkinShuffleSkinRefreshV2Payload.PACKET_CODEC, + SkinShuffleModEventHandler::handleSkinRefreshPacket); + } + + private static void handleSkinRefreshPacket(SkinShuffleSkinRefreshPayload payload, IPayloadContext context) { + var player = (ServerPlayer) context.player(); + SkinShuffleCompatibility.handleSkinRefresh(player.getServer(), player, payload); + } +} diff --git a/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/platform/NeoForgeCompatibilityHelper.java b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/platform/NeoForgeCompatibilityHelper.java new file mode 100644 index 0000000..4cd9a0c --- /dev/null +++ b/neoforge/src/main/java/net/lionarius/skinrestorer/neoforge/platform/NeoForgeCompatibilityHelper.java @@ -0,0 +1,15 @@ +package net.lionarius.skinrestorer.neoforge.platform; + +import net.lionarius.skinrestorer.compat.skinshuffle.SkinShuffleHandshakePayload; +import net.lionarius.skinrestorer.platform.services.CompatibilityHelper; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.server.level.ServerPlayer; + +public final class NeoForgeCompatibilityHelper implements CompatibilityHelper { + + @Override + public void skinShuffle_sendHandshake(ServerPlayer player) { + // we can't use the packet distributor here because neoforge doesn't support sending packets to non-neoforge players + player.connection.getConnection().send(new ClientboundCustomPayloadPacket(SkinShuffleHandshakePayload.INSTANCE)); + } +} diff --git a/neoforge/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper b/neoforge/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper new file mode 100644 index 0000000..5138d46 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/net.lionarius.skinrestorer.platform.services.CompatibilityHelper @@ -0,0 +1 @@ +net.lionarius.skinrestorer.neoforge.platform.NeoForgeCompatibilityHelper