├── src └── main │ ├── resources │ ├── presets │ │ ├── geyser.toml │ │ ├── meteor.toml │ │ ├── mojang.toml │ │ ├── wurst.toml │ │ ├── fallback.toml │ │ ├── fivezig.toml │ │ ├── liquidbounce.toml │ │ ├── minecraftcapes.toml │ │ ├── mantle.toml │ │ ├── optifine.toml │ │ ├── local.toml │ │ ├── cloaksplus.toml │ │ ├── elyby.toml │ │ ├── test.toml │ │ ├── labymod.toml │ │ ├── minelittlepony.toml │ │ ├── wynntils.toml │ │ ├── cosmetica.toml │ │ └── tlauncher.toml │ ├── assets │ │ └── skins │ │ │ ├── icon.png │ │ │ └── lang │ │ │ ├── ja_jp.json │ │ │ ├── en_ud.json │ │ │ ├── ru_ru.json │ │ │ ├── uk_ua.json │ │ │ ├── bs_ba.json │ │ │ ├── hr_hr.json │ │ │ ├── me_me.json │ │ │ ├── sr_cs.json │ │ │ ├── es_ar.json │ │ │ ├── es_bo.json │ │ │ ├── es_cl.json │ │ │ ├── es_ec.json │ │ │ ├── es_em.json │ │ │ ├── es_es.json │ │ │ ├── es_mx.json │ │ │ ├── es_uy.json │ │ │ ├── es_ve.json │ │ │ └── en_us.json │ ├── skins.mixins.json │ ├── skins.accesswidener │ └── fabric.mod.json │ └── java │ └── net │ └── zatrit │ └── skins │ ├── accessor │ ├── Refreshable.java │ ├── AsyncUUIDRefresher.java │ └── HasAssetPath.java │ ├── config │ ├── FilterMode.java │ ├── UuidMode.java │ ├── SkinsConfig.java │ ├── HostEntry.java │ ├── TomlConfigSerializer.java │ └── Resolvers.java │ ├── texture │ ├── TextureIdentifier.java │ ├── TextureLoader.java │ └── AnimatedTexture.java │ ├── util │ ├── ExceptionConsumer.java │ ├── TextureTypeUtil.java │ ├── command │ │ ├── FileProvider.java │ │ ├── DirectoryFileProvider.java │ │ ├── CommandUtil.java │ │ ├── IndexedResourceProvider.java │ │ ├── FileArgumentType.java │ │ └── TextUtil.java │ └── ExceptionConsumerImpl.java │ ├── cache │ ├── AssetCacheProvider.java │ └── AssetCache.java │ ├── mixin │ ├── MinecraftClientMixin.java │ ├── PlayerListEntryMixin.java │ ├── GameProfileMixin.java │ └── PlayerSkinProviderMixin.java │ ├── ElytraTextureFix.java │ ├── FallbackResolver.java │ ├── SkinsClient.java │ ├── ModMenuIntegration.java │ └── SkinsCommands.java ├── docs ├── img │ └── loadingOrder.png ├── httpServer.md ├── localResolverHierarchy.md ├── commands.md ├── readme.md └── presets.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── CHANGELOG.md ├── jitpack.yml ├── NOTICE.txt ├── settings.gradle ├── README.md ├── .gitignore ├── crowdin.yml ├── gradle.properties ├── scripts ├── index.gradle └── modrinth.gradle ├── .github └── workflows │ └── crowdin.yml ├── license ├── MIT.txt └── APACHE.txt ├── gradlew.bat └── gradlew /src/main/resources/presets/geyser.toml: -------------------------------------------------------------------------------- 1 | type = 'GEYSER' -------------------------------------------------------------------------------- /src/main/resources/presets/meteor.toml: -------------------------------------------------------------------------------- 1 | type = 'METEOR' -------------------------------------------------------------------------------- /src/main/resources/presets/mojang.toml: -------------------------------------------------------------------------------- 1 | type = 'MOJANG' -------------------------------------------------------------------------------- /src/main/resources/presets/wurst.toml: -------------------------------------------------------------------------------- 1 | type = 'WURST' -------------------------------------------------------------------------------- /src/main/resources/presets/fallback.toml: -------------------------------------------------------------------------------- 1 | type = 'FALLBACK' -------------------------------------------------------------------------------- /src/main/resources/presets/fivezig.toml: -------------------------------------------------------------------------------- 1 | type = 'FIVEZIG' -------------------------------------------------------------------------------- /src/main/resources/presets/liquidbounce.toml: -------------------------------------------------------------------------------- 1 | type = 'LIQUID_BOUNCE' -------------------------------------------------------------------------------- /src/main/resources/presets/minecraftcapes.toml: -------------------------------------------------------------------------------- 1 | type = 'MINECRAFT_CAPES' -------------------------------------------------------------------------------- /docs/img/loadingOrder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zatrit/openmcskins2/HEAD/docs/img/loadingOrder.png -------------------------------------------------------------------------------- /src/main/resources/presets/mantle.toml: -------------------------------------------------------------------------------- 1 | type = 'OPTIFINE' 2 | 3 | [properties] 4 | base_url = 'http://35.190.10.249' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zatrit/openmcskins2/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/presets/optifine.toml: -------------------------------------------------------------------------------- 1 | type = 'OPTIFINE' 2 | 3 | [properties] 4 | base_url = 'http://s.optifine.net' -------------------------------------------------------------------------------- /src/main/resources/presets/local.toml: -------------------------------------------------------------------------------- 1 | type = 'LOCAL' 2 | 3 | [properties] 4 | directory = '#{configDir}/openmcskins/local' -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | * Added back support for 1.20.4 2 | * Improved performance 3 | * Added automatic directories creation to LocalResolver -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - sdk install java 17.0.3-open 5 | - sdk use java 17.0.3-open 6 | -------------------------------------------------------------------------------- /src/main/resources/presets/cloaksplus.toml: -------------------------------------------------------------------------------- 1 | type = 'OPTIFINE' 2 | 3 | [properties] 4 | base_url = 'https://server.cloaksplus.com' -------------------------------------------------------------------------------- /src/main/resources/presets/elyby.toml: -------------------------------------------------------------------------------- 1 | type = 'NAMED_HTTP' 2 | 3 | [properties] 4 | base_url = 'http://skinsystem.ely.by/textures/' -------------------------------------------------------------------------------- /src/main/resources/presets/test.toml: -------------------------------------------------------------------------------- 1 | type = 'NAMED_HTTP' 2 | 3 | [properties] 4 | base_url = 'http://127.0.0.1:8000/textures/' -------------------------------------------------------------------------------- /src/main/resources/assets/skins/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zatrit/openmcskins2/HEAD/src/main/resources/assets/skins/icon.png -------------------------------------------------------------------------------- /src/main/resources/presets/labymod.toml: -------------------------------------------------------------------------------- 1 | type = 'DIRECT' 2 | 3 | [properties] 4 | types = ['CAPE'] 5 | base_url = 'https://dl.labymod.net/capes/{id}' -------------------------------------------------------------------------------- /src/main/resources/presets/minelittlepony.toml: -------------------------------------------------------------------------------- 1 | type = 'VALHALLA' 2 | 3 | [properties] 4 | base_url = 'https://skins.minelittlepony-mod.com/api/v1/user/' -------------------------------------------------------------------------------- /src/main/resources/presets/wynntils.toml: -------------------------------------------------------------------------------- 1 | type = 'DIRECT' 2 | 3 | [properties] 4 | types = ['CAPE'] 5 | base_url = 'https://athena.wynntils.com/capes/user/{id}' -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/accessor/Refreshable.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.accessor; 2 | 3 | public interface Refreshable { 4 | void skins$refresh(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/presets/cosmetica.toml: -------------------------------------------------------------------------------- 1 | type = 'DIRECT' 2 | 3 | [properties] 4 | types = ['CAPE'] 5 | base_url = 'https://api.cosmetica.cc/get/cloak?username={name}¬hirdparty' -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/config/FilterMode.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.config; 2 | 3 | public enum FilterMode { 4 | WHITELIST, 5 | BLACKLIST, 6 | NONE, 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/presets/tlauncher.toml: -------------------------------------------------------------------------------- 1 | # MOD DEVELOPER DOES NOT SUPPORT PIRACY!!!! 2 | type = 'NAMED_HTTP' 3 | 4 | [properties] 5 | base_url = 'https://auth.tlauncher.org/skin/profile/texture/login/' -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | This product includes aleph-formatter (https://github.com/nomemory/aleph-formatter) 2 | licensed under the Apache 2.0 license. 3 | 4 | This product also includes toml4j (https://github.com/mwanji/toml4j) 5 | licensed under the MIT license. -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } 11 | 12 | rootProject.name = 'openmcskins' -------------------------------------------------------------------------------- /src/main/resources/skins.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.zatrit.skins.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "client": [ 7 | "GameProfileMixin", 8 | "MinecraftClientMixin", 9 | "PlayerListEntryMixin", 10 | "PlayerSkinProviderMixin" 11 | ], 12 | "injectors": { 13 | "defaultRequire": 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/accessor/AsyncUUIDRefresher.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.accessor; 2 | 3 | import net.zatrit.skins.lib.api.Profile; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | public interface AsyncUUIDRefresher { 8 | /** 9 | * Asynchronously refreshes UUID from Mojang API or other API. 10 | */ 11 | CompletableFuture skins$refreshUuid(); 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenMCSkins 2 2 | 3 | [![Crowdin](https://badges.crowdin.net/openmcskins/localized.svg)](https://crowdin.com/project/openmcskins) 4 | 5 | ## [Official Discord server](https://discord.gg/P4SX2uEspy) 6 | 7 | ## License 8 | 9 | This mod is licensed under the [Apache License 2.0](license/APACHE.txt). 10 | 11 | Check [NOTICE](NOTICE.txt), [MIT](license/MIT.txt) and 12 | [APACHE](license/APACHE.txt) for further information. -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/accessor/HasAssetPath.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.accessor; 2 | 3 | import java.nio.file.Path; 4 | 5 | public interface HasAssetPath { 6 | /** 7 | * Used to cache skins. 8 | * 9 | * @return the path for the assets folder of the game. 10 | * @see net.zatrit.skins.mixin.MinecraftClientMixin 11 | * @see net.zatrit.skins.cache.AssetCache 12 | */ 13 | Path getAssetPath(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/config/UuidMode.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.config; 2 | 3 | /** 4 | * UUID refresh mode. 5 | */ 6 | public enum UuidMode { 7 | /** 8 | * Never refresh UUID. 9 | */ 10 | NEVER, 11 | /** 12 | * Refresh UUID always before loading the skin. 13 | */ 14 | ALWAYS, 15 | /** 16 | * Refresh UUID in offline mode only. 17 | */ 18 | OFFLINE, 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | local.properties 8 | 9 | # eclipse 10 | 11 | *.launch 12 | 13 | # idea 14 | 15 | .idea/ 16 | *.iml 17 | *.ipr 18 | *.iws 19 | 20 | # vscode 21 | 22 | .settings 23 | .vscode 24 | bin/ 25 | .classpath 26 | .project 27 | 28 | # macos 29 | 30 | *.DS_Store 31 | 32 | # fabric 33 | 34 | run/ 35 | 36 | # java 37 | 38 | hs_err_*.log 39 | replay_*.log 40 | *.hprof 41 | *.jfr 42 | 43 | # python 44 | .venv 45 | __pycache__ -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Your Crowdin credentials 3 | # 4 | project_id_env: "CROWDIN_PROJECT_ID" 5 | api_token_env: "CROWDIN_PERSONAL_TOKEN" 6 | base_path: "src/main/resources/assets/skins/lang" 7 | base_url: "https://api.crowdin.com" 8 | 9 | # 10 | # Choose file structure in Crowdin 11 | # e.g. true or false 12 | # 13 | preserve_hierarchy: true 14 | 15 | # 16 | # Files configuration 17 | # 18 | files: [ 19 | { 20 | source: "/en_us.json", 21 | translation: "/%locale_with_underscore%.json", 22 | dest: "/locale.json" 23 | } 24 | ] -------------------------------------------------------------------------------- /docs/httpServer.md: -------------------------------------------------------------------------------- 1 | # HTTP server 2 | 3 | This HTTP server API can be used for TLauncher, ely.by and some other skin sources. 4 | 5 | If you want to launch a skin server, or just understand how it works, you can take a look: 6 | * [Chrly from ely.by](https://github.com/elyby/chrly) 7 | * [Documentation from ely.by](https://docs.ely.by/en/skins-system.html) 8 | * [Skin server Implementation on Flask](https://github.com/zatrit/openmcskins2/tree/876f75243a4a8bca60b1e2bd10c3743c89ed956b/server) 9 | * [Ruby skin server implementation by KerbalNo15](https://github.com/KerbalNo15/mcskinserver-ruby) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx4G 3 | org.gradle.caching=true 4 | # Mod Properties 5 | mod_version=2.0.0b6 6 | maven_group=net.zatrit 7 | archive_base_name=openmcskins 8 | # Fabric Properties 9 | minecraft_version=1.20.4 10 | yarn_mappings=1.20.4+build.3 11 | loader_version=0.15.6 12 | #Fabric api 13 | fabric_version=0.95.4+1.20.4 14 | # Other libraries 15 | skinlib_version=1.0.3 16 | toml4j_version=0.7.3 17 | lombok_version=1.18.32 18 | jb_annotations_version=24.1.0 19 | aleph_version=0.1.1 20 | yacl_version=3.5.0+1.20.4-fabric 21 | modmenu_version=9.0.0 -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/texture/TextureIdentifier.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.texture; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import net.minecraft.util.Identifier; 7 | import net.zatrit.skins.lib.TextureType; 8 | 9 | @Getter 10 | @EqualsAndHashCode 11 | @AllArgsConstructor 12 | public class TextureIdentifier { 13 | private String name; 14 | private TextureType type; 15 | 16 | public Identifier asId() { 17 | return Identifier.of("skins", name.hashCode() + "_" + type.ordinal()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/ExceptionConsumer.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | @FunctionalInterface 7 | public interface ExceptionConsumer 8 | extends Consumer, Function { 9 | default ExceptionConsumer andReturn(T value) { 10 | return error -> { 11 | this.accept(error); 12 | return value; 13 | }; 14 | } 15 | 16 | @Override 17 | default void accept(Throwable error) { 18 | this.apply(error); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/index.gradle: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/52976567/12245612 2 | static createIndex(File path) { 3 | def index = path.listFiles().findAll { it.isFile() }.name 4 | return index.join('\n') 5 | } 6 | 7 | def indexDir(String resourceDir, String name) { 8 | file("$resourceDir/$name").mkdirs() 9 | def presetIndexFile = file("$resourceDir/${name}.index") 10 | presetIndexFile.delete() 11 | 12 | sourceSets.main.resources.srcDirs.path.forEach { 13 | presetIndexFile << createIndex(file("$it/$name")) 14 | } 15 | } 16 | 17 | tasks.register('createIndex') { 18 | def resourceDir = "$buildDir/resources/main" 19 | indexDir resourceDir, 'presets' 20 | } -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/cache/AssetCacheProvider.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.cache; 2 | 3 | import lombok.Getter; 4 | import net.zatrit.skins.accessor.HasAssetPath; 5 | import net.zatrit.skins.lib.api.cache.Cache; 6 | import net.zatrit.skins.lib.api.cache.CacheProvider; 7 | 8 | /** 9 | * Cache provider for game asset directory. 10 | * {@inheritDoc} 11 | */ 12 | @Getter 13 | public class AssetCacheProvider implements CacheProvider { 14 | public static final String CACHE_DIR = "omcs"; 15 | private final Cache skinCache; 16 | 17 | public AssetCacheProvider(HasAssetPath path) { 18 | this.skinCache = new AssetCache(path, CACHE_DIR); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/localResolverHierarchy.md: -------------------------------------------------------------------------------- 1 | # The directory structure of the local skin storage looks like this: 2 | 3 | * textures 4 | * skin 5 | * [PLAYER NAME].png 6 | * cape 7 | * [PLAYER NAME].png 8 | * ears 9 | * [PLAYER NAME].png 10 | * metadata (OPTIONAL) 11 | * skin 12 | * [PLAYER NAME].json 13 | * cape 14 | * [PLAYER NAME].json 15 | 16 | Skin metadata is a JSON file, that contains skin model name 17 | 18 | ### Example skin metadata for a skin with a slim model: 19 | 20 | *you shouldn't use metadata for the default skin model, it works without it anyway* 21 | 22 | ```json 23 | { 24 | "model": "slim" 25 | } 26 | ``` 27 | 28 | ### Example animated cape metadata: 29 | 30 | ```json 31 | { 32 | "animated": true 33 | } 34 | ``` -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/TextureTypeUtil.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util; 2 | 3 | import com.mojang.authlib.minecraft.MinecraftProfileTexture; 4 | import lombok.experimental.UtilityClass; 5 | import net.zatrit.skins.lib.TextureType; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | @UtilityClass 9 | public class TextureTypeUtil { 10 | /** 11 | * Converts texture type from {@link MinecraftProfileTexture.Type} 12 | * to {@link TextureType}. 13 | */ 14 | public static TextureType fromAuthlibType( 15 | MinecraftProfileTexture.@NotNull Type type) { 16 | return switch (type) { 17 | case SKIN -> TextureType.SKIN; 18 | case CAPE -> TextureType.CAPE; 19 | default -> null; 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/command/FileProvider.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util.command; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Path; 7 | import java.util.Collection; 8 | import java.util.Optional; 9 | 10 | /** 11 | * An interface for reading and listing abstract files. 12 | * 13 | * @see IndexedResourceProvider 14 | * @see DirectoryFileProvider 15 | * @see FileArgumentType 16 | */ 17 | public interface FileProvider { 18 | /** 19 | * @return a collection of file names. 20 | */ 21 | Collection listFiles() throws IOException; 22 | 23 | /** 24 | * Opens an InputStream from an abstract file. 25 | */ 26 | @NotNull Optional getFile(String path); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/ExceptionConsumerImpl.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | import net.minecraft.util.logging.LoggerPrintStream; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class ExceptionConsumerImpl implements ExceptionConsumer { 11 | private static final LoggerPrintStream printStream = new LoggerPrintStream( 12 | "OMCS", 13 | System.out 14 | ); 15 | private boolean verbose; 16 | 17 | @Override 18 | public Void apply(@NotNull Throwable error) { 19 | if (this.verbose) { 20 | error.printStackTrace(printStream); 21 | } 22 | 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | With the commands you can add a source of skins or change their order, 4 | instead of changing the config file manually. 5 | 6 | In OpenMCSkins, there`s a command ``/omcs`` (an alias for ``/openmcskins``) for this. 7 | 8 | ## Subcommands: 9 | 10 | * ``/omcs list`` - lists all host list. 11 | * ``/omcs add (preset) [pos]`` - adds a new host to config by cloning an existing [preset](presets.md). If the ``pos`` 12 | argument is passed, inserts host at the given position, otherwise to the top of the list. 13 | * ``/omcs remove (pos)`` - removes a host from the list of hosts at the specified position. 14 | * ``/omcs move (from) (to)`` - changes the host position. 15 | * ``/omcs refresh`` - forces OpenMCSkins to re-download all players' skins. 16 | * ``/omcs clean`` - deletes all skin caches on disk. -------------------------------------------------------------------------------- /scripts/modrinth.gradle: -------------------------------------------------------------------------------- 1 | //file:noinspection GroovyAccessibility,GroovyAssignabilityCheck 2 | @SuppressWarnings('GrMethodMayBeStatic') 3 | def getEnv(String key, String value) { 4 | return System.getenv(key.toUpperCase()) ?: localProps.getProperty(key) ?: value 5 | } 6 | 7 | modrinth { 8 | token = getEnv('modrinth_token', '') 9 | projectId = getEnv('modrinth_id', 'openmcskins') 10 | versionName = "OpenMCSkins $version for $minecraft_version" 11 | versionNumber = "$version-$minecraft_version" 12 | versionType = 'beta' 13 | uploadFile = remapJar 14 | gameVersions = ['1.20.4', '1.21'] 15 | loaders = ['fabric', 'quilt'] 16 | changelog = file('CHANGELOG.md').readLines().join '\n' 17 | 18 | dependencies { 19 | required.project 'fabric-api' 20 | required.project 'yacl' 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/mixin/MinecraftClientMixin.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.mixin; 2 | 3 | import lombok.Getter; 4 | import net.minecraft.client.MinecraftClient; 5 | import net.minecraft.client.RunArgs; 6 | import net.zatrit.skins.accessor.HasAssetPath; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Unique; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | 14 | import java.nio.file.Path; 15 | 16 | @Getter 17 | @Mixin(MinecraftClient.class) 18 | public class MinecraftClientMixin implements HasAssetPath { 19 | private @Unique Path assetPath; 20 | 21 | @Inject(method = "", at = @At("RETURN")) 22 | public void init(@NotNull RunArgs args, CallbackInfo ci) { 23 | this.assetPath = args.directories.assetDir.toPath(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/skins.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | accessible class net/minecraft/client/texture/PlayerSkinProvider$Key 3 | 4 | accessible method net/minecraft/client/texture/PlayerSkinProvider$Key (Ljava/util/UUID;Lcom/mojang/authlib/properties/Property;)V 5 | accessible method net/minecraft/client/network/AbstractClientPlayerEntity getPlayerListEntry ()Lnet/minecraft/client/network/PlayerListEntry; 6 | 7 | accessible field net/minecraft/client/util/SkinTextures texture Lnet/minecraft/util/Identifier; 8 | accessible field net/minecraft/client/util/SkinTextures capeTexture Lnet/minecraft/util/Identifier; 9 | accessible field net/minecraft/client/util/SkinTextures model Lnet/minecraft/client/util/SkinTextures$Model; 10 | mutable field net/minecraft/client/util/SkinTextures texture Lnet/minecraft/util/Identifier; 11 | mutable field net/minecraft/client/util/SkinTextures capeTexture Lnet/minecraft/util/Identifier; 12 | mutable field net/minecraft/client/util/SkinTextures model Lnet/minecraft/client/util/SkinTextures$Model; -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/ja_jp.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "設定したホスト:", 3 | "openmcskins.command.moved": "ホストは%dから%dへ移動されました。", 4 | "openmcskins.options.title": "OpenMCSkinsの設定", 5 | "openmcskins.category.general": "一般", 6 | "openmcskins.option.cacheTextures": "テクスチャのキャッシュ", 7 | "openmcskins.option.verboseLogs": "詳細なログ", 8 | "openmcskins.option.loaderTimeout": "ローダーのタイムアウト", 9 | "openmcskins.option.refreshOnConfigSave": "設定の保存時に更新", 10 | "openmcskins.option.uuidMode": "UUIDを更新", 11 | "openmcskins.option.uuidMode.never": "常に無効", 12 | "openmcskins.option.uuidMode.always": "常に有効", 13 | "openmcskins.option.uuidMode.offline": "オフライン時", 14 | "openmcskins.command.noPreset": "プリセットが見つかりませんでした。", 15 | "openmcskins.command.invalidFileFormat": "無効な入力ファイル形式です。", 16 | "openmcskins.command.unableToRefresh": "スキンを更新できません。", 17 | "openmcskins.command.cleanupAlready": "キャッシュは既に消去されています。", 18 | "openmcskins.command.cleanupSuccess": "キャッシュが正常に消去されました。", 19 | "openmcskins.command.cleanupFailed": "キャッシュを消去できません:%s" 20 | } -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/command/DirectoryFileProvider.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Cleanup; 5 | import lombok.val; 6 | import org.apache.commons.io.FilenameUtils; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.util.Collection; 13 | import java.util.Optional; 14 | 15 | @AllArgsConstructor 16 | public class DirectoryFileProvider implements FileProvider { 17 | private final Path path; 18 | 19 | @Override 20 | public Collection listFiles() throws IOException { 21 | @Cleanup val files = Files.list(this.path); 22 | return files.map(p -> FilenameUtils.getName(p.toString())).toList(); 23 | } 24 | 25 | @Override 26 | public @NotNull Optional getFile(String name) { 27 | val path = this.path.resolve(name); 28 | return Files.isRegularFile(path) ? Optional.of(path) : Optional.empty(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/crowdin.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Action 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | synchronize-with-crowdin: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Crowdin action 17 | uses: crowdin/github-action@v1 18 | with: 19 | upload_sources: true 20 | upload_translations: true 21 | download_translations: true 22 | localization_branch_name: l10n_crowdin_translations 23 | create_pull_request: true 24 | pull_request_title: 'New Crowdin Translations' 25 | pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' 26 | pull_request_base_branch_name: 'main' 27 | 28 | config: crowdin.yml 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 32 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 33 | -------------------------------------------------------------------------------- /license/MIT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/en_ud.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": ":sʇsoɥ pǝɹnᵷᴉɟuoƆ", 3 | "openmcskins.command.moved": "˙ʎꞁꞁnɟssǝɔɔns %d oʇ %d ɯoɹɟ pǝʌoɯ sɐʍ ʇsoɥ Ɐ", 4 | "openmcskins.options.title": "suoᴉʇdo suᴉʞSƆWuǝdO", 5 | "openmcskins.category.general": "ꞁɐɹǝuǝ⅁", 6 | "openmcskins.option.cacheTextures": "ᵷuᴉɥɔɐɔ ǝɹnʇxǝ⟘", 7 | "openmcskins.option.verboseLogs": "sᵷoꞁ ǝsoqɹǝɅ", 8 | "openmcskins.option.loaderTimeout": "ʇnoǝɯᴉʇ ɹǝpɐoꞀ", 9 | "openmcskins.option.refreshOnConfigSave": "ǝʌɐs ᵷᴉɟuoɔ uo ɥsǝɹɟǝᴚ", 10 | "openmcskins.option.uuidMode": "ᗡI∩∩ ɥsǝɹɟǝᴚ", 11 | "openmcskins.option.uuidMode.never": "ɹǝʌǝN", 12 | "openmcskins.option.uuidMode.always": "sʎɐʍꞁⱯ", 13 | "openmcskins.option.uuidMode.offline": "ǝuᴉꞁɟɟo uǝɥM", 14 | "openmcskins.command.noPreset": "˙punoɟ ʇǝsǝɹd oN", 15 | "openmcskins.command.invalidFileFormat": "˙ʇɐɯɹoɟ ǝꞁᴉɟ ʇnduᴉ pᴉꞁɐʌuI", 16 | "openmcskins.command.unableToRefresh": "˙suᴉʞs ɥsǝɹɟǝɹ oʇ ǝꞁqɐu∩", 17 | "openmcskins.command.cleanupAlready": "˙ᵷuᴉɹɐǝꞁɔ ʎpɐǝɹꞁɐ sᴉ ǝɥɔɐɔ ǝɥ⟘", 18 | "openmcskins.command.cleanupSuccess": "˙pǝɹɐǝꞁɔ ʎꞁꞁnɟssǝɔɔns ǝɥɔɐƆ", 19 | "openmcskins.command.cleanupFailed": "%s :ǝɥɔɐɔ ɹɐǝꞁɔ oʇ ǝꞁqɐu∩" 20 | } -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/config/SkinsConfig.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.config; 2 | 3 | import com.google.common.collect.Lists; 4 | import dev.isxander.yacl3.config.v2.api.SerialEntry; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import lombok.experimental.FieldDefaults; 9 | 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Set; 13 | 14 | @Getter 15 | @Setter 16 | @FieldDefaults(level = AccessLevel.PRIVATE) 17 | public class SkinsConfig { 18 | @SerialEntry boolean cacheTextures = true; 19 | @SerialEntry boolean verboseLogs = false; 20 | @SerialEntry boolean refreshOnConfigSave = true; 21 | @SerialEntry double loaderTimeout = 2; 22 | @SerialEntry UuidMode uuidMode = UuidMode.OFFLINE; 23 | @SerialEntry List hosts = Lists.newArrayList( 24 | new HostEntry(HostEntry.HostType.MOJANG), 25 | new HostEntry(HostEntry.HostType.FALLBACK) 26 | ); 27 | 28 | @SerialEntry FilterMode filterMode = FilterMode.NONE; 29 | @SerialEntry Set whitelist = new HashSet<>(); 30 | @SerialEntry Set blacklist = new HashSet<>(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/ru_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Настроенные хосты:", 3 | "openmcskins.command.moved": "Хост был успешно перемещен из %d в %d.", 4 | "openmcskins.options.title": "Настройки OpenMCSkins", 5 | "openmcskins.category.general": "Основные", 6 | "openmcskins.option.cacheTextures": "Кешировать текстуры", 7 | "openmcskins.option.verboseLogs": "Подробный лог", 8 | "openmcskins.option.loaderTimeout": "Тайм-аут загрузки", 9 | "openmcskins.option.refreshOnConfigSave": "Обновлять при сохранении конфигурации", 10 | "openmcskins.option.uuidMode": "Обновлять UUID", 11 | "openmcskins.option.uuidMode.never": "Никогда", 12 | "openmcskins.option.uuidMode.always": "Всегда", 13 | "openmcskins.option.uuidMode.offline": "Когда оффлайн", 14 | "openmcskins.command.noPreset": "Предустановка не найдена.", 15 | "openmcskins.command.invalidFileFormat": "Неверный формат файла.", 16 | "openmcskins.command.unableToRefresh": "Не удалось обновить скины.", 17 | "openmcskins.command.cleanupAlready": "Кэш уже очищается.", 18 | "openmcskins.command.cleanupSuccess": "Кэш успешно очищен.", 19 | "openmcskins.command.cleanupFailed": "Не удалось очистить кэш: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/uk_ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Налаштовані хости:", 3 | "openmcskins.command.moved": "Хост був успішно перенесений з %d до %d.", 4 | "openmcskins.options.title": "Налаштування OpenMCSkins", 5 | "openmcskins.category.general": "Загальні", 6 | "openmcskins.option.cacheTextures": "Кешування текстур", 7 | "openmcskins.option.verboseLogs": "Детальний звіт", 8 | "openmcskins.option.loaderTimeout": "Час очікування завантажувача", 9 | "openmcskins.option.refreshOnConfigSave": "Оновити при збереженні конфігурації", 10 | "openmcskins.option.uuidMode": "Оновити UUID", 11 | "openmcskins.option.uuidMode.never": "Ніколи", 12 | "openmcskins.option.uuidMode.always": "Завжди", 13 | "openmcskins.option.uuidMode.offline": "Коли не в мережі", 14 | "openmcskins.command.noPreset": "Шаблонів не знайдено.", 15 | "openmcskins.command.invalidFileFormat": "Невірний формат файлу.", 16 | "openmcskins.command.unableToRefresh": "Не вдалося оновити скіни.", 17 | "openmcskins.command.cleanupAlready": "Кеш вже очищується.", 18 | "openmcskins.command.cleanupSuccess": "Кеш успішно очищено.", 19 | "openmcskins.command.cleanupFailed": "Не вдалося очистити кеш: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/bs_ba.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Podešeni hostovi:", 3 | "openmcskins.command.moved": "Uspješna promjena sa hosta %d na %d.", 4 | "openmcskins.options.title": "OpenMCSkins postavke", 5 | "openmcskins.category.general": "Opšte", 6 | "openmcskins.option.cacheTextures": "Zapamti teksture", 7 | "openmcskins.option.verboseLogs": "Opširni logovi", 8 | "openmcskins.option.loaderTimeout": "Čekanje učitavača", 9 | "openmcskins.option.refreshOnConfigSave": "Osvježi na spremanju postavki", 10 | "openmcskins.option.uuidMode": "Osvježi UUID", 11 | "openmcskins.option.uuidMode.never": "Nikad", 12 | "openmcskins.option.uuidMode.always": "Uvijek", 13 | "openmcskins.option.uuidMode.offline": "Na offline serveru", 14 | "openmcskins.command.noPreset": "Nema pronađenih postavki.", 15 | "openmcskins.command.invalidFileFormat": "Neispravan unosni format.", 16 | "openmcskins.command.unableToRefresh": "Nemoguće osvježavanje skinova.", 17 | "openmcskins.command.cleanupAlready": "Keš memorija se već čisti.", 18 | "openmcskins.command.cleanupSuccess": "Keš memorija je uspješno očiščena.", 19 | "openmcskins.command.cleanupFailed": "Nije moguće očistiti keš memoriju: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/hr_hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Podešeni hostovi:", 3 | "openmcskins.command.moved": "Uspješna promjena sa hosta %d na %d.", 4 | "openmcskins.options.title": "OpenMCSkins postavke", 5 | "openmcskins.category.general": "Općenito", 6 | "openmcskins.option.cacheTextures": "Zapamti teksture", 7 | "openmcskins.option.verboseLogs": "Opširni logovi", 8 | "openmcskins.option.loaderTimeout": "Čekanje učitavača", 9 | "openmcskins.option.refreshOnConfigSave": "Osvježi na spremanju postavki", 10 | "openmcskins.option.uuidMode": "Osvježi UUID", 11 | "openmcskins.option.uuidMode.never": "Nikad", 12 | "openmcskins.option.uuidMode.always": "Uvijek", 13 | "openmcskins.option.uuidMode.offline": "Na offline serveru", 14 | "openmcskins.command.noPreset": "Nema pronađenih postavki.", 15 | "openmcskins.command.invalidFileFormat": "Neispravan unosni format.", 16 | "openmcskins.command.unableToRefresh": "Nemoguće osvježavanje skinova.", 17 | "openmcskins.command.cleanupAlready": "Predmemorija se već čisti.", 18 | "openmcskins.command.cleanupSuccess": "Predmemorija je uspješno očiščena.", 19 | "openmcskins.command.cleanupFailed": "Nije moguće očistiti predmemoriju: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/me_me.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Konfigurisani hostovi:", 3 | "openmcskins.command.moved": "Uspešna promena sa hosta %d na %d.", 4 | "openmcskins.options.title": "OpenMCSkins postavke", 5 | "openmcskins.category.general": "Opšte", 6 | "openmcskins.option.cacheTextures": "Zapamti teksture", 7 | "openmcskins.option.verboseLogs": "Opširni logovi", 8 | "openmcskins.option.loaderTimeout": "Čekanje učitavača", 9 | "openmcskins.option.refreshOnConfigSave": "Osveži pri čuvanju konfiguracije", 10 | "openmcskins.option.uuidMode": "Osveži UUID", 11 | "openmcskins.option.uuidMode.never": "Nikad", 12 | "openmcskins.option.uuidMode.always": "Uvijek", 13 | "openmcskins.option.uuidMode.offline": "Na offline serveru", 14 | "openmcskins.command.noPreset": "Nema pronađenih postavki.", 15 | "openmcskins.command.invalidFileFormat": "Neispravan unosni format.", 16 | "openmcskins.command.unableToRefresh": "Nemoguće osvježavanje skinova.", 17 | "openmcskins.command.cleanupAlready": "Keš memorija se već čisti.", 18 | "openmcskins.command.cleanupSuccess": "Keš memorija je uspješno očiščena.", 19 | "openmcskins.command.cleanupFailed": "Nije moguće očistiti keš memoriju: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/sr_cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Konfigurisani hostovi:", 3 | "openmcskins.command.moved": "Uspešna promena sa hosta %d na %d.", 4 | "openmcskins.options.title": "OpenMCSkins postavke", 5 | "openmcskins.category.general": "Opšte", 6 | "openmcskins.option.cacheTextures": "Zapamti teksture", 7 | "openmcskins.option.verboseLogs": "Opširni logovi", 8 | "openmcskins.option.loaderTimeout": "Čekanje učitavača", 9 | "openmcskins.option.refreshOnConfigSave": "Osveži pri čuvanju konfiguracije", 10 | "openmcskins.option.uuidMode": "Osveži UUID", 11 | "openmcskins.option.uuidMode.never": "Nikad", 12 | "openmcskins.option.uuidMode.always": "Uvek", 13 | "openmcskins.option.uuidMode.offline": "Na offline serveru", 14 | "openmcskins.command.noPreset": "Nema pronađenih postavki.", 15 | "openmcskins.command.invalidFileFormat": "Neispravan unosni format fajla.", 16 | "openmcskins.command.unableToRefresh": "Nemoguće osvežavanje skinova.", 17 | "openmcskins.command.cleanupAlready": "Keš memorija se već čisti.", 18 | "openmcskins.command.cleanupSuccess": "Keš memorija je uspješno očiščena.", 19 | "openmcskins.command.cleanupFailed": "Nije moguće očistiti keš memoriju: %s" 20 | } -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/ElytraTextureFix.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.val; 5 | import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener; 6 | import net.minecraft.resource.ResourceManager; 7 | import net.minecraft.util.Identifier; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import javax.imageio.ImageIO; 11 | import java.io.IOException; 12 | 13 | @AllArgsConstructor 14 | public class ElytraTextureFix 15 | implements SimpleSynchronousResourceReloadListener { 16 | @Override 17 | public Identifier getFabricId() { 18 | return Identifier.of("skins", "elytra_texture_fix"); 19 | } 20 | 21 | @Override 22 | public void reload(@NotNull ResourceManager manager) { 23 | val elytraId = Identifier.of(Identifier.DEFAULT_NAMESPACE, "textures/entity/elytra.png"); 24 | try (val stream = manager.open(elytraId)) { 25 | val elytraImage = ImageIO.read(stream); 26 | SkinsClient.getCapeLayer().setElytraTexture(elytraImage); 27 | } catch (IOException e) { 28 | SkinsClient.getErrorHandler().accept(e); 29 | } 30 | 31 | SkinsClient.refresh(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_bo.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_cl.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_ec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_em.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_es.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_uy.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/es_ve.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.list": "Hosts configurados:", 3 | "openmcskins.command.moved": "Un host ha sido movido de %d a %d con éxito.", 4 | "openmcskins.options.title": "Opciones de OpenMCSkins", 5 | "openmcskins.category.general": "General", 6 | "openmcskins.option.cacheTextures": "Caché de texturas", 7 | "openmcskins.option.verboseLogs": "Registros Verbose", 8 | "openmcskins.option.loaderTimeout": "Tiempo de carga agotado", 9 | "openmcskins.option.refreshOnConfigSave": "Actualizar al guardar configuración", 10 | "openmcskins.option.uuidMode": "Actualizar UUID", 11 | "openmcskins.option.uuidMode.never": "Nunca", 12 | "openmcskins.option.uuidMode.always": "Siempre", 13 | "openmcskins.option.uuidMode.offline": "Cuando este sin conexion", 14 | "openmcskins.command.noPreset": "No hay presets disponibles.", 15 | "openmcskins.command.invalidFileFormat": "Formato de archivo de imagen invalido.", 16 | "openmcskins.command.unableToRefresh": "No se pueden actualizar las skins.", 17 | "openmcskins.command.cleanupAlready": "La caché ya se está borrando.", 18 | "openmcskins.command.cleanupSuccess": "La caché ha sido borrada correctamente.", 19 | "openmcskins.command.cleanupFailed": "Incapaz de borrar la caché: %s" 20 | } -------------------------------------------------------------------------------- /src/main/resources/assets/skins/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "openmcskins.command.added": "§a+ %s", 3 | "openmcskins.command.removed": "§c- %s", 4 | "openmcskins.command.list": "Configured hosts:", 5 | "openmcskins.command.moved": "A host was moved from %d to %d successfully.", 6 | "openmcskins.options.title": "OpenMCSkins options", 7 | "openmcskins.category.general": "General", 8 | "openmcskins.option.cacheTextures": "Texture caching", 9 | "openmcskins.option.verboseLogs": "Verbose logs", 10 | "openmcskins.option.loaderTimeout": "Loader timeout", 11 | "openmcskins.option.refreshOnConfigSave": "Refresh on config save", 12 | "openmcskins.option.uuidMode": "Refresh UUID", 13 | "openmcskins.option.uuidMode.never": "Never", 14 | "openmcskins.option.uuidMode.always": "Always", 15 | "openmcskins.option.uuidMode.offline": "When offline", 16 | "openmcskins.command.noPreset": "No preset found.", 17 | "openmcskins.command.listEntry": "%d§7. %s", 18 | "openmcskins.command.invalidFileFormat": "Invalid input file format.", 19 | "openmcskins.command.unableToRefresh": "Unable to refresh skins.", 20 | "openmcskins.command.cleanupAlready": "The cache is already clearing.", 21 | "openmcskins.command.cleanupSuccess": "Cache successfully cleared.", 22 | "openmcskins.command.cleanupFailed": "Unable to clear cache: %s" 23 | } -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "openmcskins", 4 | "version": "${version}", 5 | "name": "OpenMCSkins", 6 | "description": "This is an example description! Tell everyone what your mod is about!", 7 | "authors": [ 8 | "Zatrit156" 9 | ], 10 | "contact": { 11 | "sources": "https://github.com/zatrit/openmcskins2", 12 | "issues": "https://github.com/zatrit/openmcskins2/issues", 13 | "homepage": "https://github.com/zatrit/openmcskins2" 14 | }, 15 | "license": "Apache-2.0", 16 | "icon": "assets/skins/icon.png", 17 | "environment": "client", 18 | "accessWidener" : "skins.accesswidener", 19 | "entrypoints": { 20 | "client": [ 21 | "net.zatrit.skins.SkinsClient" 22 | ], 23 | "modmenu": [ 24 | "net.zatrit.skins.ModMenuIntegration" 25 | ] 26 | }, 27 | "mixins": [ 28 | { 29 | "environment": "client", 30 | "config": "skins.mixins.json" 31 | } 32 | ], 33 | "depends": { 34 | "yet_another_config_lib_v3": ">=3.2", 35 | "fabric-resource-loader-v0": "*", 36 | "fabric-command-api-v2": "*", 37 | "fabricloader": ">=0.14.11", 38 | "minecraft": ">=1.20.4", 39 | "java": ">=17" 40 | }, 41 | "suggests": { 42 | "modmenu": "*" 43 | }, 44 | "contributors": [ 45 | "Wayturns", 46 | "HeyItzCarlos", 47 | "hocbmyke", 48 | "GIFanimation", 49 | "Yellow_Cat" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/cache/AssetCache.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.cache; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.SneakyThrows; 5 | import lombok.val; 6 | import net.zatrit.skins.SkinsClient; 7 | import net.zatrit.skins.accessor.HasAssetPath; 8 | import net.zatrit.skins.lib.api.cache.Cache; 9 | 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.nio.file.StandardOpenOption; 13 | 14 | @AllArgsConstructor 15 | public class AssetCache implements Cache { 16 | private final HasAssetPath pathProvider; 17 | private final String type; 18 | 19 | @Override 20 | @SneakyThrows 21 | @SuppressWarnings("ResultOfMethodCallIgnored") 22 | public byte[] getOrLoad(String id, LoadFunction load) { 23 | val path = this.pathProvider.getAssetPath().resolve(type); 24 | val function = SkinsClient.getHashFunction(); 25 | val hash = function.hashUnencodedChars(id).toString(); 26 | 27 | val cacheFile = Paths.get(path.toString(), hash.substring(0, 2), hash); 28 | 29 | if (cacheFile.toFile().exists()) { 30 | return Files.readAllBytes(cacheFile); 31 | } else { 32 | val content = load.load(); 33 | cacheFile.getParent().toFile().mkdirs(); 34 | Files.write(cacheFile, content, StandardOpenOption.CREATE); 35 | return content; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/mixin/PlayerListEntryMixin.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.mixin; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import lombok.val; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.client.network.PlayerListEntry; 7 | import net.minecraft.client.util.DefaultSkinHelper; 8 | import net.minecraft.client.util.SkinTextures; 9 | import org.spongepowered.asm.mixin.Final; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Mutable; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Redirect; 15 | 16 | import java.util.function.Supplier; 17 | 18 | @Mixin(PlayerListEntry.class) 19 | public class PlayerListEntryMixin { 20 | @Shadow @Final private GameProfile profile; 21 | @Shadow @Final @Mutable private Supplier texturesSupplier; 22 | 23 | @Redirect( 24 | method = "", at = @At( 25 | value = "FIELD", 26 | target = "Lnet/minecraft/client/network/PlayerListEntry;texturesSupplier:Ljava/util/function/Supplier;")) 27 | private void customTexturesSupplier( 28 | PlayerListEntry instance, Supplier unused) { 29 | val provider = MinecraftClient.getInstance().getSkinProvider(); 30 | 31 | texturesSupplier = () -> provider.fetchSkinTextures(profile).getNow( 32 | DefaultSkinHelper.getSkinTextures(profile)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/config/HostEntry.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.config; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.val; 7 | import net.minecraft.text.MutableText; 8 | import net.minecraft.text.Text; 9 | import net.minecraft.util.Formatting; 10 | import net.zatrit.skins.util.command.TextUtil; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Getter 17 | @AllArgsConstructor 18 | @RequiredArgsConstructor 19 | public class HostEntry implements TextUtil.ToText { 20 | private final HostType type; 21 | private Map properties; 22 | 23 | @Override 24 | public void toText(@NotNull MutableText text) { 25 | val map = new HashMap(2, 1); 26 | 27 | map.put("type", this.getType()); 28 | map.put("properties", this.getProperties()); 29 | 30 | text.append(Text.literal("HostEntry") 31 | .styled(style -> style.withFormatting(Formatting.RESET))); 32 | 33 | TextUtil.mapToText(text, map); 34 | } 35 | 36 | public enum HostType { 37 | DIRECT, 38 | FALLBACK, 39 | FIVEZIG, 40 | GEYSER, 41 | LIQUID_BOUNCE, 42 | LOCAL, 43 | METEOR, 44 | MINECRAFT_CAPES, 45 | MOJANG, 46 | NAMED_HTTP, 47 | OPTIFINE, 48 | VALHALLA, 49 | WURST, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/command/CommandUtil.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util.command; 2 | 3 | import com.mojang.brigadier.arguments.ArgumentType; 4 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 5 | import com.mojang.brigadier.builder.RequiredArgumentBuilder; 6 | import com.mojang.brigadier.context.CommandContext; 7 | import lombok.experimental.UtilityClass; 8 | import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; 9 | import net.minecraft.command.argument.ArgumentTypes; 10 | import net.minecraft.text.Text; 11 | import org.jetbrains.annotations.Contract; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | @UtilityClass 15 | public class CommandUtil { 16 | @Contract(value = "_ -> new", pure = true) 17 | public static @NotNull LiteralArgumentBuilder literal( 18 | final String name) { 19 | return LiteralArgumentBuilder.literal(name) 20 | .executes(CommandUtil::noArguments); 21 | } 22 | 23 | @Contract(value = "_, _ -> new", pure = true) 24 | public static @NotNull RequiredArgumentBuilder argument( 25 | final String name, final ArgumentType type) { 26 | return RequiredArgumentBuilder.argument( 27 | name, 28 | type 29 | ).executes(CommandUtil::noArguments); 30 | } 31 | 32 | public static int noArguments( 33 | @NotNull CommandContext context) { 34 | context.getSource().sendError(Text.translatable( 35 | "command.unknown.argument")); 36 | return -1; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/command/IndexedResourceProvider.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Cleanup; 5 | import lombok.SneakyThrows; 6 | import lombok.val; 7 | import org.apache.commons.io.IOUtils; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.io.IOException; 11 | import java.net.URL; 12 | import java.nio.charset.Charset; 13 | import java.nio.file.Path; 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | import java.util.Optional; 17 | import java.util.function.Function; 18 | 19 | /** 20 | * A FileProvider implementation that reads a specific resource 21 | * file with the .index extension and uses it to get a list of 22 | * files in a specific resource folder (e.g. presets). 23 | * 24 | * @see FileProvider 25 | */ 26 | @AllArgsConstructor 27 | public class IndexedResourceProvider implements FileProvider { 28 | private final String path; 29 | private final ClassLoader classLoader; 30 | 31 | @Override 32 | public Collection listFiles() throws IOException { 33 | val url = classLoader.getResource(path + ".index"); 34 | 35 | if (url != null) { 36 | @Cleanup val indexFile = url.openStream(); 37 | return IOUtils.readLines(indexFile, Charset.defaultCharset()); 38 | } 39 | 40 | return Collections.emptyList(); 41 | } 42 | 43 | @Override 44 | @SneakyThrows 45 | public @NotNull Optional getFile(String name) { 46 | val url = classLoader.getResource(this.path + "/" + name); 47 | return Optional.ofNullable(url).map(new Function() { 48 | @Override 49 | @SneakyThrows 50 | public Path apply(URL u) { 51 | return Path.of(u.toURI()); 52 | } 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/mixin/GameProfileMixin.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.mixin; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import com.mojang.authlib.properties.PropertyMap; 5 | import lombok.Cleanup; 6 | import lombok.SneakyThrows; 7 | import lombok.val; 8 | import net.zatrit.skins.SkinsClient; 9 | import net.zatrit.skins.accessor.AsyncUUIDRefresher; 10 | import net.zatrit.skins.lib.api.Profile; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | 14 | import java.io.InputStreamReader; 15 | import java.net.URL; 16 | import java.util.Map; 17 | import java.util.UUID; 18 | import java.util.concurrent.CompletableFuture; 19 | import java.util.function.Supplier; 20 | 21 | @Mixin(value = GameProfile.class, remap = false) 22 | public abstract class GameProfileMixin implements Profile, AsyncUUIDRefresher { 23 | @Shadow 24 | public abstract PropertyMap getProperties(); 25 | 26 | @Override 27 | public CompletableFuture skins$refreshUuid() { 28 | return CompletableFuture.supplyAsync(new Supplier() { 29 | @Override 30 | @SneakyThrows 31 | public Profile get() { 32 | val url = new URL( 33 | "https://api.mojang.com/users/profiles/minecraft/" + 34 | GameProfileMixin.this.getName()); 35 | 36 | @Cleanup val reader = new InputStreamReader(url.openStream()); 37 | val map = SkinsClient.getSkinlibConfig().getGson().fromJson( 38 | reader, 39 | Map.class 40 | ); 41 | 42 | val id = String.valueOf(map.get("id")).replaceAll( 43 | "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", 44 | "$1-$2-$3-$4-$5" 45 | ); 46 | 47 | val profile = new GameProfile( 48 | UUID.fromString(id), 49 | GameProfileMixin.this.getName() 50 | ); 51 | profile.getProperties() 52 | .putAll(GameProfileMixin.this.getProperties()); 53 | 54 | return (Profile) profile; 55 | } 56 | }); 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # History of OpenMCSkins 2 | 3 | Most skin mods allow you to load skins from multiple hosts, but they either don't allow you to change the loading order, 4 | or it is customized by editing the configuration file through a text editor. 5 | 6 | [I](https://github.com/zatrit/), as an OpenMCSkins developer, was not happy with this. So I wanted to write my own mod 7 | with the ability to fine-tune it. This is how OpenMCSkins 1.0 was created, but at the time I didn't know Java very well, 8 | which led to [numerous bugs](https://github.com/zatrit/openmcskins/issues) that I couldn't fix. In the spring of 2023, I 9 | decided to rewrite it from scratch, which is how OpenMCSkins 2.0 was created. 10 | 11 | OpenMCSkins now has fewer bugs, better performance, and community support 12 | via [Discord](https://discord.gg/P4SX2uEspy) and [Crowdin](https://crowdin.com/project/openmcskins). 13 | 14 | # How this mod loads skins 15 | 16 | ## Download order 17 | 18 | The mod stores all hosts as a numbered list. When it needs to load a player's skin, it asks ALL hosts for their skins in 19 | multiple [parallel threads](https://en.wikipedia.org/wiki/Parallel_computing) and then goes through all the responses 20 | from top to bottom (in ascending ID order) and displays the first skin found. 21 | 22 | ![Loading Order](img/loadingOrder.png) 23 | 24 | *(this image shows a configuration where the mod will first look for a MinecraftCapes cloak, and if it doesn't find one, 25 | it will use the vanilla one)*. 26 | 27 | ## [Presets](presets.md) 28 | 29 | Often different hosts can use the same logic, differing only in the URL. To avoid hardcoding every such host in the mod, 30 | the mod has the option of using built-in presets and creating your own. 31 | Each preset contains the type of API it uses and some additional fields for more flexible customization. 32 | 33 | ## [Commands](commands.md) 34 | 35 | The mod has a handy way to control its behavior - commands. 36 | With them, you can easily change the host list during the game and refresh already loaded player skins during the game. 37 | 38 | ## GUI options 39 | 40 | For versions 1.16-1.18 the mod uses [Cloth Config](https://modrinth.com/mod/cloth-config) to create the options screen. 41 | 42 | And for versions from 1.19 onwards it uses [YACL](https://modrinth.com/mod/yacl). 43 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/texture/TextureLoader.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.texture; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import lombok.Cleanup; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.val; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.texture.AbstractTexture; 9 | import net.minecraft.client.texture.NativeImage; 10 | import net.minecraft.client.texture.NativeImageBackedTexture; 11 | import net.minecraft.util.Identifier; 12 | import net.zatrit.skins.lib.TextureType; 13 | import net.zatrit.skins.lib.api.Texture; 14 | import org.apache.commons.lang3.NotImplementedException; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.IOException; 19 | import java.util.function.Consumer; 20 | 21 | @RequiredArgsConstructor 22 | public class TextureLoader { 23 | private final Texture texture; 24 | private boolean animated = false; 25 | 26 | public static @NotNull TextureLoader create(@NotNull Texture texture) { 27 | val metadata = texture.getMetadata(); 28 | val config = new TextureLoader(texture); 29 | 30 | if (metadata == null) { 31 | return config; 32 | } 33 | 34 | config.animated = metadata.isAnimated(); 35 | 36 | return config; 37 | } 38 | 39 | public void getTexture( 40 | @NotNull TextureIdentifier identifier, 41 | @NotNull Consumer callback) throws IOException { 42 | val id = identifier.asId(); 43 | 44 | @Cleanup val stream = new ByteArrayInputStream(this.texture.getBytes()); 45 | val image = NativeImage.read(stream); 46 | val manager = MinecraftClient.getInstance().getTextureManager(); 47 | 48 | AbstractTexture texture; 49 | if (this.animated && identifier.getType() == TextureType.CAPE) { 50 | texture = new AnimatedTexture(image, 100); 51 | } else if (this.animated) { 52 | throw new NotImplementedException( 53 | "Only animated capes are supported."); 54 | } else { 55 | texture = new NativeImageBackedTexture(image); 56 | } 57 | 58 | RenderSystem.recordRenderCall(() -> { 59 | manager.registerTexture(id, texture); 60 | callback.accept(id); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/FallbackResolver.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins; 2 | 3 | import com.google.common.base.Verify; 4 | import com.google.common.base.VerifyException; 5 | import com.mojang.authlib.GameProfile; 6 | import com.mojang.authlib.minecraft.MinecraftProfileTexture; 7 | import com.mojang.authlib.minecraft.MinecraftSessionService; 8 | import lombok.AllArgsConstructor; 9 | import lombok.val; 10 | import net.zatrit.skins.lib.CachedPlayerTextures; 11 | import net.zatrit.skins.lib.Config; 12 | import net.zatrit.skins.lib.TextureType; 13 | import net.zatrit.skins.lib.api.PlayerTextures; 14 | import net.zatrit.skins.lib.api.Profile; 15 | import net.zatrit.skins.lib.api.Resolver; 16 | import net.zatrit.skins.lib.data.Metadata; 17 | import net.zatrit.skins.lib.texture.URLTexture; 18 | import net.zatrit.skins.util.TextureTypeUtil; 19 | import org.jetbrains.annotations.NotNull; 20 | 21 | import java.util.EnumMap; 22 | import java.util.Optional; 23 | 24 | @AllArgsConstructor 25 | public class FallbackResolver implements Resolver { 26 | private final Config config; 27 | private final MinecraftSessionService sessionService; 28 | 29 | @Override 30 | public @NotNull PlayerTextures resolve(Profile profile) 31 | throws VerifyException { 32 | Verify.verify(profile instanceof GameProfile); 33 | 34 | val gameProfile = (GameProfile) profile; 35 | val textures = this.sessionService.getTextures(gameProfile); 36 | val newTextures = new EnumMap(TextureType.class); 37 | 38 | for (val type : MinecraftProfileTexture.Type.values()) { 39 | val texture = switch (type) { 40 | case SKIN -> textures.skin(); 41 | case CAPE -> textures.cape(); 42 | default -> null; 43 | }; 44 | 45 | if (texture == null) { 46 | continue; 47 | } 48 | 49 | val metadata = new Metadata( 50 | Boolean.parseBoolean(texture.getMetadata("animated")), 51 | texture.getMetadata("model") 52 | ); 53 | 54 | newTextures.put( 55 | TextureTypeUtil.fromAuthlibType(type), 56 | new URLTexture(texture.getUrl(), metadata) 57 | ); 58 | } 59 | 60 | return new CachedPlayerTextures<>( 61 | newTextures, 62 | config.getLayers(), 63 | config.getCacheProvider() 64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/texture/AnimatedTexture.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.texture; 2 | 3 | import com.mojang.blaze3d.platform.GlStateManager; 4 | import com.mojang.blaze3d.platform.TextureUtil; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.val; 7 | import net.minecraft.client.texture.AbstractTexture; 8 | import net.minecraft.client.texture.NativeImage; 9 | import net.minecraft.resource.ResourceManager; 10 | import org.lwjgl.opengl.GL11; 11 | 12 | /** 13 | * An optimized version of OpenMCSkins 14 | * AnimatedTexture. 15 | */ 16 | @RequiredArgsConstructor 17 | public class AnimatedTexture extends AbstractTexture { 18 | private final long firstFrameTime = System.currentTimeMillis(); 19 | private final NativeImage image; 20 | private final int frameTime; 21 | private int[] ids; 22 | private int framesCount; 23 | 24 | /* 25 | All of these functions are executed in the rendering thread, 26 | so any thread checks are unnecessary. 27 | */ 28 | 29 | @Override 30 | public void load(ResourceManager manager) { 31 | val frameHeight = this.image.getWidth() / 2; 32 | 33 | this.framesCount = this.image.getHeight() / frameHeight; 34 | this.ids = new int[this.framesCount]; 35 | 36 | // Generates a buffer with frame IDs. 37 | GL11.glGenTextures(this.ids); 38 | 39 | for (int i = 0; i < this.framesCount; i++) { 40 | TextureUtil.prepareImage( 41 | this.ids[i], 42 | this.image.getWidth(), 43 | frameHeight 44 | ); 45 | 46 | this.image.upload( 47 | 0, 48 | 0, 49 | 0, 50 | 0, 51 | frameHeight * i, 52 | this.image.getWidth(), 53 | frameHeight, 54 | false, 55 | false 56 | ); 57 | } 58 | } 59 | 60 | @Override 61 | public void bindTexture() { 62 | val time = System.currentTimeMillis(); 63 | val deltaTime = time - this.firstFrameTime; 64 | val frameIndex = (int) (deltaTime / this.frameTime) % this.framesCount; 65 | 66 | this.glId = this.ids[frameIndex]; 67 | 68 | super.bindTexture(); 69 | } 70 | 71 | @Override 72 | public void close() { 73 | this.image.close(); 74 | } 75 | 76 | @Override 77 | public void clearGlId() { 78 | GlStateManager._deleteTextures(this.ids); 79 | 80 | for (int id : this.ids) { 81 | TextureUtil.releaseTextureId(id); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /docs/presets.md: -------------------------------------------------------------------------------- 1 | # Presets 2 | 3 | In OpenMCSkins, a preset is a small [TOML file](https://toml.io/) containing a description of the host so that the mod 4 | can use it to add a host. 5 | 6 | Example preset file: 7 | 8 | ```toml 9 | type = 'GEYSER' 10 | 11 | [properties] 12 | floodgate_prefix = '.' 13 | ``` 14 | 15 | As you can see, there are two main fields here: ``type`` and ``properties``. ``properties`` contains additional 16 | information, such as the host API URL or the GeyserMC prefix as in the example. 17 | Each host type has its own set of properties required for its operation. 18 | 19 | ## Here is a list of supported host types: 20 | 21 | *(if the host does not have ``properties`` explicitly specified, then it does not have them)* 22 | 23 | ### ``GEYSER`` 24 | 25 | GeyserMC implementation [Global API](https://wiki.geysermc.org/geyser/global-api/). 26 | 27 | Properties: 28 | 29 | * ``floodgate_prefix`` (optional) - a prefix before the player's name, indicating that the player plays through 30 | GeyserMC. 31 | 32 | ### ``FALLBACK`` 33 | 34 | Uses vanilla skin implementation and allows NPC skins to work correctly, but only works on online-mode servers. 35 | 36 | ### ``FIVEZIG`` 37 | 38 | Implementation of the 5zig capes API. 39 | 40 | ### ``MOJANG`` 41 | 42 | Implementation of [Mojang skin system API](https://wiki.vg/Mojang_API). 43 | 44 | ### ``MINECRAFT_CAPES`` 45 | 46 | Implementation of the MinecraftCapes API. 47 | 48 | ### ``NAMED_HTTP`` 49 | 50 | Implementation of the API described [here](httpServer.md). 51 | 52 | Properties: 53 | 54 | * ``base_url`` - the base URL to which the player's name will be appended (must end with ``/``). 55 | 56 | ### ``OPTIFINE`` 57 | 58 | Implementation of [Optifine capes API](https://optifine.readthedocs.io/capes.html). 59 | 60 | Properties: 61 | 62 | * ``base_url`` - the base URL to which the player's name will be appended (must end with ``/``). 63 | 64 | ### ``VALHALLA`` 65 | 66 | Implementation of [Valhalla Skins API](https://skins.minelittlepony-mod.com/docs) 67 | 68 | Properties: 69 | 70 | * ``base_url`` - the base URL to which the player's name will be appended (must end with ``/``). 71 | 72 | ### ``DIRECT`` 73 | 74 | Loads the skin directly from the URL, where fields such as ``{id}``, ``{shortId}``, ``{name}`` and ``{type}`` are 75 | substituted. 76 | 77 | Properties: 78 | 79 | * ``types`` - types of skins for which the substitution will be performed. Example: ``['CAPE', 'SKIN', 'EARS']``. 80 | * ``base_url`` - base URL where the substitution will take place. Example: ``https://dl.labymod.net/capes/{id}`` 81 | 82 | ### ``LOCAL`` 83 | 84 | Searches for a skin in the computer's file system, starting from the given path. 85 | 86 | Properties: 87 | 88 | * ``directory`` - path to the directory with skins, must have the structure described [here](localResolverHierarchy.md) 89 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/config/TomlConfigSerializer.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.config; 2 | 3 | import com.moandjiezana.toml.Toml; 4 | import com.moandjiezana.toml.TomlWriter; 5 | import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; 6 | import dev.isxander.yacl3.config.v2.api.ConfigField; 7 | import dev.isxander.yacl3.config.v2.api.ConfigSerializer; 8 | import dev.isxander.yacl3.config.v2.api.FieldAccess; 9 | import lombok.Cleanup; 10 | import lombok.Getter; 11 | import lombok.SneakyThrows; 12 | import lombok.val; 13 | import net.zatrit.skins.SkinsClient; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import java.io.IOException; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.util.ArrayList; 20 | import java.util.Collection; 21 | import java.util.Map; 22 | import java.util.function.Consumer; 23 | 24 | /** 25 | * TOML implementation for {@link ConfigSerializer}. 26 | */ 27 | public class TomlConfigSerializer extends ConfigSerializer { 28 | private final Collection> listeners = new ArrayList<>(); 29 | private final @Getter Path file; 30 | 31 | public TomlConfigSerializer(Path file, ConfigClassHandler config) { 32 | super(config); 33 | this.file = file; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | *

39 | * This implementation saves a config of 40 | * type T to a file stored in {@link #file}. 41 | */ 42 | @Override 43 | public void save() { 44 | val tomlWriter = new TomlWriter(); 45 | val config = this.config.instance(); 46 | 47 | try { 48 | @Cleanup val stream = Files.newOutputStream(file); 49 | tomlWriter.write(config, stream); 50 | } catch (IOException e) { 51 | SkinsClient.getErrorHandler().accept(e); 52 | } 53 | 54 | for (val listener : this.listeners) { 55 | listener.accept(config); 56 | } 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | @SneakyThrows 61 | @Override 62 | public LoadResult loadSafely( 63 | @NotNull Map, FieldAccess> bufferAccessMap) { 64 | @Cleanup val stream = Files.newInputStream(file); 65 | val configClass = this.config.configClass(); 66 | val instance = new Toml().read(stream).to(configClass); 67 | 68 | for (val access : bufferAccessMap.values()) { 69 | val field = configClass.getDeclaredField(access.name()); 70 | val fieldAccess = ((FieldAccess) access); 71 | field.setAccessible(true); 72 | 73 | fieldAccess.set(field.get(instance)); 74 | } 75 | 76 | return LoadResult.SUCCESS; 77 | } 78 | 79 | public void addSaveListener(Consumer listener) { 80 | this.listeners.add(listener); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/command/FileArgumentType.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util.command; 2 | 3 | import com.mojang.brigadier.StringReader; 4 | import com.mojang.brigadier.arguments.ArgumentType; 5 | import com.mojang.brigadier.context.CommandContext; 6 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 7 | import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; 8 | import com.mojang.brigadier.suggestion.Suggestions; 9 | import com.mojang.brigadier.suggestion.SuggestionsBuilder; 10 | import lombok.AllArgsConstructor; 11 | import lombok.Getter; 12 | import lombok.val; 13 | import net.minecraft.text.Text; 14 | import net.zatrit.skins.SkinsClient; 15 | import org.apache.commons.io.FilenameUtils; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.io.IOException; 19 | import java.nio.file.Path; 20 | import java.util.Arrays; 21 | import java.util.Collection; 22 | import java.util.HashSet; 23 | import java.util.Optional; 24 | import java.util.concurrent.CompletableFuture; 25 | 26 | /** 27 | * An argument type that allows access to 28 | * mod resources and files in the mod folder. 29 | * 30 | * @see FileProvider 31 | * @see IndexedResourceProvider 32 | * @see DirectoryFileProvider 33 | */ 34 | @AllArgsConstructor 35 | public class FileArgumentType implements ArgumentType { 36 | private static final SimpleCommandExceptionType NO_PRESET_EXCEPTION = new SimpleCommandExceptionType( 37 | Text.translatable("openmcskins.command.noPreset")); 38 | private final FileProvider[] providers; 39 | private final @Getter String extension; 40 | private final Collection files = new HashSet<>(); 41 | 42 | @Override 43 | public Path parse(@NotNull StringReader reader) 44 | throws CommandSyntaxException { 45 | if (reader.canRead() && reader.peek() != ' ') { 46 | val file = reader.readString() + "." + this.extension; 47 | val path = Arrays.stream(this.providers).map(p -> p.getFile(file)) 48 | .filter(Optional::isPresent).map(Optional::get).findFirst(); 49 | 50 | if (path.isPresent()) { 51 | return path.get(); 52 | } 53 | } 54 | 55 | throw NO_PRESET_EXCEPTION.create(); 56 | } 57 | 58 | @Override 59 | public CompletableFuture listSuggestions( 60 | CommandContext context, @NotNull SuggestionsBuilder builder) { 61 | for (String file : this.files) { 62 | builder.suggest(file); 63 | } 64 | 65 | return builder.buildFuture(); 66 | } 67 | 68 | @Override 69 | public Collection getExamples() { 70 | return this.files; 71 | } 72 | 73 | /** 74 | * Searches for all available files 75 | * and stores them in {@link #files} 76 | */ 77 | public void refresh() { 78 | this.files.clear(); 79 | 80 | val set = new HashSet(); 81 | 82 | for (val provider : this.providers) { 83 | try { 84 | set.addAll(provider.listFiles()); 85 | } catch (IOException e) { 86 | SkinsClient.getErrorHandler().accept(e); 87 | } 88 | } 89 | 90 | for (val name : set) { 91 | if (FilenameUtils.isExtension(name, this.extension)) { 92 | this.files.add(FilenameUtils.removeExtension(name)); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/config/Resolvers.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.config; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import lombok.val; 5 | import net.fabricmc.loader.api.FabricLoader; 6 | import net.minecraft.client.MinecraftClient; 7 | import net.zatrit.skins.FallbackResolver; 8 | import net.zatrit.skins.SkinsClient; 9 | import net.zatrit.skins.lib.TextureType; 10 | import net.zatrit.skins.lib.api.Resolver; 11 | import net.zatrit.skins.lib.resolver.*; 12 | import net.zatrit.skins.lib.resolver.capes.*; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import java.nio.file.Path; 17 | import java.util.Collections; 18 | import java.util.List; 19 | 20 | import static net.andreinc.aleph.AlephFormatter.str; 21 | 22 | @UtilityClass 23 | public class Resolvers { 24 | @SuppressWarnings("unchecked") 25 | public static @Nullable Resolver resolverFromEntry( 26 | @NotNull HostEntry entry) { 27 | val props = entry.getProperties(); 28 | val config = SkinsClient.getSkinlibConfig(); 29 | 30 | try { 31 | return switch (entry.getType()) { 32 | case GEYSER -> { 33 | var floodgatePrefix = Collections.singletonList("."); 34 | if (props != null) { 35 | val value = props.get("floodgate_prefix"); 36 | 37 | if (value instanceof List prefixes) { 38 | floodgatePrefix = (List) prefixes; 39 | } else if (value instanceof String prefix) { 40 | floodgatePrefix = Collections.singletonList(prefix); 41 | } 42 | } 43 | 44 | yield new GeyserResolver(config, floodgatePrefix); 45 | } 46 | case FALLBACK -> new FallbackResolver( 47 | config, 48 | MinecraftClient.getInstance() 49 | .getSessionService() 50 | ); 51 | case FIVEZIG -> new FiveZigResolver(config); 52 | case LIQUID_BOUNCE -> new LiquidBounceResolver(config); 53 | case METEOR -> new MeteorResolver(config); 54 | case MOJANG -> new MojangResolver(config); 55 | case MINECRAFT_CAPES -> new MinecraftCapesResolver(config); 56 | case NAMED_HTTP, OPTIFINE, VALHALLA, DIRECT -> { 57 | val baseUrl = (String) props.get("base_url"); 58 | 59 | yield switch (entry.getType()) { 60 | case OPTIFINE -> new OptifineResolver(config, baseUrl); 61 | case VALHALLA -> new ValhallaResolver(config, baseUrl); 62 | case NAMED_HTTP -> new NamedHTTPResolver( 63 | config, 64 | baseUrl 65 | ); 66 | case DIRECT -> { 67 | val types = ((List) props.get("types")).stream() 68 | .map(TextureType::valueOf).toList(); 69 | yield new DirectResolver(config, baseUrl, types); 70 | } 71 | default -> null; 72 | }; 73 | } 74 | case LOCAL -> { 75 | val directoryPattern = (String) props.get("directory"); 76 | 77 | val instance = FabricLoader.getInstance(); 78 | val directory = Path.of(str(directoryPattern).arg( 79 | "configDir", 80 | instance.getConfigDir() 81 | ).arg( 82 | "gameDir", 83 | instance.getGameDir() 84 | ).fmt()); 85 | 86 | yield new LocalResolver(config, directory); 87 | } 88 | case WURST -> new WurstResolver(config); 89 | }; 90 | } catch (Exception ex) { 91 | SkinsClient.getErrorHandler().accept(ex); 92 | return null; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/util/command/TextUtil.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.util.command; 2 | 3 | import com.google.common.collect.Lists; 4 | import lombok.experimental.UtilityClass; 5 | import lombok.val; 6 | import net.minecraft.text.ClickEvent; 7 | import net.minecraft.text.MutableText; 8 | import net.minecraft.text.Text; 9 | import net.minecraft.util.Formatting; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.net.MalformedURLException; 13 | import java.net.URISyntaxException; 14 | import java.net.URL; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | import java.util.function.UnaryOperator; 20 | 21 | /** 22 | * Various utility functions for interacting with Minecraft text APIs. 23 | * 24 | * @see MutableText 25 | * @see Text 26 | * @see ToText 27 | */ 28 | @UtilityClass 29 | public class TextUtil { 30 | private static final List SKIP_ELEMENTS = Lists.newArrayList( 31 | null, 32 | Collections.emptyMap(), 33 | Collections.emptyList() 34 | ); 35 | 36 | /** 37 | * Converts {@link Map} into a nicely formatted {@link Text}. 38 | */ 39 | @SuppressWarnings("unchecked") 40 | public static void mapToText( 41 | @NotNull MutableText text, @NotNull Map map) { 42 | val specialStyle = (UnaryOperator) style -> style.withFormatting( 43 | Formatting.GRAY); 44 | 45 | text.append(Text.literal("{").styled(specialStyle)); 46 | 47 | boolean first = true; 48 | for (val entry : map.entrySet()) { 49 | val value = entry.getValue(); 50 | 51 | if (SKIP_ELEMENTS.stream().anyMatch(e -> Objects.equals(e, value))) { 52 | continue; 53 | } 54 | 55 | if (!first) { 56 | text.append(Text.literal(", ").styled(specialStyle)); 57 | } 58 | first = false; 59 | 60 | text.append(Text.literal(entry.getKey()) 61 | .styled(style -> style.withFormatting(Formatting.RESET))); 62 | text.append(Text.literal(" = ").styled(specialStyle)); 63 | 64 | if (value instanceof Number numberValue) { 65 | text.append(formatNumber(numberValue)); 66 | } else if (value instanceof Map) { 67 | mapToText(text, (Map) value); 68 | } else if (value instanceof ToText textValue) { 69 | textValue.toText(text); 70 | } else { 71 | val stringValue = value.toString(); 72 | var mutableText = Text.literal(stringValue) 73 | .formatted(Formatting.GREEN); 74 | 75 | if (isURL(stringValue)) { 76 | val clickAction = new ClickEvent( 77 | ClickEvent.Action.OPEN_URL, 78 | stringValue 79 | ); 80 | mutableText = mutableText.styled(s -> s.withFormatting( 81 | Formatting.UNDERLINE).withClickEvent(clickAction)); 82 | } 83 | 84 | text.append(mutableText); 85 | } 86 | } 87 | 88 | text.append(Text.literal("}").styled(specialStyle)); 89 | } 90 | 91 | /** 92 | * Converts int to text and colors it with {@link Formatting#GREEN}. 93 | * 94 | * @see Formatting 95 | */ 96 | public static MutableText formatNumber(Number n) { 97 | return Text.literal(String.valueOf(n)).formatted(Formatting.GREEN); 98 | } 99 | 100 | /** 101 | * @return true if the {@code string} is a URL. 102 | */ 103 | // https://stackoverflow.com/a/41268655/12245612 104 | private static boolean isURL(String string) { 105 | try { 106 | var url = new URL(string); 107 | url.toURI(); 108 | return true; 109 | } catch (MalformedURLException | URISyntaxException e) { 110 | return false; 111 | } 112 | } 113 | 114 | public interface ToText { 115 | void toText(@NotNull MutableText text); 116 | 117 | default Text toText() { 118 | val text = Text.empty(); 119 | this.toText(text); 120 | 121 | return text; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/SkinsClient.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins; 2 | 3 | import com.google.common.hash.HashFunction; 4 | import com.google.common.hash.Hashing; 5 | import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; 6 | import lombok.Getter; 7 | import lombok.val; 8 | import net.fabricmc.api.ClientModInitializer; 9 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; 10 | import net.fabricmc.fabric.api.resource.ResourceManagerHelper; 11 | import net.fabricmc.loader.api.FabricLoader; 12 | import net.minecraft.client.MinecraftClient; 13 | import net.minecraft.resource.ResourceType; 14 | import net.minecraft.util.Identifier; 15 | import net.zatrit.skins.accessor.HasAssetPath; 16 | import net.zatrit.skins.accessor.Refreshable; 17 | import net.zatrit.skins.cache.AssetCacheProvider; 18 | import net.zatrit.skins.config.Resolvers; 19 | import net.zatrit.skins.config.SkinsConfig; 20 | import net.zatrit.skins.config.TomlConfigSerializer; 21 | import net.zatrit.skins.lib.Config; 22 | import net.zatrit.skins.lib.TextureDispatcher; 23 | import net.zatrit.skins.lib.api.Resolver; 24 | import net.zatrit.skins.lib.layer.awt.LegacySkinLayer; 25 | import net.zatrit.skins.lib.layer.awt.ScaleCapeLayer; 26 | import net.zatrit.skins.util.ExceptionConsumer; 27 | import net.zatrit.skins.util.ExceptionConsumerImpl; 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | import java.net.http.HttpClient; 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | 34 | public final class SkinsClient implements ClientModInitializer { 35 | private static final @Getter List resolvers = new ArrayList<>(); 36 | private static final @Getter HashFunction hashFunction = Hashing.murmur3_128(); 37 | private static final @Getter ScaleCapeLayer capeLayer = new ScaleCapeLayer(); 38 | private static @Getter ConfigClassHandler configHandler; 39 | private static @Getter Config skinlibConfig; 40 | private static @Getter TextureDispatcher dispatcher; 41 | private static @Getter HttpClient httpClient; 42 | private static @Getter ExceptionConsumer errorHandler = new ExceptionConsumerImpl( 43 | true); 44 | 45 | public static boolean refresh() { 46 | for (Resolver resolver : getResolvers()) { 47 | resolver.refresh(); 48 | } 49 | 50 | val provider = MinecraftClient.getInstance().getSkinProvider(); 51 | 52 | if (provider instanceof Refreshable refreshable) { 53 | refreshable.skins$refresh(); 54 | return true; 55 | } 56 | 57 | return false; 58 | } 59 | 60 | private void applyConfig(@NotNull SkinsConfig config) { 61 | val path = (HasAssetPath) MinecraftClient.getInstance(); 62 | 63 | errorHandler = new ExceptionConsumerImpl(config.isVerboseLogs()); 64 | 65 | resolvers.clear(); 66 | for (val hostEntry : config.getHosts()) { 67 | val resolver = Resolvers.resolverFromEntry(hostEntry); 68 | if (resolver != null) { 69 | resolvers.add(resolver); 70 | } 71 | } 72 | 73 | skinlibConfig.setCacheProvider(config.isCacheTextures() ? 74 | new AssetCacheProvider(path) : 75 | null); 76 | 77 | if (config.isRefreshOnConfigSave()) { 78 | refresh(); 79 | } 80 | } 81 | 82 | @Override 83 | public void onInitializeClient() { 84 | skinlibConfig = new Config(); 85 | dispatcher = new TextureDispatcher(skinlibConfig); 86 | 87 | skinlibConfig.setLayers(List.of(capeLayer, new LegacySkinLayer())); 88 | 89 | configHandler = ConfigClassHandler.createBuilder(SkinsConfig.class) 90 | .id(Identifier.of("openmcskins", "config")) 91 | .serializer(handler1 -> { 92 | val serializer = new TomlConfigSerializer<>( 93 | FabricLoader.getInstance() 94 | .getConfigDir() 95 | .resolve( 96 | "openmcskins.toml"), 97 | handler1 98 | ); 99 | serializer.addSaveListener(this::applyConfig); 100 | return serializer; 101 | }).build(); 102 | configHandler.load(); 103 | 104 | ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES) 105 | .registerReloadListener(new ElytraTextureFix()); 106 | 107 | this.applyConfig(configHandler.instance()); 108 | 109 | val commands = new SkinsCommands( 110 | configHandler, 111 | (HasAssetPath) MinecraftClient.getInstance() 112 | ); 113 | 114 | ClientCommandRegistrationCallback.EVENT.register(commands); 115 | 116 | httpClient = HttpClient.newBuilder() 117 | .executor(skinlibConfig.getExecutor()).build(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/ModMenuIntegration.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins; 2 | 3 | import com.terraformersmc.modmenu.api.ConfigScreenFactory; 4 | import com.terraformersmc.modmenu.api.ModMenuApi; 5 | import dev.isxander.yacl3.api.ConfigCategory; 6 | import dev.isxander.yacl3.api.Option; 7 | import dev.isxander.yacl3.api.YetAnotherConfigLib; 8 | import dev.isxander.yacl3.api.controller.BooleanControllerBuilder; 9 | import dev.isxander.yacl3.api.controller.DoubleSliderControllerBuilder; 10 | import dev.isxander.yacl3.api.controller.EnumControllerBuilder; 11 | import lombok.val; 12 | import net.minecraft.text.Text; 13 | import net.zatrit.skins.config.FilterMode; 14 | import net.zatrit.skins.config.SkinsConfig; 15 | import net.zatrit.skins.config.UuidMode; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import static net.minecraft.text.Text.translatable; 19 | 20 | public class ModMenuIntegration implements ModMenuApi { 21 | @Override 22 | public ConfigScreenFactory getModConfigScreenFactory() { 23 | return parent -> { 24 | val instance = SkinsClient.getConfigHandler(); 25 | 26 | return YetAnotherConfigLib.create(instance, this::initConfig) 27 | .generateScreen(parent); 28 | }; 29 | } 30 | 31 | /** 32 | * Initializes OpenMCSkins config for ConfigInstance. 33 | */ 34 | private YetAnotherConfigLib.Builder initConfig( 35 | @NotNull SkinsConfig defaults, 36 | @NotNull SkinsConfig config, 37 | YetAnotherConfigLib.@NotNull Builder builder) { 38 | return builder.title(translatable("openmcskins.options.title")).category( 39 | initializeGeneralCategory( 40 | defaults, 41 | config, 42 | ConfigCategory.createBuilder() 43 | ).build()); 44 | } 45 | 46 | /** 47 | * Adds general category options. 48 | */ 49 | private ConfigCategory.Builder initializeGeneralCategory( 50 | @NotNull SkinsConfig defaults, 51 | @NotNull SkinsConfig config, 52 | ConfigCategory.@NotNull Builder category) { 53 | return category.name(translatable("openmcskins.category.general")) 54 | .option(Option.createBuilder().controller( 55 | BooleanControllerBuilder::create).binding( 56 | defaults.isCacheTextures(), 57 | config::isCacheTextures, 58 | config::setCacheTextures 59 | ).name(translatable("openmcskins.option.cacheTextures")) 60 | .build()) 61 | .option(Option.createBuilder().controller( 62 | BooleanControllerBuilder::create).binding( 63 | defaults.isVerboseLogs(), 64 | config::isVerboseLogs, 65 | config::setVerboseLogs 66 | ).name(translatable("openmcskins.option.verboseLogs")) 67 | .build()) 68 | .option(Option.createBuilder().controller( 69 | BooleanControllerBuilder::create).binding( 70 | defaults.isRefreshOnConfigSave(), 71 | config::isRefreshOnConfigSave, 72 | config::setRefreshOnConfigSave 73 | ).name(translatable( 74 | "openmcskins.option.refreshOnConfigSave")) 75 | .build()) 76 | .option(Option.createBuilder() 77 | .controller(option -> DoubleSliderControllerBuilder.create( 78 | option).range(0.5, 60.).step(0.5)) 79 | .binding( 80 | defaults.getLoaderTimeout(), 81 | config::getLoaderTimeout, 82 | config::setLoaderTimeout 83 | ).name(translatable( 84 | "openmcskins.option.loaderTimeout")) 85 | .build()) 86 | .option(Option.createBuilder() 87 | .controller(option -> EnumControllerBuilder.create( 88 | option).enumClass(UuidMode.class) 89 | .formatValue(this::formatUuidMode)) 90 | .binding( 91 | defaults.getUuidMode(), 92 | config::getUuidMode, 93 | config::setUuidMode 94 | ).name(translatable( 95 | "openmcskins.option.uuidMode")).build()) 96 | .option(Option.createBuilder() 97 | .controller(option -> EnumControllerBuilder.create( 98 | option).enumClass(FilterMode.class) 99 | .formatValue(this::formatFilterMode)) 100 | .binding( 101 | defaults.getFilterMode(), 102 | config::getFilterMode, 103 | config::setFilterMode 104 | ).name(translatable( 105 | "openmcskins.option.filterMode")).build()); 106 | } 107 | 108 | private @NotNull Text formatUuidMode(@NotNull UuidMode mode) { 109 | return translatable( 110 | "openmcskins.option.uuidMode." + mode.toString().toLowerCase()); 111 | } 112 | 113 | private @NotNull Text formatFilterMode(@NotNull FilterMode mode) { 114 | return translatable( 115 | "openmcskins.option.filterMode." + mode.toString().toLowerCase()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/mixin/PlayerSkinProviderMixin.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins.mixin; 2 | 3 | import com.google.common.cache.LoadingCache; 4 | import com.mojang.authlib.GameProfile; 5 | import lombok.SneakyThrows; 6 | import lombok.val; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.texture.PlayerSkinProvider; 9 | import net.minecraft.client.util.DefaultSkinHelper; 10 | import net.minecraft.client.util.SkinTextures; 11 | import net.zatrit.skins.SkinsClient; 12 | import net.zatrit.skins.accessor.AsyncUUIDRefresher; 13 | import net.zatrit.skins.accessor.Refreshable; 14 | import net.zatrit.skins.lib.api.Profile; 15 | import net.zatrit.skins.lib.api.Resolver; 16 | import net.zatrit.skins.lib.data.TypedTexture; 17 | import net.zatrit.skins.texture.TextureIdentifier; 18 | import net.zatrit.skins.texture.TextureLoader; 19 | import org.jetbrains.annotations.NotNull; 20 | import org.spongepowered.asm.mixin.Final; 21 | import org.spongepowered.asm.mixin.Mixin; 22 | import org.spongepowered.asm.mixin.Shadow; 23 | import org.spongepowered.asm.mixin.Unique; 24 | import org.spongepowered.asm.mixin.injection.At; 25 | import org.spongepowered.asm.mixin.injection.Inject; 26 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 27 | 28 | import java.util.concurrent.CompletableFuture; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | @Mixin(PlayerSkinProvider.class) 32 | public class PlayerSkinProviderMixin implements Refreshable { 33 | @Shadow @Final 34 | private LoadingCache> cache; 35 | 36 | @SneakyThrows 37 | @Inject( 38 | at = @At("HEAD"), 39 | method = "fetchSkinTextures(Lcom/mojang/authlib/GameProfile;)Ljava/util/concurrent/CompletableFuture;", 40 | cancellable = true) 41 | public void fetchSkinTextures( 42 | @NotNull GameProfile profile2, 43 | @NotNull CallbackInfoReturnable> cir) { 44 | 45 | cir.setReturnValue(cache.get(new PlayerSkinProvider.Key( 46 | profile2.getId(), 47 | null 48 | ), () -> fetchSkinTextures((Profile) profile2))); 49 | } 50 | 51 | @Unique 52 | private CompletableFuture fetchSkinTextures( 53 | @NotNull Profile profile) { 54 | val dispatcher = SkinsClient.getDispatcher(); 55 | val resolvers = SkinsClient.getResolvers(); 56 | 57 | val config = SkinsClient.getConfigHandler().instance(); 58 | val timeout = (int) (config.getLoaderTimeout() * 1000); 59 | val refreshUuid = switch (config.getUuidMode()) { 60 | case NEVER -> false; 61 | case ALWAYS -> true; 62 | case OFFLINE -> { 63 | val client = MinecraftClient.getInstance(); 64 | val networkHandler = client.getNetworkHandler(); 65 | 66 | yield networkHandler != null && 67 | !networkHandler.getConnection().isEncrypted(); 68 | } 69 | }; 70 | 71 | CompletableFuture profileFuture; 72 | if (resolvers.stream().anyMatch(Resolver::requiresUuid) && refreshUuid) { 73 | profileFuture = ((AsyncUUIDRefresher) profile).skins$refreshUuid() 74 | .exceptionally(SkinsClient.getErrorHandler().andReturn(profile)); 75 | } else { 76 | profileFuture = CompletableFuture.completedFuture(profile); 77 | } 78 | 79 | val errorHandler = SkinsClient.getErrorHandler(); 80 | val defaultTextures = DefaultSkinHelper.getSkinTextures(profile.getId()); 81 | val textures = new SkinTextures( 82 | defaultTextures.texture(), 83 | null, 84 | defaultTextures.capeTexture(), 85 | defaultTextures.elytraTexture(), 86 | defaultTextures.model(), 87 | true 88 | ); 89 | 90 | return profileFuture.thenApplyAsync(profile1 -> { 91 | val futures = dispatcher.resolveAsync(resolvers, profile1) 92 | // Added error handling in all futures 93 | .map(f -> f.exceptionally(errorHandler.andReturn(null))); 94 | 95 | return dispatcher.fetchTexturesAsync(futures).join(); 96 | }).orTimeout(timeout, TimeUnit.MILLISECONDS).thenApplyAsync(result -> { 97 | if (result != null) { 98 | for (val texture : result) { 99 | this.loadTexture(texture, textures, profile); 100 | } 101 | } 102 | 103 | return textures; 104 | }).exceptionally(errorHandler.andReturn(textures)); 105 | } 106 | 107 | @Unique 108 | @SneakyThrows 109 | private void loadTexture( 110 | @NotNull TypedTexture result, SkinTextures textures, 111 | @NotNull Profile profile) { 112 | val texture = result.getTexture(); 113 | val metadata = texture.getMetadata(); 114 | val textureId = new TextureIdentifier( 115 | profile.getName(), 116 | result.getType() 117 | ); 118 | 119 | TextureLoader.create(texture).getTexture(textureId, id -> { 120 | switch (result.getType()) { 121 | case SKIN -> { 122 | textures.texture = id; 123 | 124 | if (metadata != null && metadata.getModel() != null) { 125 | textures.model = SkinTextures.Model.fromName(metadata.getModel()); 126 | } else { 127 | textures.model = SkinTextures.Model.WIDE; 128 | } 129 | } 130 | case CAPE -> textures.capeTexture = id; 131 | } 132 | }); 133 | } 134 | 135 | @Unique 136 | @Override 137 | public void skins$refresh() { 138 | cache.invalidateAll(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/net/zatrit/skins/SkinsCommands.java: -------------------------------------------------------------------------------- 1 | package net.zatrit.skins; 2 | 3 | import com.moandjiezana.toml.Toml; 4 | import com.mojang.brigadier.CommandDispatcher; 5 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 6 | import com.mojang.brigadier.context.CommandContext; 7 | import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; 8 | import lombok.Cleanup; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.SneakyThrows; 11 | import lombok.val; 12 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; 13 | import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; 14 | import net.fabricmc.loader.api.FabricLoader; 15 | import net.minecraft.command.CommandRegistryAccess; 16 | import net.minecraft.text.Text; 17 | import net.zatrit.skins.accessor.HasAssetPath; 18 | import net.zatrit.skins.config.HostEntry; 19 | import net.zatrit.skins.config.SkinsConfig; 20 | import net.zatrit.skins.util.command.*; 21 | import org.apache.commons.io.FileUtils; 22 | import org.jetbrains.annotations.Contract; 23 | import org.jetbrains.annotations.NotNull; 24 | import org.jetbrains.annotations.Nullable; 25 | 26 | import java.io.IOException; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | import java.util.List; 30 | import java.util.concurrent.CompletableFuture; 31 | import java.util.function.Supplier; 32 | 33 | import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; 34 | import static net.zatrit.skins.cache.AssetCacheProvider.CACHE_DIR; 35 | import static net.zatrit.skins.util.command.CommandUtil.argument; 36 | import static net.zatrit.skins.util.command.CommandUtil.literal; 37 | 38 | @RequiredArgsConstructor 39 | public class SkinsCommands implements ClientCommandRegistrationCallback { 40 | private final ConfigClassHandler configHolder; 41 | private final HasAssetPath assetPath; 42 | private @Nullable CompletableFuture cleanupFuture; 43 | 44 | @Override 45 | public void register( 46 | @NotNull CommandDispatcher dispatcher, 47 | CommandRegistryAccess registryAccess) { 48 | val presetsPath = FabricLoader.getInstance().getConfigDir().resolve( 49 | "openmcskins"); 50 | 51 | val presetsType = new FileArgumentType(new FileProvider[]{ 52 | new IndexedResourceProvider( 53 | "presets", 54 | getClass().getClassLoader() 55 | ), new DirectoryFileProvider(presetsPath) 56 | }, "toml"); 57 | presetsType.refresh(); 58 | 59 | val command = literal("openmcskins") 60 | // omcs refresh 61 | .then(literal("refresh").executes(this::refresh)) 62 | // omcs clean 63 | .then(literal("clean").executes(this::clean)) 64 | // omcs add (preset (e.g. mojang)) [pos] 65 | .then(literal("add").then(argument("preset", presetsType).executes( 66 | this::addHost).then(argument( 67 | "pos", 68 | integer(0) 69 | ).executes(this::addHost)))) 70 | // omcs list 71 | .then(literal("list").executes(this::listHosts)) 72 | // omcs remove (pos) 73 | .then(literal("remove").then(argument("pos", integer(0)).executes( 74 | this::removeHost))) 75 | // omcs move (from) (to) 76 | .then(literal("move").then(argument( 77 | "from", 78 | integer(0) 79 | ).then(argument( 80 | "to", 81 | integer(0) 82 | ).executes(this::moveHost)))) 83 | // omcs (blacklist | whitelist) -> 84 | // add (name | uuid) 85 | // remove (name | uuid) 86 | // clear 87 | .then(filterListArgument("blacklist", List.of())).then( 88 | filterListArgument("whitelist", List.of())); 89 | 90 | dispatcher.register(command); 91 | dispatcher.register(literal("omcs").redirect(command.build())); 92 | } 93 | 94 | private int refresh( 95 | @NotNull CommandContext context) { 96 | if (SkinsClient.refresh()) { 97 | return 0; 98 | } else { 99 | context.getSource().sendError(Text.translatable( 100 | "openmcskins.command.unableToRefresh")); 101 | return -1; 102 | } 103 | } 104 | 105 | @SneakyThrows 106 | public int addHost( 107 | @NotNull CommandContext context) { 108 | @Cleanup val stream = Files.newInputStream(context.getArgument( 109 | "preset", 110 | Path.class 111 | )); 112 | int pos = 0; 113 | 114 | try { 115 | pos = context.getArgument("pos", Integer.class); 116 | } catch (IllegalArgumentException ignored) { 117 | } 118 | 119 | val toml = new Toml().read(stream); 120 | val entry = toml.to(HostEntry.class); 121 | 122 | if (entry.getType() == null) { 123 | context.getSource().sendError(Text.translatable( 124 | "openmcskins.command.invalidFileFormat")); 125 | return -1; 126 | } 127 | 128 | val config = this.configHolder.instance(); 129 | config.getHosts().add(pos, entry); 130 | this.configHolder.save(); 131 | 132 | context.getSource().sendFeedback(Text.translatable( 133 | "openmcskins.command.added", 134 | entry.toText() 135 | )); 136 | 137 | return 0; 138 | } 139 | 140 | private int listHosts( 141 | @NotNull CommandContext context) { 142 | val entries = this.configHolder.instance().getHosts().stream().map( 143 | TextUtil.ToText::toText).toArray(Text[]::new); 144 | var result = Text.translatable("openmcskins.command.list"); 145 | 146 | for (int i = 0; i < entries.length; i++) { 147 | result.append(Text.literal("\n").append(Text.translatable( 148 | "openmcskins.command.listEntry", 149 | TextUtil.formatNumber(i), 150 | entries[i] 151 | ))); 152 | } 153 | 154 | context.getSource().sendFeedback(result); 155 | 156 | return 0; 157 | } 158 | 159 | private int removeHost( 160 | @NotNull CommandContext context) { 161 | val pos = context.getArgument("pos", Integer.class); 162 | 163 | val config = this.configHolder.instance(); 164 | val entry = config.getHosts().remove(pos.intValue()); 165 | this.configHolder.save(); 166 | 167 | context.getSource().sendFeedback(Text.translatable( 168 | "openmcskins.command.removed", 169 | entry.toText() 170 | )); 171 | 172 | return 0; 173 | } 174 | 175 | private int moveHost( 176 | @NotNull CommandContext context) { 177 | val from = context.getArgument("from", Integer.class); 178 | val to = context.getArgument("to", Integer.class); 179 | 180 | val config = this.configHolder.instance(); 181 | val entry = config.getHosts().remove(from.intValue()); 182 | config.getHosts().add(to, entry); 183 | this.configHolder.save(); 184 | 185 | context.getSource().sendFeedback(Text.translatable( 186 | "openmcskins.command.moved", 187 | TextUtil.formatNumber(from), 188 | TextUtil.formatNumber(to) 189 | )); 190 | 191 | return 0; 192 | } 193 | 194 | @SneakyThrows 195 | @SuppressWarnings("resource") 196 | private int clean( 197 | @NotNull CommandContext context) { 198 | if (cleanupFuture != null && !cleanupFuture.isDone()) { 199 | context.getSource().sendError(Text.translatable( 200 | "openmcskins.command.cleanupAlready")); 201 | return -1; 202 | } 203 | 204 | cleanupFuture = CompletableFuture.supplyAsync(new Supplier() { 205 | @Override 206 | @SneakyThrows 207 | public Void get() { 208 | Files.list(assetPath.getAssetPath().resolve(CACHE_DIR)) 209 | .map(Path::toFile).parallel().forEach(directory -> { 210 | try { 211 | FileUtils.deleteDirectory(directory); 212 | } catch (IOException e) { 213 | SkinsClient.getErrorHandler().accept(e); 214 | } 215 | }); 216 | 217 | return null; 218 | } 219 | }).whenComplete((r, e) -> { 220 | if (e == null) { 221 | context.getSource().sendFeedback(Text.translatable( 222 | "openmcskins.command.cleanupSuccess")); 223 | } else { 224 | context.getSource().sendError(Text.translatable( 225 | "openmcskins.command.cleanupFailed", 226 | e.getMessage() 227 | )); 228 | } 229 | }); 230 | 231 | return 0; 232 | } 233 | 234 | @Contract(value = "_, _ -> new", pure = true) 235 | private @NotNull LiteralArgumentBuilder filterListArgument( 236 | @NotNull String name, @NotNull List list) { 237 | return literal(name); 238 | } 239 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /license/APACHE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------