From bfaddf15bc09e337b41ce77f215ab431361a9078 Mon Sep 17 00:00:00 2001 From: Howard ZHY <96782166+HowardZHY@users.noreply.github.com> Date: Thu, 17 Nov 2022 01:07:08 +0800 Subject: [PATCH] Fix Cape+Cleanup useless files --- .../customskinloader/CustomSkinLoader.java | 233 ++++++ src/main/java/customskinloader/Logger.java | 106 +++ .../java/customskinloader/config/Config.java | 252 +++++++ .../config/SkinSiteProfile.java | 44 ++ .../fabric/MixinConfigPlugin.java | 86 +++ .../customskinloader/fake/FakeCapeBuffer.java | 209 ++++++ .../fake/FakeClientPlayer.java | 89 +++ .../customskinloader/fake/FakeSkinBuffer.java | 175 +++++ .../fake/FakeSkinManager.java | 153 ++++ .../fake/FakeThreadDownloadImageData.java | 82 +++ .../fake/itf/FakeInterfaceManager.java | 42 ++ .../fake/itf/IFakeIResource.java | 22 + .../fake/itf/IFakeIResourceManager.java | 20 + .../fake/itf/IFakeMinecraft.java | 15 + .../fake/itf/IFakeTextureManager.java | 27 + .../itf/IFakeThreadDownloadImageData.java | 12 + .../fake/texture/FakeBufferedImage.java | 75 ++ .../fake/texture/FakeImage.java | 26 + .../fake/texture/FakeNativeImage.java | 61 ++ .../loader/JsonAPILoader.java | 138 ++++ .../customskinloader/loader/LegacyLoader.java | 200 ++++++ .../loader/MojangAPILoader.java | 223 ++++++ .../loader/ProfileLoader.java | 33 + .../loader/jsonapi/CustomSkinAPI.java | 119 ++++ .../loader/jsonapi/CustomSkinAPIPlus.java | 65 ++ .../loader/jsonapi/ElyByAPI.java | 57 ++ .../loader/jsonapi/GlitchlessAPI.java | 56 ++ .../loader/jsonapi/MinecraftCapesAPI.java | 110 +++ .../loader/jsonapi/UniSkinAPI.java | 93 +++ .../mixin/MixinGuiPlayerTabOverlay.java | 24 + .../mixin/MixinIResource.java | 10 + .../mixin/MixinIResourceManager.java | 9 + .../mixin/MixinMinecraft.java | 10 + .../mixin/MixinPlayerMenuObject.java | 34 + .../mixin/MixinSkinManager.java | 70 ++ .../mixin/MixinTextureManager.java | 22 + .../mixin/MixinThreadDownloadImageDataV1.java | 23 + .../plugin/ICustomSkinLoaderPlugin.java | 35 + .../customskinloader/plugin/PluginLoader.java | 54 ++ .../profile/DynamicSkullManager.java | 160 +++++ .../profile/ModelManager0.java | 131 ++++ .../profile/ProfileCache.java | 126 ++++ .../customskinloader/profile/UserProfile.java | 103 +++ .../utils/HttpRequestUtil.java | 281 ++++++++ .../utils/HttpTextureUtil.java | 107 +++ .../customskinloader/utils/HttpUtil0.java | 127 ++++ .../java/customskinloader/utils/JavaUtil.java | 34 + .../customskinloader/utils/MinecraftUtil.java | 99 +++ .../java/customskinloader/utils/TimeUtil.java | 17 + .../java/customskinloader/utils/Version.java | 50 ++ .../render/BufferedImageSkinProvider.java | 16 + .../client/texture/DownloadingTexture.java | 17 + .../minecraft/client/texture/NativeImage.java | 63 ++ .../texture/ThreadDownloadImageData.java | 13 + src/main/resources/BuildInfo.json | 8 + src/main/resources/CSL_LICENSE | 36 + src/main/resources/LICENSE | 674 ++++++++++++++++++ src/main/resources/fabric.mod.json | 18 + .../resources/mixins.customskinloader.json | 19 + 59 files changed, 5213 insertions(+) create mode 100644 src/main/java/customskinloader/CustomSkinLoader.java create mode 100644 src/main/java/customskinloader/Logger.java create mode 100644 src/main/java/customskinloader/config/Config.java create mode 100644 src/main/java/customskinloader/config/SkinSiteProfile.java create mode 100644 src/main/java/customskinloader/fabric/MixinConfigPlugin.java create mode 100644 src/main/java/customskinloader/fake/FakeCapeBuffer.java create mode 100644 src/main/java/customskinloader/fake/FakeClientPlayer.java create mode 100644 src/main/java/customskinloader/fake/FakeSkinBuffer.java create mode 100644 src/main/java/customskinloader/fake/FakeSkinManager.java create mode 100644 src/main/java/customskinloader/fake/FakeThreadDownloadImageData.java create mode 100644 src/main/java/customskinloader/fake/itf/FakeInterfaceManager.java create mode 100644 src/main/java/customskinloader/fake/itf/IFakeIResource.java create mode 100644 src/main/java/customskinloader/fake/itf/IFakeIResourceManager.java create mode 100644 src/main/java/customskinloader/fake/itf/IFakeMinecraft.java create mode 100644 src/main/java/customskinloader/fake/itf/IFakeTextureManager.java create mode 100644 src/main/java/customskinloader/fake/itf/IFakeThreadDownloadImageData.java create mode 100644 src/main/java/customskinloader/fake/texture/FakeBufferedImage.java create mode 100644 src/main/java/customskinloader/fake/texture/FakeImage.java create mode 100644 src/main/java/customskinloader/fake/texture/FakeNativeImage.java create mode 100644 src/main/java/customskinloader/loader/JsonAPILoader.java create mode 100644 src/main/java/customskinloader/loader/LegacyLoader.java create mode 100644 src/main/java/customskinloader/loader/MojangAPILoader.java create mode 100644 src/main/java/customskinloader/loader/ProfileLoader.java create mode 100644 src/main/java/customskinloader/loader/jsonapi/CustomSkinAPI.java create mode 100644 src/main/java/customskinloader/loader/jsonapi/CustomSkinAPIPlus.java create mode 100644 src/main/java/customskinloader/loader/jsonapi/ElyByAPI.java create mode 100644 src/main/java/customskinloader/loader/jsonapi/GlitchlessAPI.java create mode 100644 src/main/java/customskinloader/loader/jsonapi/MinecraftCapesAPI.java create mode 100644 src/main/java/customskinloader/loader/jsonapi/UniSkinAPI.java create mode 100644 src/main/java/customskinloader/mixin/MixinGuiPlayerTabOverlay.java create mode 100644 src/main/java/customskinloader/mixin/MixinIResource.java create mode 100644 src/main/java/customskinloader/mixin/MixinIResourceManager.java create mode 100644 src/main/java/customskinloader/mixin/MixinMinecraft.java create mode 100644 src/main/java/customskinloader/mixin/MixinPlayerMenuObject.java create mode 100644 src/main/java/customskinloader/mixin/MixinSkinManager.java create mode 100644 src/main/java/customskinloader/mixin/MixinTextureManager.java create mode 100644 src/main/java/customskinloader/mixin/MixinThreadDownloadImageDataV1.java create mode 100644 src/main/java/customskinloader/plugin/ICustomSkinLoaderPlugin.java create mode 100644 src/main/java/customskinloader/plugin/PluginLoader.java create mode 100644 src/main/java/customskinloader/profile/DynamicSkullManager.java create mode 100644 src/main/java/customskinloader/profile/ModelManager0.java create mode 100644 src/main/java/customskinloader/profile/ProfileCache.java create mode 100644 src/main/java/customskinloader/profile/UserProfile.java create mode 100644 src/main/java/customskinloader/utils/HttpRequestUtil.java create mode 100644 src/main/java/customskinloader/utils/HttpTextureUtil.java create mode 100644 src/main/java/customskinloader/utils/HttpUtil0.java create mode 100644 src/main/java/customskinloader/utils/JavaUtil.java create mode 100644 src/main/java/customskinloader/utils/MinecraftUtil.java create mode 100644 src/main/java/customskinloader/utils/TimeUtil.java create mode 100644 src/main/java/customskinloader/utils/Version.java create mode 100644 src/main/java/net/minecraft/client/render/BufferedImageSkinProvider.java create mode 100644 src/main/java/net/minecraft/client/texture/DownloadingTexture.java create mode 100644 src/main/java/net/minecraft/client/texture/NativeImage.java create mode 100644 src/main/java/net/minecraft/client/texture/ThreadDownloadImageData.java create mode 100644 src/main/resources/BuildInfo.json create mode 100644 src/main/resources/CSL_LICENSE create mode 100644 src/main/resources/LICENSE create mode 100644 src/main/resources/fabric.mod.json create mode 100644 src/main/resources/mixins.customskinloader.json diff --git a/src/main/java/customskinloader/CustomSkinLoader.java b/src/main/java/customskinloader/CustomSkinLoader.java new file mode 100644 index 0000000..5d692f3 --- /dev/null +++ b/src/main/java/customskinloader/CustomSkinLoader.java @@ -0,0 +1,233 @@ +package customskinloader; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import customskinloader.config.Config; +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.ProfileLoader; +import customskinloader.profile.DynamicSkullManager; +import customskinloader.profile.ModelManager0; +import customskinloader.profile.ProfileCache; +import customskinloader.profile.UserProfile; +import customskinloader.utils.MinecraftUtil; + +/** + * Custom skin loader mod for Minecraft. + * + * @author (C) Jeremy Lam [JLChnToZ] 2013-2014 & Alexander Xia [xfl03] 2014-2022 + * @version @MOD_FULL_VERSION@ + */ +public class CustomSkinLoader { + public static final String CustomSkinLoader_VERSION = "14.14-LF"; + public static final String CustomSkinLoader_FULL_VERSION = "14.14-LF"; + public static final int CustomSkinLoader_BUILD_NUMBER = 0; + + public static final File + DATA_DIR = new File(MinecraftUtil.getMinecraftDataDir(), "CustomSkinLoader"), + LOG_FILE = new File(DATA_DIR, "CustomSkinLoader.log"), + CONFIG_FILE = new File(DATA_DIR, "CustomSkinLoader.json"); + + public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + public static final Logger logger = initLogger(); + public static final Config config = Config.loadConfig0(); + + private static final ProfileCache profileCache = new ProfileCache(); + private static final DynamicSkullManager dynamicSkullManager = new DynamicSkullManager(); + + private static final ExecutorService THREAD_POOL = new ThreadPoolExecutor(config.threadPoolSize, config.threadPoolSize, 1L, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); + + //Correct thread name in thread pool + private static final ThreadFactory defaultFactory = Executors.defaultThreadFactory(); + private static final ThreadFactory customFactory = r -> { + Thread t = defaultFactory.newThread(r); + if (r instanceof Thread) { + t.setName(((Thread) r).getName()); + } + return t; + }; + //Thread pool will discard the oldest task when queue reaches 333 tasks + private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor( + config.threadPoolSize, config.threadPoolSize, 1L, TimeUnit.MINUTES, + new LinkedBlockingQueue<>(333), customFactory, new ThreadPoolExecutor.DiscardOldestPolicy() + ); + + public static void loadProfileTextures(Runnable runnable) { + THREAD_POOL.execute(runnable); + } + + public static void loadProfileLazily(GameProfile gameProfile, Consumer> consumer) { + String username = gameProfile.getName(); + String credential = MinecraftUtil.getCredential(gameProfile); + // Fix: http://hopper.minecraft.net/crashes/minecraft/MCX-2773713 + if (username == null) { + logger.warning("Could not load profile: username is null."); + consumer.accept(Maps.newHashMap()); + return; + } + String tempName = Thread.currentThread().getName(); + Thread.currentThread().setName(username); // Change Thread Name + if (profileCache.isLoading(credential)) { + profileCache.putLoader(credential, consumer); + Thread.currentThread().setName(tempName); + return; + } + consumer.accept(loadProfile(gameProfile)); + Thread.currentThread().setName(tempName); + profileCache.getLastLoader(credential).ifPresent(c -> loadProfileLazily(gameProfile, c)); + } + + //For User Skin + public static Map loadProfile(GameProfile gameProfile) { + String credential = MinecraftUtil.getCredential(gameProfile); + UserProfile profile; + if (profileCache.isReady(credential)) { + logger.info("Cached profile will be used."); + profile = profileCache.getProfile(credential); + if (profile == null) { + logger.warning("(Cached Profile is empty!) Expiry: " + profileCache.getExpiry(credential)); + if (profileCache.isExpired(credential)) { // force load + profile = loadProfile0(gameProfile, false); + } + } else { + logger.info(profile.toString(profileCache.getExpiry(credential))); + } + } else { + profileCache.setLoading(credential, true); + profile = loadProfile0(gameProfile, false); + } + return ModelManager0.fromUserProfile(profile); + } + + //Core + public static UserProfile loadProfile0(GameProfile gameProfile, boolean isSkull) { + String username = gameProfile.getName(); + String credential = MinecraftUtil.getCredential(gameProfile); + + profileCache.setLoading(credential, true); + logger.info("Loading " + username + "'s profile."); + if (config.loadlist == null || config.loadlist.isEmpty()) { + logger.info("LoadList is Empty."); + return null; + } + + UserProfile profile0 = new UserProfile(); + for (int i = 0; i < config.loadlist.size(); i++) { + SkinSiteProfile ssp = config.loadlist.get(i); + logger.info((i + 1) + "/" + config.loadlist.size() + " Try to load profile from '" + ssp.name + "'."); + if (ssp.type == null) { + logger.info("The type of '" + ssp.name + "' is null."); + continue; + } + ProfileLoader.IProfileLoader loader = ProfileLoader.LOADERS.get(ssp.type.toLowerCase()); + if (loader == null) { + logger.info("Type '" + ssp.type + "' is not defined."); + continue; + } + UserProfile profile = null; + try { + profile = loader.loadProfile(ssp, gameProfile); + } catch (Exception e) { + logger.warning("Exception occurs while loading."); + logger.warning(e); + if (e.getCause() != null) { + logger.warning("Caused By:"); + logger.warning(e.getCause()); + } + } + if (profile == null) { + continue; + } + profile0.mix(profile); + if (isSkull && !profile0.hasSkinUrl()) { + continue; + } + if (!config.forceLoadAllTextures) { + break; + } + if (profile0.isFull()) { + break; + } + } + if (!profile0.isEmpty()) { + logger.info(username + "'s profile loaded."); + if (!config.enableCape) { + profile0.capeUrl = null; + } + profileCache.updateCache(credential, profile0); + profileCache.setLoading(credential, false); + logger.info(profile0.toString(profileCache.getExpiry(credential))); + return profile0; + } + logger.info(username + "'s profile not found in load list."); + + if (config.enableLocalProfileCache) { + UserProfile profile = profileCache.getLocalProfile(credential); + if (profile == null) { + logger.info(username + "'s LocalProfile not found."); + } else { + profileCache.updateCache(credential, profile, false); + profileCache.setLoading(credential, false); + logger.info(username + "'s LocalProfile will be used."); + logger.info(profile.toString(profileCache.getExpiry(credential))); + return profile; + } + } + profileCache.setLoading(credential, false); + return null; + } + + //For Skull + public static Map loadProfileFromCache(final GameProfile gameProfile) { + String username = gameProfile.getName(); + String credential = MinecraftUtil.getCredential(gameProfile); + + //CustomSkinLoader needs username to load standard skin, if username not exist, only textures in NBT can be used + //Authlib 3.11.50 makes empty username to "\u0020" + if (username == null || username.isEmpty() || username.equals("\u0020") || credential == null) { + return dynamicSkullManager.getTexture(gameProfile); + } + if (config.forceUpdateSkull ? profileCache.isReady(credential) : profileCache.isExist(credential)) { + UserProfile profile = profileCache.getProfile(credential); + return ModelManager0.fromUserProfile(profile); + } + if (!profileCache.isLoading(credential)) { + profileCache.setLoading(credential, true); + Runnable loadThread = () -> { + String tempName = Thread.currentThread().getName(); + Thread.currentThread().setName(username + "'s skull"); + loadProfile0(gameProfile, true);//Load in thread + Thread.currentThread().setName(tempName); + }; + if (config.forceUpdateSkull) { + new Thread(loadThread).start(); + } else { + threadPool.execute(loadThread); + } + } + return Maps.newHashMap(); + } + + private static Logger initLogger() { + Logger logger = new Logger(LOG_FILE); + logger.info("CustomSkinLoader " + CustomSkinLoader_FULL_VERSION); + logger.info("DataDir: " + DATA_DIR.getAbsolutePath()); + logger.info("Operating System: " + System.getProperty("os.name") + " (" + System.getProperty("os.arch") + ") version " + System.getProperty("os.version")); + logger.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor")); + logger.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor")); + logger.info("Minecraft: " + MinecraftUtil.getMinecraftMainVersion() + "Legacy Fabric"); + return logger; + } +} diff --git a/src/main/java/customskinloader/Logger.java b/src/main/java/customskinloader/Logger.java new file mode 100644 index 0000000..ea32d42 --- /dev/null +++ b/src/main/java/customskinloader/Logger.java @@ -0,0 +1,106 @@ +package customskinloader; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +@SuppressWarnings("ResultOfMethodCallIgnored") +public class Logger { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + public enum Level{ + DEBUG("DEBUG",false), + INFO("INFO",true), + WARNING("WARNING",true); + + + String name; + boolean display; + Level(String name,boolean display){ + this.name=name; + this.display=display; + } + public String getName(){ + return name; + } + public boolean display(){ + return display; + } + } + private BufferedWriter writer=null; + public Logger(){ + //Logger isn't created. + } + public Logger(String logFile) { + this(new File(logFile)); + } + public Logger(File logFile){ + try { + if(!logFile.getParentFile().exists()){ + logFile.getParentFile().mkdirs(); + } + if(!logFile.exists()) + logFile.createNewFile(); + + writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(logFile), "UTF-8")); + + System.out.println("Log Path: " + logFile.getAbsolutePath()); + } catch (Exception e) { + e.printStackTrace(); + } + } + public void close(){ + if(writer!=null){ + try { + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public void log(Level level,String msg){ + if(!level.display()&&writer==null) + return; + String sb = String.format("[%s %s] %s", Thread.currentThread().getName(), level.getName(), msg); + if(level.display()) + System.out.println(sb); + if(writer==null) + return; + try { + String sb2 = String.format("[%s] %s\r\n", DATE_FORMAT.format(new Date()), sb); + writer.write(sb2); + writer.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + public void debug(String msg){ + log(Level.DEBUG,msg); + } + public void debug(String format, Object... objs) { + debug(String.format(format, objs)); + } + public void info(String msg){ + log(Level.INFO,msg); + } + public void info(String format, Object... objs){ + info(String.format(format, objs)); + } + public void warning(String msg){ + log(Level.WARNING,msg); + } + public void warning(String format, Object... objs){ + warning(String.format(format, objs)); + } + public void warning(Throwable e){ + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + log(Level.WARNING,"Exception: "+sw.toString()); + } +} diff --git a/src/main/java/customskinloader/config/Config.java b/src/main/java/customskinloader/config/Config.java new file mode 100644 index 0000000..0abf538 --- /dev/null +++ b/src/main/java/customskinloader/config/Config.java @@ -0,0 +1,252 @@ +package customskinloader.config; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import customskinloader.CustomSkinLoader; +import customskinloader.loader.ProfileLoader; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.plugin.PluginLoader; +import customskinloader.utils.HttpRequestUtil; +import customskinloader.utils.HttpTextureUtil; +import customskinloader.utils.HttpUtil0; +import customskinloader.utils.Version; +import org.apache.commons.io.FileUtils; + +@SuppressWarnings("ResultOfMethodCallIgnored") +public class Config { + //Program + public String version; + public int buildNumber; + public List loadlist; + + //Function + public boolean enableDynamicSkull=true; + public boolean enableTransparentSkin=true; + public boolean forceIgnoreHttpsCertificate = false; + public boolean forceLoadAllTextures=false; + public boolean enableCape = true; + public int threadPoolSize = 3; + public int retryTime = 1; + + //Profile Cache + public int cacheExpiry=30; + public boolean forceUpdateSkull = false; + public boolean enableLocalProfileCache=false; + + //Network Cache + public boolean enableCacheAutoClean=false; + public boolean forceDisableCache = false; + + // Used by Gson to create an instance with default value. + public Config() { + this(new ArrayList<>()); + } + + //Init config + public Config(List loadlist){ + this.version=CustomSkinLoader.CustomSkinLoader_VERSION; + this.buildNumber=CustomSkinLoader.CustomSkinLoader_BUILD_NUMBER; + this.loadlist=loadlist; + } + + public static Config loadConfig0() { + Config config=loadConfig(); + + //LoadList null checker + if(config.loadlist==null){ + config.loadlist=new ArrayList(); + }else{ + for(int i=0;i adds=new ArrayList(); + File[] files=listAddition.listFiles(); + for(File file: files != null ? files : new File[0]){ + if(!file.getName().toLowerCase().endsWith(".json")&&!file.getName().toLowerCase().endsWith(".txt")) + continue; + try { + CustomSkinLoader.logger.info("Try to load Extra List.("+file.getName()+")"); + String json=FileUtils.readFileToString(file, "UTF-8"); + SkinSiteProfile ssp=CustomSkinLoader.GSON.fromJson(json, SkinSiteProfile.class); + CustomSkinLoader.logger.info("Successfully load Extra List."); + if (ssp.type != null) { + ProfileLoader.IProfileLoader loader = ProfileLoader.LOADERS.get(ssp.type.toLowerCase()); + if (loader == null) { + CustomSkinLoader.logger.info("Extra List will be ignored: Type '" + ssp.type + "' is not defined."); + continue; + } + boolean duplicate = false; + for (SkinSiteProfile ssp0 : this.loadlist) { + if (!ssp0.type.equalsIgnoreCase(ssp.type)) + continue; + if (loader.compare(ssp0, ssp)) { + duplicate = true; + break; + } + } + if (!duplicate) { + adds.add(ssp); + CustomSkinLoader.logger.info("Successfully apply Extra List.(" + ssp.name + ")"); + } else { + CustomSkinLoader.logger.info("Extra List will be ignored: Duplicate.(" + ssp.name + ")"); + } + file.delete(); + } else { + CustomSkinLoader.logger.info("Extra List is invalid: Type is not defined.(" + file.getName() + ")"); + createBrokenFile(file); + } + }catch (Exception e) { + CustomSkinLoader.logger.info("Failed to load Extra List.("+e.toString()+")"); + createBrokenFile(file); + } + } + if(adds.size()!=0){ + adds.addAll(this.loadlist); + this.loadlist=adds; + } + } + + private void updateLoadlist() { + PluginLoader.PLUGINS.stream() + .map(ICustomSkinLoaderPlugin::getDefaultProfiles) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .forEach(profile -> this.loadlist.stream() + .filter(ssp -> profile.getName().equals(ssp.name)) + .forEach(profile::updateSkinSiteProfile)); + } + + private void initLocalFolder(){ + for(SkinSiteProfile ssp:this.loadlist){ + if(ssp.type==null) + continue; + ProfileLoader.IProfileLoader loader=ProfileLoader.LOADERS.get(ssp.type.toLowerCase()); + if(loader==null) + continue; + loader.init(ssp); + } + } + + private static void createBrokenFile(File file) { + try { + File brokenFile = new File(file.getParentFile(), file.getName() + ".broken"); + if (brokenFile.exists()) { + brokenFile.delete(); + } + file.renameTo(brokenFile); + } catch (Exception e) { + CustomSkinLoader.logger.warning("Failed to create broken file. (" + file.getName() + ")"); + CustomSkinLoader.logger.warning(e); + } + } + + // The config file does not exist or was broken. + private static Config initConfig() { + List profiles = new ArrayList<>(); + for (ICustomSkinLoaderPlugin plugin : PluginLoader.PLUGINS) { + List defaultProfiles = plugin.getDefaultProfiles(); + if (defaultProfiles != null) { + profiles.addAll(defaultProfiles); + } + } + profiles.sort(Comparator.comparingInt(ICustomSkinLoaderPlugin.IDefaultProfile::getPriority)); + + List loadlist = new ArrayList<>(); + for (ICustomSkinLoaderPlugin.IDefaultProfile profile : profiles) { + SkinSiteProfile ssp = new SkinSiteProfile(); + ssp.name = profile.getName(); + profile.updateSkinSiteProfile(ssp); + loadlist.add(ssp); + } + + Config config = new Config(loadlist); + writeConfig(config, false); + return config; + } + private static void writeConfig(Config config, boolean update){ + String json=CustomSkinLoader.GSON.toJson(config); + if(CustomSkinLoader.CONFIG_FILE.exists()) + CustomSkinLoader.CONFIG_FILE.delete(); + try { + CustomSkinLoader.CONFIG_FILE.createNewFile(); + FileUtils.write(CustomSkinLoader.CONFIG_FILE, json, "UTF-8"); + CustomSkinLoader.logger.info("Successfully "+(update?"update":"create")+" config."); + } catch (Exception e) { + CustomSkinLoader.logger.info("Failed to "+(update?"update":"create")+" config.("+e.toString()+")"); + } + } +} diff --git a/src/main/java/customskinloader/config/SkinSiteProfile.java b/src/main/java/customskinloader/config/SkinSiteProfile.java new file mode 100644 index 0000000..17faa90 --- /dev/null +++ b/src/main/java/customskinloader/config/SkinSiteProfile.java @@ -0,0 +1,44 @@ +package customskinloader.config; + +import java.lang.reflect.Field; + +public class SkinSiteProfile { + //Common + public String name; + public String type; + public String userAgent; + + //Mojang API + public String apiRoot; + public String sessionRoot; + + //Json API + public String root; + + //Legacy + public Boolean checkPNG;//Not suitable for local skin + public String skin; + public String model; + public String cape; + public String elytra; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{ "); + Field[] fields = SkinSiteProfile.class.getDeclaredFields(); + boolean first = true; + for (Field field : fields) { + try { + Object value = field.get(this); + if (value == null) continue; + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append("\"").append(field.getName()).append("\": \"").append(value).append("\""); + } catch (Exception ignored) { } + } + return sb.append(" }").toString(); + } +} diff --git a/src/main/java/customskinloader/fabric/MixinConfigPlugin.java b/src/main/java/customskinloader/fabric/MixinConfigPlugin.java new file mode 100644 index 0000000..78452bc --- /dev/null +++ b/src/main/java/customskinloader/fabric/MixinConfigPlugin.java @@ -0,0 +1,86 @@ +package customskinloader.fabric; + +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.List; +import java.util.Set; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import customskinloader.Logger; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +public class MixinConfigPlugin implements IMixinConfigPlugin { + public static Logger logger = new Logger(new File("./CustomSkinLoader/FabricPlugin.log")); + + private long world_version; + private long protocol_version; + + @Override + public void onLoad(String mixinPackage) { + URL versionJson = this.getClass().getResource("/version.json"); + if (versionJson != null) { + logger.info("\"version.json\": " + versionJson.toString()); + try ( + InputStream is = versionJson.openStream(); + InputStreamReader isr = new InputStreamReader(is) + ) { + JsonObject object = new JsonParser().parse(isr).getAsJsonObject(); + String name = object.get("name").getAsString(); + this.protocol_version = object.get("protocol_version").getAsLong(); + logger.info("MinecraftVersion: {name='" + name + "' + protocol_version='" + this.protocol_version + "'}"); + } catch (Throwable t) { + logger.warning("An exception occurred when reading \"version.json\"!"); + logger.warning(t); + } + } else { + logger.warning("CSL is loading on LF."); + } + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + boolean result = true; + logger.info("target: " + targetClassName + ", mixin: " + mixinClassName + ", result: " + result); + return result; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + // To be compatible with 0.7.11 + public void preApply(String targetClassName, org.spongepowered.asm.lib.tree.ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + // To be compatible with 0.7.11 + public void postApply(String targetClassName, org.spongepowered.asm.lib.tree.ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } +} diff --git a/src/main/java/customskinloader/fake/FakeCapeBuffer.java b/src/main/java/customskinloader/fake/FakeCapeBuffer.java new file mode 100644 index 0000000..9591e9f --- /dev/null +++ b/src/main/java/customskinloader/fake/FakeCapeBuffer.java @@ -0,0 +1,209 @@ +package customskinloader.fake; + +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +import customskinloader.fake.itf.FakeInterfaceManager; +import customskinloader.fake.texture.FakeBufferedImage; +import customskinloader.fake.texture.FakeImage; +import customskinloader.utils.MinecraftUtil; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.Identifier; + +public class FakeCapeBuffer extends FakeSkinBuffer { + private static final Identifier TEXTURE_ELYTRA = new Identifier("textures/entity/elytra.png"); + private static int loadedGlobal = 0; + private static FakeImage elytraImage; + + private static MinecraftClient mc; + + private static FakeImage loadElytra(FakeImage originalImage) { + loadedGlobal++; + try { + InputStream is = FakeInterfaceManager.IResource_getInputStream(FakeInterfaceManager.IResourceManager_getResource(FakeInterfaceManager.Minecraft_getResourceManager(MinecraftClient.getInstance()), TEXTURE_ELYTRA).get()); + if (is != null) { + FakeImage image = originalImage.createImage(is); + if (image.getWidth() % 64 != 0 || image.getHeight() % 32 != 0) { // wtf? + return elytraImage; + } + image = resetImageFormat(image, 22, 0, 46, 22); + return image; + } + } catch (IOException | NoSuchElementException ignored) {} + return null; + } + + private int loaded = 0; + private double ratioX = -1; + private double ratioY = -1; + private Identifier location; + private String type = null; + + public FakeCapeBuffer(Identifier location) { + this.location = location; + } + + public FakeImage parseSkin(FakeImage image) { + // was parseUserSkin + if (image == null) return null; + this.image = image; + + // When resource packs were changed, the elytra image needs to be reloaded, and here will be entered again + if (this.loaded == loadedGlobal) { + elytraImage = loadElytra(image); + } + this.loaded = loadedGlobal; + if (elytraImage != null) { + if (this.ratioX < 0) + this.ratioX = this.image.getWidth() / 64.0D; + if (this.ratioY < 0) + this.ratioY = this.image.getHeight() / 32.0D; + if (this.type == null) + this.type = this.judgeType(); + + if ("cape".equals(this.type)) { + this.image = resetImageFormat(this.image, 0, 0, 22, 17); + this.attachElytra(elytraImage); + if (this.image instanceof FakeBufferedImage) { // before 1.12.2 + this.refreshTexture((FakeBufferedImage) this.image); + } + } + } + return this.image; + } + + /** + * Judge the cape type + * + * @return "elytra" if the cape contains elytra texture, otherwise "cape" + */ + @Override + public String judgeType() { + if (this.image != null && elytraImage != null) { + // If all the pixels in ((22, 0), (45, 21)) is same as background, it means the cape doesn't contain elytra + Predicate predicate = EQU_BG.apply(this.image.getRGBA(this.image.getWidth() - 1, this.image.getHeight() - 1)); + return withElytraPixels((x, y) -> !predicate.test(this.image.getRGBA(x, y)), "elytra", "cape"); + } + return "cape"; + } + + private void attachElytra(FakeImage elytraImage) { + if (this.image != null) { + int capeW = this.image.getWidth(), capeH = this.image.getHeight(); + int elytraW = elytraImage.getWidth(), elytraH = elytraImage.getHeight(); + + // scale cape and elytra to the same size + // cape part ((0, 0), (21, 16)) -> (22 * 17) + if (capeW < elytraW) { + this.image = scaleImage(this.image, true, elytraW / (double) capeW, 1, capeW / 64.0D, capeH / 32.0D, elytraW, capeH, 0, 0, 22, 17); + capeW = elytraW; + } + if (capeH < elytraH) { + this.image = scaleImage(this.image, true, 1, elytraH / (double) capeH, capeW / 64.0D, capeH / 32.0D, capeW, elytraH, 0, 0, 22, 17); + capeH = elytraH; + } + // elytra part ((22, 0), (45, 21)) -> (24 * 22) + if (elytraW < capeW) { + elytraImage = scaleImage(elytraImage, false, capeW / (double) elytraW, 1, elytraW / 64.0D, elytraH / 32.0D, capeW, elytraH, 22, 0, 46, 22); + elytraW = capeW; + } + if (elytraH < capeH) { + elytraImage = scaleImage(elytraImage, false, 1, capeH / (double) elytraH, elytraW / 64.0D, elytraH / 32.0D, elytraW, capeH, 22, 0, 46, 22); + elytraH = capeH; + } + this.ratioX = capeW / 64.0D; + this.ratioY = capeH / 32.0D; + + // Overwrite pixels from elytra to cape + FakeImage finalElytraImage = elytraImage; + withElytraPixels((x, y) -> { + this.image.setRGBA(x, y, finalElytraImage.getRGBA(x, y)); + return false; + }, null, null); + } + } + + /** + * Traverse every elytra pixel + * @param predicate the predicate with x and y + * @param returnValue if the condition flag equals the condition, then return this value + * @param defaultReturnValue otherwise return this value + */ + private R withElytraPixels(BiPredicate predicate, R returnValue, R defaultReturnValue) { + int startX = (int) Math.ceil(22 * ratioX), endX = (int) Math.ceil(46 * ratioX); + int startY = (int) Math.ceil(0 * ratioY), endY = (int) Math.ceil(22 * ratioY); + int excludeX0 = (int) Math.ceil(24 * ratioX), excludeX1 = (int) Math.ceil(44 * ratioX); + int excludeY = (int) Math.ceil(2 * ratioY); + for (int x = startX; x < endX; ++x) { + for (int y = startY; y < endY; ++y) { + if (y < excludeY && (x < excludeX0 || x >= excludeX1)) continue; + if (predicate.test(x, y)) { + return returnValue; + } + } + } + return defaultReturnValue; + } + + // TextureID won't be regenerated when changing resource packs before 1.12.2 + private void refreshTexture(FakeBufferedImage image) { + Object textureObj = MinecraftUtil.getTextureManager().getTexture(this.location); + if (textureObj != null) { + // NOTICE: OptiFine modified the upload process of the texture from ThreadDownloadImageData + // Therefore, it may not be correct to simply copy the vanilla behavior + FakeInterfaceManager.ThreadDownloadImageData_resetNewBufferedImage(textureObj, image.getImage()); + } + } + + // Some cape image doesn't support alpha channel, so reset image format to ARGB + private static FakeImage resetImageFormat(FakeImage image, int startX, int startY, int endX, int endY) { + if (image != null) { + int width = image.getWidth(), height = image.getHeight(); + image = scaleImage(image, true, 1, 1, width / 64.0D, height / 32.0D, width, height, startX, startY, endX, endY); + } + return image; + } + + /** + * Scale image + * @param image the image to scale. + * @param closeOldImage whether close old image + * @param scaleWidth width enlargement ratio + * @param scaleHeight height enlargement ratio + * @param ratioX the ratio of 64 of the old image width + * @param ratioY the ratio of 32 of the old image height + * @param width the width after scaling. + * @param height the height after scaling. + * @param startX the x where start to copy. + * @param startY the y where start to copy. + * @param endX the x where end to copy. + * @param endY the y where end to copy. + * @return the image after scaling. + */ + private static FakeImage scaleImage(FakeImage image, boolean closeOldImage, double scaleWidth, double scaleHeight, double ratioX, double ratioY, int width, int height, int startX, int startY, int endX, int endY) { + FakeImage newImage = image.createImage(width, height); + startX = (int) (startX * ratioX); endX = (int) (endX * ratioX); + startY = (int) (startY * ratioY); endY = (int) (endY * ratioY); + + int x0 = (int) (startX * scaleWidth), x1 = (int) ((startX + 1) * scaleWidth), dx0 = x1 - x0; + for (int x = startX; x < endX; ++x) { + int y0 = (int) (startY * scaleHeight), y1 = (int) ((startY + 1) * scaleHeight), dy0 = y1 - y0; + for (int y = startY; y < endY; ++y) { + int rgba = image.getRGBA(x, y); + for (int dx = 0; dx < dx0; dx++) { + for (int dy = 0; dy < dy0; dy++) { + newImage.setRGBA(x0 + dx, y0 + dy, rgba); + } + } + y0 = y1; y1 = (int) ((y + 2) * scaleHeight); dy0 = y1 - y0; + } + x0 = x1; x1 = (int) ((x + 2) * scaleWidth); dx0 = x1 - x0; + } + if (closeOldImage) + image.close(); + return newImage; + } +} diff --git a/src/main/java/customskinloader/fake/FakeClientPlayer.java b/src/main/java/customskinloader/fake/FakeClientPlayer.java new file mode 100644 index 0000000..aa627dd --- /dev/null +++ b/src/main/java/customskinloader/fake/FakeClientPlayer.java @@ -0,0 +1,89 @@ +package customskinloader.fake; + +import java.util.Map; +import java.util.UUID; + +import com.google.common.collect.Maps; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +import customskinloader.CustomSkinLoader; +import customskinloader.utils.MinecraftUtil; +import net.minecraft.client.texture.PlayerSkinTexture; +import net.minecraft.client.texture.Texture; +import net.minecraft.client.texture.ResourceTexture; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.client.texture.PlayerSkinProvider; +import net.minecraft.client.texture.PlayerSkinProvider.SkinTextureAvailableCallback; +import net.minecraft.util.Identifier; +import net.minecraft.util.ChatUtil; + +public class FakeClientPlayer { + //For Legacy Skin + public static PlayerSkinTexture getDownloadImageSkin(Identifier resourceLocationIn, String username) { + //CustomSkinLoader.logger.debug("FakeClientPlayer/getDownloadImageSkin "+username); + TextureManager textman = MinecraftUtil.getTextureManager(); + Texture ito = textman.getTexture(resourceLocationIn); + + if (ito == null || !(ito instanceof PlayerSkinTexture)) { + //if Legacy Skin for username not loaded yet + PlayerSkinProvider skinman = MinecraftUtil.getSkinManager(); + UUID offlineUUID = getOfflineUUID(username); + GameProfile offlineProfile = new GameProfile(offlineUUID, username); + + //Load Default Skin + Identifier defaultSkin = DefaultSkinHelper.getTexture(offlineUUID); + Texture defaultSkinObj = new ResourceTexture(defaultSkin); + textman.loadTexture(resourceLocationIn, defaultSkinObj); + + //Load Skin from SkinManager + skinman.loadSkin(offlineProfile, (PlayerSkinProvider.SkinTextureAvailableCallback) new LegacyBuffer(resourceLocationIn), false); + } + + if (ito instanceof PlayerSkinTexture) + return (PlayerSkinTexture) ito; + else + return null; + } + + public static Identifier getLocationSkin(String username) { + //CustomSkinLoader.logger.debug("FakeClientPlayer/getLocationSkin "+username); + return new Identifier("skins/legacy-" + ChatUtil.stripTextFormat(username)); + } + + public static UUID getOfflineUUID(String username) { + return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes()); + } + + public static Map textureCache = Maps.newHashMap(); + + public static class LegacyBuffer implements SkinTextureAvailableCallback { + Identifier resourceLocationIn; + boolean loaded = false; + + public LegacyBuffer(Identifier resourceLocationIn) { + CustomSkinLoader.logger.debug("Loading Legacy Texture (" + resourceLocationIn + ")"); + this.resourceLocationIn = resourceLocationIn; + } + + @Override + public void method_7047(Type typeIn, Identifier location, MinecraftProfileTexture profileTexture) { + if (typeIn != Type.SKIN || loaded) + return; + + TextureManager textman = MinecraftUtil.getTextureManager(); + Texture ito = textman.getTexture(location); + if (ito == null) + ito = textureCache.get(location); + if (ito == null) + return; + + loaded = true; + textman.loadTexture(resourceLocationIn, ito); + CustomSkinLoader.logger.debug("Legacy Texture (" + resourceLocationIn + ") Loaded as " + + ito.toString() + " (" + location + ")"); + } + } +} diff --git a/src/main/java/customskinloader/fake/FakeSkinBuffer.java b/src/main/java/customskinloader/fake/FakeSkinBuffer.java new file mode 100644 index 0000000..3c55329 --- /dev/null +++ b/src/main/java/customskinloader/fake/FakeSkinBuffer.java @@ -0,0 +1,175 @@ +package customskinloader.fake; + +import java.awt.image.BufferedImage; +import java.util.function.Function; +import java.util.function.Predicate; + +import customskinloader.CustomSkinLoader; +import customskinloader.fake.texture.FakeBufferedImage; +import customskinloader.fake.texture.FakeImage; +import customskinloader.fake.texture.FakeNativeImage; +import net.minecraft.client.render.BufferedImageSkinProvider; +import net.minecraft.client.texture.NativeImage; + +public class FakeSkinBuffer implements BufferedImageSkinProvider { + private int ratio = 1; + FakeImage image = null; + + //parseUserSkin for 1.15+ + + //parseUserSkin for 1.13+ + + //parseUserSkin for 1.12.2- + public BufferedImage parseSkin(BufferedImage image) { + if (image == null) + return null; + + FakeImage img = parseSkin(new FakeBufferedImage(image)); + if (img instanceof FakeBufferedImage) + return ((FakeBufferedImage) img).getImage(); + + CustomSkinLoader.logger.warning("Failed to parseUserSkin."); + return null; + } + + public FakeImage parseSkin(FakeImage image) { + if (image == null) return null; + this.ratio = image.getWidth() / 64; + + if (image.getHeight() != image.getWidth()) {//Single Layer + //Create a new image and copy origin image data + FakeImage img = image.createImage(64 * ratio, 64 * ratio); + img.copyImageData(image); + image.close(); + image = img; + image.fillArea(0 * ratio, 32 * ratio, 64 * ratio, 32 * ratio); + + //Right Leg -> Left Leg + image.copyArea(4 * ratio, 16 * ratio, 16 * ratio, 32 * ratio, 4 * ratio, 4 * ratio, true, false);//Top + image.copyArea(8 * ratio, 16 * ratio, 16 * ratio, 32 * ratio, 4 * ratio, 4 * ratio, true, false);//Bottom + image.copyArea(0 * ratio, 20 * ratio, 24 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Right + image.copyArea(4 * ratio, 20 * ratio, 16 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Front + image.copyArea(8 * ratio, 20 * ratio, 8 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Left + image.copyArea(12 * ratio, 20 * ratio, 16 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Back + //Right Arm -> Left Arm + image.copyArea(44 * ratio, 16 * ratio, -8 * ratio, 32 * ratio, 4 * ratio, 4 * ratio, true, false);//Top + image.copyArea(48 * ratio, 16 * ratio, -8 * ratio, 32 * ratio, 4 * ratio, 4 * ratio, true, false);//Bottom + image.copyArea(40 * ratio, 20 * ratio, 0 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Right + image.copyArea(44 * ratio, 20 * ratio, -8 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Front + image.copyArea(48 * ratio, 20 * ratio, -16 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Left + image.copyArea(52 * ratio, 20 * ratio, -8 * ratio, 32 * ratio, 4 * ratio, 12 * ratio, true, false);//Back + } + + this.image = image; + setAreaDueToConfig(0 * ratio, 0 * ratio, 32 * ratio, 16 * ratio);//Head - 1 + setAreaTransparent(32 * ratio, 0 * ratio, 64 * ratio, 16 * ratio);//Head - 2 + setAreaDueToConfig(16 * ratio, 16 * ratio, 40 * ratio, 32 * ratio);//Body - 1 + setAreaTransparent(16 * ratio, 32 * ratio, 40 * ratio, 48 * ratio);//Body - 2 + + setAreaDueToConfig(40 * ratio, 16 * ratio, 56 * ratio, 32 * ratio);//Right Arm - 1 + setAreaTransparent(40 * ratio, 32 * ratio, 56 * ratio, 48 * ratio);//Right Arm - 2 + setAreaDueToConfig(0 * ratio, 16 * ratio, 16 * ratio, 32 * ratio);//Right Leg - 1 + setAreaTransparent(0 * ratio, 32 * ratio, 16 * ratio, 48 * ratio);//Right Leg - 2 + + setAreaDueToConfig(32 * ratio, 48 * ratio, 48 * ratio, 64 * ratio);//Left Arm - 1 + setAreaTransparent(48 * ratio, 48 * ratio, 64 * ratio, 64 * ratio);//Left Arm - 2 + setAreaDueToConfig(16 * ratio, 48 * ratio, 32 * ratio, 64 * ratio);//Left Leg - 1 + setAreaTransparent(0 * ratio, 48 * ratio, 16 * ratio, 64 * ratio);//Left Leg - 2 + return image; + } + + /** Judge if the color is transparent or same with background */ + static final Function> EQU_BG = bgColor -> getA(bgColor) == 0 ? (c) -> getA(c) == 0 : (c) -> c.equals(bgColor); + + /** + * Judge the type of skin + * Must be called after parseUserSkin + * + * @return type of skin (slim / default) + * @since 14.9 + */ + public String judgeType() { + if (this.image == null) + return "default"; + int bgColor = image.getRGBA(63 * ratio, 20 * ratio); + /* + * If background is transparent, all the pixels in ((54, 20), (55, 31)) areas is transparent, + * which means it is Alex model. + * If background is opaque, all the pixels in ((54, 20), (55, 31)) areas is same with background, + * which means it is Alex model. + * Otherwise, it is Steve model. + * */ + Predicate predicate = EQU_BG.apply(bgColor); + for (int x = 54 * ratio; x <= 55 * ratio; ++x) { + for (int y = 20 * ratio; y <= 31 * ratio; ++y) { + int color = image.getRGBA(x, y); + if (!predicate.test(color)) { + return "default"; + } + } + } + return "slim"; + } + + /* 2^24-1 + * 00000000 11111111 11111111 11111111 */ + private static final int A = 16777215; + private static final int WHITE = getARGB(255, 255, 255, 255); + private static final int BLACK = getARGB(255, 0, 0, 0); + + private boolean isFilled(int x0, int y0, int x1, int y1) { + int data = image.getRGBA(x0, y0); + if (data != WHITE && data != BLACK) + return false; + for (int x = x0; x < x1; ++x) + for (int y = y0; y < y1; ++y) + if (image.getRGBA(x, y) != data) + return false; + return true; + } + + private void setAreaTransparent(int x0, int y0, int x1, int y1) { + if (!isFilled(x0, y0, x1, y1)) + return; + for (int x = x0; x < x1; ++x) + for (int y = y0; y < y1; ++y) + image.setRGBA(x, y, image.getRGBA(x, y) & A); + } + + /* -2^24 + * 00000001 00000000 00000000 00000000 -> + * 11111110 11111111 11111111 11111111 -> + * 11111111 00000000 00000000 00000000 */ + private static final int B = -16777216; + + private void setAreaOpaque(int x0, int y0, int x1, int y1) { + for (int x = x0; x < x1; ++x) + for (int y = y0; y < y1; ++y) + image.setRGBA(x, y, image.getRGBA(x, y) | B); + } + + private void setAreaDueToConfig(int x0, int y0, int x1, int y1) { + if (customskinloader.CustomSkinLoader.config.enableTransparentSkin) + setAreaTransparent(x0, y0, x1, y1); + else + setAreaOpaque(x0, y0, x1, y1); + } + + @Override + public void setAvailable() { + //A callback when skin loaded, nothing to do + } + + @Override + public void run() { + BufferedImageSkinProvider.super.run(); + } + + private static int getARGB(int a, int r, int g, int b) { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private static int getA(int argb) { + return (argb & B) >>> 24; + } +} diff --git a/src/main/java/customskinloader/fake/FakeSkinManager.java b/src/main/java/customskinloader/fake/FakeSkinManager.java new file mode 100644 index 0000000..1e43e55 --- /dev/null +++ b/src/main/java/customskinloader/fake/FakeSkinManager.java @@ -0,0 +1,153 @@ +package customskinloader.fake; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.google.common.collect.Maps; +import com.google.common.hash.Hashing; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import customskinloader.CustomSkinLoader; +import customskinloader.fake.itf.FakeInterfaceManager; +import customskinloader.utils.HttpTextureUtil; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.BufferedImageSkinProvider; +import net.minecraft.client.texture.PlayerSkinProvider; +import net.minecraft.client.texture.ResourceTexture; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.util.Identifier; + +public class FakeSkinManager { + private final TextureManager textureManager; + + private final Map modelCache = new ConcurrentHashMap<>(); + + public FakeSkinManager(TextureManager textureManagerInstance, File skinCacheDirectory, MinecraftSessionService sessionService) { + this.textureManager = textureManagerInstance; + HttpTextureUtil.defaultCacheDir = skinCacheDirectory; + } + + public Identifier loadSkin(MinecraftProfileTexture profileTexture, MinecraftProfileTexture.Type textureType) { + return this.loadSkin(profileTexture, textureType, null); + } + + public Identifier loadSkin(final MinecraftProfileTexture profileTexture, final MinecraftProfileTexture.Type textureType, final PlayerSkinProvider.SkinTextureAvailableCallback skinAvailableCallback) { + return this.loadSkin(profileTexture, HttpTextureUtil.toHttpTextureInfo(profileTexture.getUrl()), textureType, skinAvailableCallback); + } + + private Identifier loadSkin(final MinecraftProfileTexture profileTexture, final HttpTextureUtil.HttpTextureInfo info, final MinecraftProfileTexture.Type textureType, final PlayerSkinProvider.SkinTextureAvailableCallback skinAvailableCallback) { + final Identifier resourcelocation = new Identifier("skins/" + Hashing.sha1().hashUnencodedChars(info.hash).toString()); + + if (FakeInterfaceManager.TextureManager_getTexture(this.textureManager, resourcelocation, null) != null) {//Have already loaded + makeCallback(skinAvailableCallback, textureType, resourcelocation, modelCache.getOrDefault(resourcelocation, profileTexture)); + } else { + ResourceTexture threaddownloadimagedata = FakeThreadDownloadImageData.createThreadDownloadImageData( + info.cacheFile, + info.url, + DefaultSkinHelper.getTexture(), + new FakeSkinManager.BaseBuffer(skinAvailableCallback, textureType, resourcelocation, profileTexture), + textureType); + if (skinAvailableCallback instanceof FakeClientPlayer.LegacyBuffer)//Cache for client player + FakeClientPlayer.textureCache.put(resourcelocation, threaddownloadimagedata); + FakeInterfaceManager.TextureManager_loadTexture(this.textureManager, resourcelocation, threaddownloadimagedata); + } + return resourcelocation; + } + + public void loadProfileTextures(final GameProfile profile, final PlayerSkinProvider.SkinTextureAvailableCallback skinAvailableCallback, final boolean requireSecure) { + CustomSkinLoader.loadProfileTextures(() -> CustomSkinLoader.loadProfileLazily(profile, m -> { + final Map map = Maps.newHashMap(); + map.putAll(m); + + for (MinecraftProfileTexture.Type type : MinecraftProfileTexture.Type.values()) { + MinecraftProfileTexture profileTexture = map.get(type); + if (profileTexture != null) { + HttpTextureUtil.HttpTextureInfo info = HttpTextureUtil.toHttpTextureInfo(profileTexture.getUrl()); + FakeThreadDownloadImageData.downloadTexture(info.cacheFile, info.url); + + FakeInterfaceManager.Minecraft_addScheduledTask(MinecraftClient.getInstance(), () -> { + CustomSkinLoader.logger.debug("Loading type: " + type); + try { + this.loadSkin(profileTexture, info, type, skinAvailableCallback); + } catch (Throwable t) { + CustomSkinLoader.logger.warning(t); + } + }); + } + } + })); + } + + public Map loadSkinFromCache(GameProfile profile) { + Map map = CustomSkinLoader.loadProfileFromCache(profile); + for (Iterator> it = map.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + MinecraftProfileTexture texture = entry.getValue(); + if (shouldJudgeType(texture)) { + texture = this.modelCache.get(this.loadSkin(texture, entry.getKey())); + if (texture == null) { // remove texture if was not loaded before + it.remove(); + } else { + map.put(entry.getKey(), texture); + } + } + } + return map; + } + + private static void makeCallback(PlayerSkinProvider.SkinTextureAvailableCallback callback, MinecraftProfileTexture.Type type, Identifier location, MinecraftProfileTexture texture) { + if (callback != null) + callback.method_7047(type, location, texture); + } + + private static boolean shouldJudgeType(MinecraftProfileTexture texture) { + return texture != null && "auto".equals(texture.getMetadata("model")); + } + + private class BaseBuffer implements BufferedImageSkinProvider{ + private BufferedImageSkinProvider buffer; + private PlayerSkinProvider.SkinTextureAvailableCallback callback; + private MinecraftProfileTexture.Type type; + private Identifier location; + private MinecraftProfileTexture texture; + + public BaseBuffer(PlayerSkinProvider.SkinTextureAvailableCallback callback, MinecraftProfileTexture.Type type, Identifier location, MinecraftProfileTexture texture) { + switch (type) { + case SKIN: this.buffer = new FakeSkinBuffer(); break; + case CAPE: this.buffer = new FakeCapeBuffer(location); break; + } + + this.callback = callback; + this.type = type; + this.location = location; + this.texture = texture; + } + + + public BufferedImage parseSkin(BufferedImage image) { + return buffer instanceof FakeSkinBuffer ? ((FakeSkinBuffer) buffer).parseSkin(image) : image; + } + + @Override + public void setAvailable() { + if (buffer != null) { + buffer.setAvailable(); + if (shouldJudgeType(texture) && buffer instanceof FakeSkinBuffer) { + //Auto judge skin type + Map metadata = Maps.newHashMap(); + String type = ((FakeSkinBuffer) buffer).judgeType(); + metadata.put("model", type); + texture = new MinecraftProfileTexture(texture.getUrl(), metadata); + FakeSkinManager.this.modelCache.put(location, texture); + } + } + + FakeSkinManager.makeCallback(callback, type, location, this.texture); + } + } +} diff --git a/src/main/java/customskinloader/fake/FakeThreadDownloadImageData.java b/src/main/java/customskinloader/fake/FakeThreadDownloadImageData.java new file mode 100644 index 0000000..bbb1834 --- /dev/null +++ b/src/main/java/customskinloader/fake/FakeThreadDownloadImageData.java @@ -0,0 +1,82 @@ +package customskinloader.fake; + +import java.io.File; +import java.util.EnumMap; +import java.util.function.Supplier; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import customskinloader.CustomSkinLoader; +import customskinloader.utils.HttpRequestUtil; +import net.minecraft.client.render.BufferedImageSkinProvider; +import net.minecraft.client.texture.PlayerSkinTexture; +import net.minecraft.client.texture.ResourceTexture; +import net.minecraft.util.Identifier; + +public class FakeThreadDownloadImageData { + private static IThreadDownloadImageDataBuilder builder; + + public static ResourceTexture createThreadDownloadImageData(File cacheFileIn, String imageUrlIn, Identifier textureResourceLocationIn, BufferedImageSkinProvider imageBufferIn, MinecraftProfileTexture.Type textureTypeIn) { + ResourceTexture texture = null; + if (FakeThreadDownloadImageData.builder == null) { + EnumMap throwables = new EnumMap<>(ThreadDownloadImageDataBuilder.class); + for (ThreadDownloadImageDataBuilder builder : ThreadDownloadImageDataBuilder.values()) { + try { + FakeThreadDownloadImageData.builder = builder.get().get(); + texture = FakeThreadDownloadImageData.builder.build(cacheFileIn, imageUrlIn, textureResourceLocationIn, imageBufferIn, textureTypeIn); + CustomSkinLoader.logger.info("ThreadDownloadImageData Class: %s", texture.getClass().getName()); + break; + } catch (Throwable t) { + throwables.put(builder, t); + } + } + + if (texture == null) { + CustomSkinLoader.logger.warning("Unable to get ThreadDownloadImageData Class: "); + throwables.forEach((k, v) -> { + CustomSkinLoader.logger.warning("Caused by: (%s)", k.name()); + CustomSkinLoader.logger.warning(v); + }); + throw new RuntimeException("Unable to get ThreadDownloadImageData Class!"); + } + } else { + texture = builder.build(cacheFileIn, imageUrlIn, textureResourceLocationIn, imageBufferIn, textureTypeIn); + } + return texture; + } + + public static void downloadTexture(File cacheFile, String imageUrl) { + HttpRequestUtil.HttpRequest request = new HttpRequestUtil.HttpRequest(imageUrl).setLoadContent(false).setCacheTime(0).setCacheFile(cacheFile); + for (int i = 0; i <= CustomSkinLoader.config.retryTime; i++) { + if (i != 0) { + CustomSkinLoader.logger.debug("Retry to download texture %s (%s)", imageUrl, i); + } + if (HttpRequestUtil.makeHttpRequest(request).success) { + break; + } + } + } + + private interface IThreadDownloadImageDataBuilder { + ResourceTexture build(File cacheFile, String imageUrl, Identifier textureResourceLocation, BufferedImageSkinProvider imageBuffer, MinecraftProfileTexture.Type textureType); + } + + private enum ThreadDownloadImageDataBuilder { + // DO NOT replace new IThreadDownloadImageDataBuilder() with lambda. + V1(() -> new IThreadDownloadImageDataBuilder() { // Forge 1.8.x~1.12.x + @Override + public ResourceTexture build(File cacheFile, String imageUrl, Identifier textureResourceLocation, BufferedImageSkinProvider imageBuffer, MinecraftProfileTexture.Type textureType) { + return new PlayerSkinTexture(cacheFile, imageUrl, textureResourceLocation, imageBuffer); + } + }); + + private final Supplier builder; + + ThreadDownloadImageDataBuilder(Supplier builder) { + this.builder = builder; + } + + public Supplier get() { + return this.builder; + } + } +} diff --git a/src/main/java/customskinloader/fake/itf/FakeInterfaceManager.java b/src/main/java/customskinloader/fake/itf/FakeInterfaceManager.java new file mode 100644 index 0000000..48694c8 --- /dev/null +++ b/src/main/java/customskinloader/fake/itf/FakeInterfaceManager.java @@ -0,0 +1,42 @@ +package customskinloader.fake.itf; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.Texture; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; + +public class FakeInterfaceManager { + public static InputStream IResource_getInputStream(Object resource) { + return ((IFakeIResource.V2) resource).open(); + } + + public static Optional IResourceManager_getResource(Object resourceManager, Identifier location) throws IOException { + return ((IFakeIResourceManager) resourceManager).getResource(location); + } + + public static ResourceManager Minecraft_getResourceManager(MinecraftClient minecraft) { + return ((IFakeMinecraft) minecraft).getResourceManager(); + } + + public static void Minecraft_addScheduledTask(MinecraftClient minecraft, Runnable runnable) { + ((IFakeMinecraft) minecraft).execute(runnable); + } + + public static void TextureManager_loadTexture(TextureManager textureManager, Identifier textureLocation, Object textureObj) { + ((IFakeTextureManager.V2) textureManager).loadTexture(textureLocation, (Texture) textureObj); + } + + public static Texture TextureManager_getTexture(TextureManager textureManager, Identifier textureLocation, Object textureObj) { + return ((IFakeTextureManager.V1) textureManager).getTexture(textureLocation, (Texture) textureObj); + } + + public static void ThreadDownloadImageData_resetNewBufferedImage(Object threadDownloadImageData, BufferedImage image) { + ((IFakeThreadDownloadImageData) threadDownloadImageData).resetNewBufferedImage(image); + } +} diff --git a/src/main/java/customskinloader/fake/itf/IFakeIResource.java b/src/main/java/customskinloader/fake/itf/IFakeIResource.java new file mode 100644 index 0000000..96ff67e --- /dev/null +++ b/src/main/java/customskinloader/fake/itf/IFakeIResource.java @@ -0,0 +1,22 @@ +package customskinloader.fake.itf; + +import java.io.InputStream; + +import net.minecraft.resource.Resource; + +/** { net.minecraft(.client).resources.IResource} is no longer an interface since 22w14a */ +public interface IFakeIResource { + // 1.13.2 ~ 22w13a + interface V1 { + default InputStream getInputStream() { + return ((Resource) this).getInputStream(); + } + } + + // 22w14a+ + interface V2 { + default InputStream open() { + return ((IFakeIResource.V1) this).getInputStream(); + } + } +} diff --git a/src/main/java/customskinloader/fake/itf/IFakeIResourceManager.java b/src/main/java/customskinloader/fake/itf/IFakeIResourceManager.java new file mode 100644 index 0000000..f8e8c7f --- /dev/null +++ b/src/main/java/customskinloader/fake/itf/IFakeIResourceManager.java @@ -0,0 +1,20 @@ +package customskinloader.fake.itf; + +import java.io.IOException; +import java.util.Optional; + +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.Resource; +import net.minecraft.util.Identifier; + +public interface IFakeIResourceManager { + // 1.13.2 ~ 22w13a + default Resource func_199002_a(Identifier location) throws IOException { + return (Resource) ((ResourceManager) this).getResource(location); + } + + // 22w14a+ + default Optional getResource(Identifier location) throws IOException { + return Optional.ofNullable(this.func_199002_a(location)); + } +} diff --git a/src/main/java/customskinloader/fake/itf/IFakeMinecraft.java b/src/main/java/customskinloader/fake/itf/IFakeMinecraft.java new file mode 100644 index 0000000..4291349 --- /dev/null +++ b/src/main/java/customskinloader/fake/itf/IFakeMinecraft.java @@ -0,0 +1,15 @@ +package customskinloader.fake.itf; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.resource.ResourceManager; + +public interface IFakeMinecraft { + // 1.13.2+ + default ResourceManager getResourceManager() { + return (ResourceManager) ((MinecraftClient) this).getResourceManager(); + } + // 1.14+ + default void execute(Runnable runnable) { + ((MinecraftClient) this).submit(runnable); + } +} diff --git a/src/main/java/customskinloader/fake/itf/IFakeTextureManager.java b/src/main/java/customskinloader/fake/itf/IFakeTextureManager.java new file mode 100644 index 0000000..98450b4 --- /dev/null +++ b/src/main/java/customskinloader/fake/itf/IFakeTextureManager.java @@ -0,0 +1,27 @@ +package customskinloader.fake.itf; + +import net.minecraft.client.texture.Texture; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.util.Identifier; + +public interface IFakeTextureManager { + interface V1 { + default boolean loadTexture(Identifier textureLocation, Texture textureObj) { + return ((TextureManager) this).loadTexture(textureLocation, (Texture) textureObj); + } + + default Texture getTexture(Identifier textureLocation) { + return (Texture) ((TextureManager) this).getTexture(textureLocation); + } + + default Texture getTexture(Identifier textureLocation, Texture textureObj) { + return getTexture(textureLocation); + } + } + + interface V2 { + default void loadTexture(Identifier textureLocation, Texture textureObj) { + ((IFakeTextureManager.V1) this).loadTexture(textureLocation, textureObj); + } + } +} diff --git a/src/main/java/customskinloader/fake/itf/IFakeThreadDownloadImageData.java b/src/main/java/customskinloader/fake/itf/IFakeThreadDownloadImageData.java new file mode 100644 index 0000000..a5685e7 --- /dev/null +++ b/src/main/java/customskinloader/fake/itf/IFakeThreadDownloadImageData.java @@ -0,0 +1,12 @@ +package customskinloader.fake.itf; + +import java.awt.image.BufferedImage; + +// This interface is only available before 1.12.2 +public interface IFakeThreadDownloadImageData { + /** + * Reset {@link net.minecraft.client.renderer.ThreadDownloadImageData#bufferedImage} and + * {@link net.minecraft.client.renderer.ThreadDownloadImageData#textureUploaded} to false to refresh texture. + */ + void resetNewBufferedImage(BufferedImage image); +} diff --git a/src/main/java/customskinloader/fake/texture/FakeBufferedImage.java b/src/main/java/customskinloader/fake/texture/FakeBufferedImage.java new file mode 100644 index 0000000..0e96498 --- /dev/null +++ b/src/main/java/customskinloader/fake/texture/FakeBufferedImage.java @@ -0,0 +1,75 @@ +package customskinloader.fake.texture; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import javax.imageio.ImageIO; + +public class FakeBufferedImage implements FakeImage { + private BufferedImage image; + private Graphics graphics; + + public FakeBufferedImage(int width, int height) { + this(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)); + } + + public FakeBufferedImage(BufferedImage image) { + this.image = image; + graphics = image.getGraphics(); + } + + public BufferedImage getImage() { + graphics.dispose(); + return image; + } + + public FakeImage createImage(int width, int height) { + return new FakeBufferedImage(width, height); + } + + public FakeImage createImage(InputStream is) throws IOException { + return new FakeBufferedImage(ImageIO.read(is)); + } + + public int getWidth() { + return image.getWidth(); + } + + public int getHeight() { + return image.getHeight(); + } + + public int getRGBA(int x, int y) { + return image.getRGB(x, y); + } + + public void setRGBA(int x, int y, int rgba) { + image.setRGB(x, y, rgba); + } + + public void copyImageData(FakeImage image) { + if (!(image instanceof FakeBufferedImage)) return; + BufferedImage img = ((FakeBufferedImage) image).getImage(); + graphics.drawImage(img, 0, 0, null); + } + + public void fillArea(int x0, int y0, int width, int height) { + graphics.setColor(new Color(0, 0, 0, 0)); + graphics.fillRect(x0, y0, width, height); + } + + public void copyArea(int x0, int y0, int dx, int dy, int width, int height, boolean reversex, boolean reversey) { + int x1 = x0 + width, x2 = x0 + dx, x3 = x2 + width; + int y1 = y0 + height, y2 = y0 + dy, y3 = y2 + height; + graphics.drawImage(image, + reversex ? x3 : x2, reversey ? y3 : y2, + reversex ? x2 : x3, reversey ? y2 : y3, + x0, y0, x1, y1, null); + } + + public void close() { + graphics.dispose(); + } +} diff --git a/src/main/java/customskinloader/fake/texture/FakeImage.java b/src/main/java/customskinloader/fake/texture/FakeImage.java new file mode 100644 index 0000000..7614865 --- /dev/null +++ b/src/main/java/customskinloader/fake/texture/FakeImage.java @@ -0,0 +1,26 @@ +package customskinloader.fake.texture; + +import java.io.IOException; +import java.io.InputStream; + +public interface FakeImage { + FakeImage createImage(int width, int height); + + FakeImage createImage(InputStream is) throws IOException; + + int getWidth(); + + int getHeight(); + + int getRGBA(int x, int y); + + void setRGBA(int x, int y, int rgba); + + void copyImageData(FakeImage image); + + void fillArea(int x0, int y0, int width, int height); + + void copyArea(int x0, int y0, int dx, int dy, int width, int height, boolean reversex, boolean reversey); + + void close(); +} diff --git a/src/main/java/customskinloader/fake/texture/FakeNativeImage.java b/src/main/java/customskinloader/fake/texture/FakeNativeImage.java new file mode 100644 index 0000000..5e2e475 --- /dev/null +++ b/src/main/java/customskinloader/fake/texture/FakeNativeImage.java @@ -0,0 +1,61 @@ +package customskinloader.fake.texture; + +import java.io.InputStream; + +import net.minecraft.client.texture.NativeImage; + +public class FakeNativeImage implements FakeImage { + private NativeImage image; + + public FakeNativeImage(int width, int height) { + this(new NativeImage(width, height, true)); + } + + public FakeNativeImage(NativeImage image) { + this.image = image; + } + + public NativeImage getImage() { + return image; + } + + public FakeImage createImage(int width, int height) { + return new FakeNativeImage(width, height); + } + + public FakeImage createImage(InputStream is) { + return new FakeNativeImage(NativeImage.func_195713_a(is)); + } + + public int getWidth() { + return image.func_195702_a(); + } + + public int getHeight() { + return image.func_195714_b(); + } + + public int getRGBA(int x, int y) { + return image.func_195709_a(x, y); + } + + public void setRGBA(int x, int y, int rgba) { + image.func_195700_a(x, y, rgba); + } + + public void copyImageData(FakeImage image) { + if (!(image instanceof FakeNativeImage)) return; + this.image.func_195703_a(((FakeNativeImage) image).getImage()); + } + + public void fillArea(int x0, int y0, int width, int height) { + image.func_195715_a(x0, y0, width, height, 0); + } + + public void copyArea(int x0, int y0, int dx, int dy, int width, int height, boolean reversex, boolean reversey) { + image.func_195699_a(x0, y0, dx, dy, width, height, reversex, reversey); + } + + public void close() { + } +} diff --git a/src/main/java/customskinloader/loader/JsonAPILoader.java b/src/main/java/customskinloader/loader/JsonAPILoader.java new file mode 100644 index 0000000..968dbd0 --- /dev/null +++ b/src/main/java/customskinloader/loader/JsonAPILoader.java @@ -0,0 +1,138 @@ +package customskinloader.loader; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +import com.mojang.authlib.GameProfile; +import customskinloader.CustomSkinLoader; +import customskinloader.config.SkinSiteProfile; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.UserProfile; +import customskinloader.utils.HttpRequestUtil; +import customskinloader.utils.HttpUtil0; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +public class JsonAPILoader implements ICustomSkinLoaderPlugin, ProfileLoader.IProfileLoader { + + public interface IJsonAPI { + List getDefaultProfiles(JsonAPILoader loader); + + String toJsonUrl(String root, String username); + + String getPayload(SkinSiteProfile ssp); + + UserProfile toUserProfile(String root, String json, boolean local); + + String getName(); + } + + public static class ErrorProfile { + public int errno; + public String msg; + } + + private final IJsonAPI jsonAPI; + + public JsonAPILoader(IJsonAPI jsonAPI) { + this.jsonAPI = jsonAPI; + } + + // === ICustomSkinLoaderPlugin === + + @Override + public ProfileLoader.IProfileLoader getProfileLoader() { + return this; + } + + @Override + public List getDefaultProfiles() { + return this.jsonAPI.getDefaultProfiles(this); + } + + public abstract static class DefaultProfile implements ICustomSkinLoaderPlugin.IDefaultProfile { + protected final JsonAPILoader loader; + + public DefaultProfile(JsonAPILoader loader) { + this.loader = loader; + } + + @Override + public void updateSkinSiteProfile(SkinSiteProfile ssp) { + ssp.type = this.loader.getName(); + ssp.root = this.getRoot(); + } + + public abstract String getRoot(); + } + + // === IProfileLoader === + + @Override + public UserProfile loadProfile(SkinSiteProfile ssp, GameProfile gameProfile) throws Exception { + String username = gameProfile.getName(); + if (StringUtils.isEmpty(ssp.root)) { + CustomSkinLoader.logger.info("Root not defined."); + return null; + } + boolean local = HttpUtil0.isLocal(ssp.root); + String jsonUrl = this.jsonAPI.toJsonUrl(ssp.root, username); + //Some API like MinecraftCapesAPI won't load profile at sometime + if (jsonUrl == null) { + CustomSkinLoader.logger.info("Profile url not found."); + return null; + } + String json; + if (local) { + File jsonFile = new File(CustomSkinLoader.DATA_DIR, jsonUrl); + if (!jsonFile.exists()) { + CustomSkinLoader.logger.info("Profile File not found."); + return null; + } + json = IOUtils.toString(Files.newInputStream(jsonFile.toPath()), StandardCharsets.UTF_8); + } else { + HttpRequestUtil.HttpResponce responce = HttpRequestUtil.makeHttpRequest(new HttpRequestUtil.HttpRequest(jsonUrl).setCacheTime(90).setUserAgent(ssp.userAgent).setPayload(this.jsonAPI.getPayload(ssp))); + json = responce.content; + } + if (json == null || json.equals("")) { + CustomSkinLoader.logger.info("Profile not found."); + return null; + } + + ErrorProfile profile = CustomSkinLoader.GSON.fromJson(json, ErrorProfile.class); + if (profile.errno != 0) { + CustomSkinLoader.logger.info("Error " + profile.errno + ": " + profile.msg); + return null; + } + + UserProfile p = this.jsonAPI.toUserProfile(ssp.root, json, local); + if (p == null || p.isEmpty()) { + CustomSkinLoader.logger.info("Both skin and cape not found."); + return null; + } else { + return p; + } + } + + @Override + public boolean compare(SkinSiteProfile ssp0, SkinSiteProfile ssp1) { + return !StringUtils.isNoneEmpty(ssp0.root) || ssp0.root.equalsIgnoreCase(ssp1.root); + } + + @Override + public String getName() { + return this.jsonAPI.getName(); + } + + @Override + public void init(SkinSiteProfile ssp) { + if (HttpUtil0.isLocal(ssp.root)) { + File f = new File(ssp.root); + if (!f.exists()) + //noinspection ResultOfMethodCallIgnored + f.mkdirs(); + } + } +} diff --git a/src/main/java/customskinloader/loader/LegacyLoader.java b/src/main/java/customskinloader/loader/LegacyLoader.java new file mode 100644 index 0000000..6e78880 --- /dev/null +++ b/src/main/java/customskinloader/loader/LegacyLoader.java @@ -0,0 +1,200 @@ +package customskinloader.loader; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import com.mojang.authlib.GameProfile; +import customskinloader.CustomSkinLoader; +import customskinloader.config.SkinSiteProfile; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.UserProfile; +import customskinloader.utils.HttpRequestUtil; +import customskinloader.utils.HttpTextureUtil; +import customskinloader.utils.HttpUtil0; + +public class LegacyLoader implements ICustomSkinLoaderPlugin, ProfileLoader.IProfileLoader { + + @Override + public ProfileLoader.IProfileLoader getProfileLoader() { + return this; + } + + @Override + public List getDefaultProfiles() { + return Collections.singletonList(new LocalSkin(this)); + } + + public abstract static class DefaultProfile implements ICustomSkinLoaderPlugin.IDefaultProfile { + protected final LegacyLoader loader; + + public DefaultProfile(LegacyLoader loader) { + this.loader = loader; + } + + @Override + public void updateSkinSiteProfile(SkinSiteProfile ssp) { + ssp.type = this.loader.getName(); + if (ssp.checkPNG == null) { + ssp.checkPNG = false; + } + if (ssp.model == null) { + ssp.model = "auto"; + } + //Set texture url when it is empty + //Remote texture url could be changed, it will be auto updated here + if (ssp.skin == null || !HttpUtil0.isLocal(this.getSkinRoot())) { + ssp.skin = this.getSkinRoot(); + } + if (ssp.cape == null || !HttpUtil0.isLocal(this.getCapeRoot())) { + ssp.cape = this.getCapeRoot(); + } + if (ssp.elytra == null || !HttpUtil0.isLocal(this.getElytraRoot())) { + ssp.elytra = this.getElytraRoot(); + } + } + + public abstract String getSkinRoot(); + + public abstract String getCapeRoot(); + + public abstract String getElytraRoot(); + } + + public static class LocalSkin extends LegacyLoader.DefaultProfile { + public LocalSkin(LegacyLoader loader) { + super(loader); + } + + @Override + public String getName() { + return "LocalSkin"; + } + + @Override + public int getPriority() { + return 600; + } + + @Override + public String getSkinRoot() { + return "LocalSkin/skins/{USERNAME}.png"; + } + + @Override + public String getCapeRoot() { + return "LocalSkin/capes/{USERNAME}.png"; + } + + @Override + public String getElytraRoot() { + return "LocalSkin/elytras/{USERNAME}.png"; + } + } + + // // Minecrack could not load skin correctly + // public static class Minecrack extends LegacyLoader.DefaultProfile { + // public Minecrack(LegacyLoader loader) { super(loader); } + // @Override public String getName() { return "Minecrack"; } + // @Override public int getPriority() { return 600; } + // @Override public String getSkinRoot() { return "http://minecrack.fr.nf/mc/skinsminecrackd/{USERNAME}.png"; } + // @Override public String getCapeRoot() { return "http://minecrack.fr.nf/mc/cloaksminecrackd/{USERNAME}.png"; } + // @Override public String getElytraRoot() { return null; } + // } + + public static final String USERNAME_PLACEHOLDER = "{USERNAME}"; + public static final String UUID_PLACEHOLDER = "{UUID}"; + + @Override + public UserProfile loadProfile(SkinSiteProfile ssp, GameProfile gameProfile) { + UserProfile profile = new UserProfile(); + //Try to load all textures + getTextureUrl(ssp, gameProfile, ssp.skin, it -> { + profile.skinUrl = it; + profile.model = ssp.model; + }); + getTextureUrl(ssp, gameProfile, ssp.cape, it -> profile.capeUrl = it); + getTextureUrl(ssp, gameProfile, ssp.elytra, it -> profile.elytraUrl = it); + + if (profile.isEmpty()) { + CustomSkinLoader.logger.info("No texture could be found."); + return null; + } + return profile; + } + + private void getTextureUrl( + SkinSiteProfile ssp, GameProfile gameProfile, String baseUrl, Consumer onSuccess) { + //Base URL is empty + if (baseUrl == null || baseUrl.isEmpty()) { + return; + } + String url = expandURL(baseUrl, gameProfile.getName()); + //No texture can be loaded + if (url == null) { + return; + } + //Local texture file logic + if (HttpUtil0.isLocal(url)) { + File file = new File(CustomSkinLoader.DATA_DIR, url); + if (file.exists() && file.isFile()) { + String fakeUrl = HttpTextureUtil.getLocalLegacyFakeUrl(url, + HttpTextureUtil.getHash(url, file.length(), file.lastModified())); + onSuccess.accept(fakeUrl); + } + return; + } + //Remote texture logic + HttpRequestUtil.HttpResponce responce = HttpRequestUtil.makeHttpRequest(new HttpRequestUtil.HttpRequest(url) + .setUserAgent(ssp.userAgent).setCheckPNG(ssp.checkPNG != null && ssp.checkPNG).setLoadContent(false)); + if (responce.success) { + onSuccess.accept(HttpTextureUtil.getLegacyFakeUrl(url)); + } + } + + @Override + public boolean compare(SkinSiteProfile ssp0, SkinSiteProfile ssp1) { + return Objects.equals(ssp0.skin, ssp1.skin) && Objects.equals(ssp0.cape, ssp1.cape) && + Objects.equals(ssp0.elytra, ssp1.elytra); + } + + @Override + public String getName() { + return "Legacy"; + } + + @Override + public void init(SkinSiteProfile ssp) { + initFolder(ssp.skin); + initFolder(ssp.cape); + initFolder(ssp.elytra); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void initFolder(String target) { + //Only local skin should init folder + if (!HttpUtil0.isLocal(target)) { + return; + } + String file = target.replace(USERNAME_PLACEHOLDER, "init"); + File folder = new File(CustomSkinLoader.DATA_DIR, file).getParentFile(); + if (folder != null && !folder.exists()) { + folder.mkdirs(); + } + } + + private String expandURL(String url, String username) { + String t = url.replace(USERNAME_PLACEHOLDER, username); + if (!t.contains(UUID_PLACEHOLDER)) { + return t; + } + String uuid = MojangAPILoader.getMojangUuidByUsername(username); + //If Mojang uuid not found, won't load the texture + if (uuid == null) { + return null; + } + return t.replace(UUID_PLACEHOLDER, uuid); + } +} diff --git a/src/main/java/customskinloader/loader/MojangAPILoader.java b/src/main/java/customskinloader/loader/MojangAPILoader.java new file mode 100644 index 0000000..0451b25 --- /dev/null +++ b/src/main/java/customskinloader/loader/MojangAPILoader.java @@ -0,0 +1,223 @@ +package customskinloader.loader; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.util.UUIDTypeAdapter; +import customskinloader.CustomSkinLoader; +import customskinloader.config.SkinSiteProfile; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.ModelManager0; +import customskinloader.profile.UserProfile; +import customskinloader.utils.HttpRequestUtil; +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; + +public class MojangAPILoader implements ICustomSkinLoaderPlugin, ProfileLoader.IProfileLoader { + + @Override + public ProfileLoader.IProfileLoader getProfileLoader() { + return this; + } + + @Override + public List getDefaultProfiles() { + return Lists.newArrayList(new Mojang(this)); + } + + public abstract static class DefaultProfile implements ICustomSkinLoaderPlugin.IDefaultProfile { + protected final MojangAPILoader loader; + + public DefaultProfile(MojangAPILoader loader) { + this.loader = loader; + } + + @Override + public void updateSkinSiteProfile(SkinSiteProfile ssp) { + ssp.type = this.loader.getName(); + ssp.apiRoot = this.getAPIRoot(); + ssp.sessionRoot = this.getSessionRoot(); + } + + public abstract String getAPIRoot(); + + public abstract String getSessionRoot(); + } + + public static class Mojang extends MojangAPILoader.DefaultProfile { + public Mojang(MojangAPILoader loader) { + super(loader); + } + + @Override + public String getName() { + return "Mojang"; + } + + @Override + public int getPriority() { + return 100; + } + + @Override + public String getAPIRoot() { + return getMojangApiRoot(); + } + + @Override + public String getSessionRoot() { + return getMojangSessionRoot(); + } + } + + @Override + public UserProfile loadProfile(SkinSiteProfile ssp, GameProfile gameProfile) { + Map map = getTextures(gameProfile); + if (!map.isEmpty()) { + CustomSkinLoader.logger.info("Default profile will be used."); + return ModelManager0.toUserProfile(map); + } + String username = gameProfile.getName(); + GameProfile newGameProfile = loadGameProfile(ssp.apiRoot, username); + if (newGameProfile == null) { + CustomSkinLoader.logger.info("Profile not found.(" + username + "'s profile not found.)"); + return null; + } + newGameProfile = fillGameProfile(ssp.sessionRoot, newGameProfile); + map = getTextures(newGameProfile); + if (!map.isEmpty()) { + gameProfile.getProperties().putAll(newGameProfile.getProperties()); + return ModelManager0.toUserProfile(map); + } + CustomSkinLoader.logger.info("Profile not found.(" + username + " doesn't have skin/cape.)"); + return null; + } + + //Username -> UUID + public static GameProfile loadGameProfile(String apiRoot, String username) { + //Doc (https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs) + Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + + HttpRequestUtil.HttpResponce responce = HttpRequestUtil.makeHttpRequest( + new HttpRequestUtil.HttpRequest(apiRoot + "profiles/minecraft") + .setCacheTime(600).setPayload(gson.toJson(Collections.singletonList(username))) + ); + if (StringUtils.isEmpty(responce.content)) { + return null; + } + + GameProfile[] profiles = gson.fromJson(responce.content, GameProfile[].class); + if (profiles.length == 0) { + return null; + } + GameProfile gameProfile = profiles[0]; + + if (gameProfile.getId() == null) { + return null; + } + return new GameProfile(gameProfile.getId(), gameProfile.getName()); + } + + /** + * Get Mojang UUID by username. + * + * @param username username to query + * @return UUID in Mojang API style string. Returns {@code null} if username not found in Mojang API. + */ + public static String getMojangUuidByUsername(String username) { + GameProfile profile = loadGameProfile(getMojangApiRoot(), username); + if (profile == null) { + return null; + } + return UUIDTypeAdapter.fromUUID(profile.getId()); + } + + //UUID -> Profile + public static GameProfile fillGameProfile(String sessionRoot, GameProfile profile) { + //Doc (http://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape) + HttpRequestUtil.HttpResponce responce = HttpRequestUtil.makeHttpRequest( + new HttpRequestUtil.HttpRequest(sessionRoot + "session/minecraft/profile/" + + UUIDTypeAdapter.fromUUID(profile.getId())).setCacheTime(90)); + if (StringUtils.isEmpty(responce.content)) { + return profile; + } + + Gson gson = new GsonBuilder() + .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) + .registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()) + .create(); + MinecraftProfilePropertiesResponse propertiesResponce = gson.fromJson(responce.content, MinecraftProfilePropertiesResponse.class); + GameProfile newGameProfile = new GameProfile(propertiesResponce.getId(), propertiesResponce.getName()); + newGameProfile.getProperties().putAll(propertiesResponce.getProperties()); + + return newGameProfile; + } + + public static Map getTextures(GameProfile gameProfile) { + if (gameProfile == null) { + return Maps.newHashMap(); + } + Property textureProperty = Iterables.getFirst(gameProfile.getProperties().get("textures"), null); + if (textureProperty == null) { + return Maps.newHashMap(); + } + String value = textureProperty.getValue(); + if (StringUtils.isBlank(value)) { + return Maps.newHashMap(); + } + String json = new String(Base64.decodeBase64(value), StandardCharsets.UTF_8); + Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + MinecraftTexturesPayload result = gson.fromJson(json, MinecraftTexturesPayload.class); + if (result == null || result.getTextures() == null) { + return Maps.newHashMap(); + } + return result.getTextures(); + } + + @Override + public boolean compare(SkinSiteProfile ssp0, SkinSiteProfile ssp1) { + return (!StringUtils.isNoneEmpty(ssp0.apiRoot) || ssp0.apiRoot.equalsIgnoreCase(ssp1.apiRoot)) || + (!StringUtils.isNoneEmpty(ssp0.sessionRoot) || ssp0.sessionRoot.equalsIgnoreCase(ssp1.sessionRoot)); + } + + @Override + public String getName() { + return "MojangAPI"; + } + + @Override + public void init(SkinSiteProfile ssp) { + //Init default api & session root for Mojang API + if (ssp.apiRoot == null) + ssp.apiRoot = getMojangApiRoot(); + if (ssp.sessionRoot == null) + ssp.sessionRoot = getMojangSessionRoot(); + } + + // Prevent authlib-injector (https://github.com/yushijinhun/authlib-injector) from modifying these strings + private static final String MOJANG_API_ROOT = "https://api{DO_NOT_MODIFY}.mojang.com/"; + private static final String MOJANG_SESSION_ROOT = "https://sessionserver{DO_NOT_MODIFY}.mojang.com/"; + + public static String getMojangApiRoot() { + return MOJANG_API_ROOT.replace("{DO_NOT_MODIFY}", ""); + } + + public static String getMojangSessionRoot() { + return MOJANG_SESSION_ROOT.replace("{DO_NOT_MODIFY}", ""); + } +} diff --git a/src/main/java/customskinloader/loader/ProfileLoader.java b/src/main/java/customskinloader/loader/ProfileLoader.java new file mode 100644 index 0000000..009c52f --- /dev/null +++ b/src/main/java/customskinloader/loader/ProfileLoader.java @@ -0,0 +1,33 @@ +package customskinloader.loader; + +import java.util.HashMap; + +import com.mojang.authlib.GameProfile; +import customskinloader.CustomSkinLoader; +import customskinloader.config.SkinSiteProfile; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.plugin.PluginLoader; +import customskinloader.profile.UserProfile; + +public class ProfileLoader { + public static final HashMap LOADERS = initLoaders(); + + private static HashMap initLoaders() { + HashMap profileLoaders = new HashMap<>(); + for (ICustomSkinLoaderPlugin plugin : PluginLoader.PLUGINS) { + ProfileLoader.IProfileLoader profileLoader = plugin.getProfileLoader(); + if (profileLoader != null) { + profileLoaders.put(profileLoader.getName().toLowerCase(), profileLoader); + CustomSkinLoader.logger.info("Add a profile loader: " + profileLoader.getName()); + } + } + return profileLoaders; + } + + public interface IProfileLoader { + UserProfile loadProfile(SkinSiteProfile ssp,GameProfile gameProfile) throws Exception; + boolean compare(SkinSiteProfile ssp0,SkinSiteProfile ssp1); + String getName(); + void init(SkinSiteProfile ssp); + } +} diff --git a/src/main/java/customskinloader/loader/jsonapi/CustomSkinAPI.java b/src/main/java/customskinloader/loader/jsonapi/CustomSkinAPI.java new file mode 100644 index 0000000..5b80412 --- /dev/null +++ b/src/main/java/customskinloader/loader/jsonapi/CustomSkinAPI.java @@ -0,0 +1,119 @@ +package customskinloader.loader.jsonapi; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Lists; +import customskinloader.CustomSkinLoader; +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.JsonAPILoader; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.ModelManager0; +import customskinloader.profile.UserProfile; +import customskinloader.utils.HttpTextureUtil; +import org.apache.commons.lang3.StringUtils; + +public class CustomSkinAPI implements JsonAPILoader.IJsonAPI { + + public static class LittleSkin extends JsonAPILoader.DefaultProfile { + public LittleSkin(JsonAPILoader loader) { super(loader); } + @Override public String getName() { return "LittleSkin"; } + @Override public int getPriority() { return 200; } + @Override public String getRoot() { return "https://littleskin.cn/csl/"; } + } + + public static class BlessingSkin extends JsonAPILoader.DefaultProfile { + public BlessingSkin(JsonAPILoader loader) { super(loader); } + @Override public String getName() { return "BlessingSkin"; } + @Override public int getPriority() { return 300; } + @Override public String getRoot() { return "https://skin.prinzeugen.net/"; } + } + + // // OneSkin has been removed temporarily + // public static class OneSkin extends JsonAPILoader.DefaultProfile { + // public OneSkin(JsonAPILoader loader) { super(loader); } + // @Override public String getName() { return "OneSkin"; } + // @Override public int getPriority() { return 500; } + // @Override public String getRoot() { return "http://fleey.cn/skin/skin_user/skin_json.php/"; } + // } + + private static final String TEXTURES="textures/"; + private static final String SUFFIX=".json"; + + @Override + public List getDefaultProfiles(JsonAPILoader loader) { + return Lists.newArrayList(new LittleSkin(loader), new BlessingSkin(loader)); + } + + @Override + public String toJsonUrl(String root, String username) { + return root + username + SUFFIX; + } + + @Override + public UserProfile toUserProfile(String root, String json, boolean local) { + CustomSkinAPIProfile profile=CustomSkinLoader.GSON.fromJson(json, CustomSkinAPIProfile.class); + UserProfile p=new UserProfile(); + + if(StringUtils.isNotBlank(profile.skin)){ + p.skinUrl=root+TEXTURES+profile.skin; + if(local) + p.skinUrl=HttpTextureUtil.getLocalFakeUrl(p.skinUrl); + } + if(StringUtils.isNotBlank(profile.cape)){ + p.capeUrl=root+TEXTURES+profile.cape; + if(local) + p.capeUrl=HttpTextureUtil.getLocalFakeUrl(p.capeUrl); + } + if(StringUtils.isNotBlank(profile.elytra)){ + p.elytraUrl=root+TEXTURES+profile.elytra; + if(local) + p.elytraUrl=HttpTextureUtil.getLocalFakeUrl(p.elytraUrl); + } + + Map textures=new LinkedHashMap<>(); + if(profile.skins!=null) + textures.putAll(profile.skins); + if(profile.textures!=null) + textures.putAll(profile.textures); + if(textures.isEmpty()) + return p; + + boolean hasSkin=false; + for(String model:textures.keySet()){ + ModelManager0.Model enumModel=ModelManager0.getEnumModel(model); + if(enumModel==null||StringUtils.isEmpty(textures.get(model))) + continue; + if(ModelManager0.isSkin(enumModel)) + if(hasSkin) + continue; + else + hasSkin=true; + String url=root+TEXTURES+textures.get(model); + if(local) + url=HttpTextureUtil.getLocalFakeUrl(url); + p.put(enumModel, url); + } + + return p; + } + private static class CustomSkinAPIProfile{ + public String username; + public LinkedHashMap textures; + + public LinkedHashMap skins; + + public String skin; + public String cape; + public String elytra; + } + @Override + public String getPayload(SkinSiteProfile ssp) { + return null; + } + @Override + public String getName() { + return "CustomSkinAPI"; + } +} diff --git a/src/main/java/customskinloader/loader/jsonapi/CustomSkinAPIPlus.java b/src/main/java/customskinloader/loader/jsonapi/CustomSkinAPIPlus.java new file mode 100644 index 0000000..6c7f7d3 --- /dev/null +++ b/src/main/java/customskinloader/loader/jsonapi/CustomSkinAPIPlus.java @@ -0,0 +1,65 @@ +package customskinloader.loader.jsonapi; + +import java.io.File; +import java.util.List; +import java.util.UUID; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import customskinloader.CustomSkinLoader; +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.JsonAPILoader; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.utils.MinecraftUtil; +import org.apache.commons.io.FileUtils; + +public class CustomSkinAPIPlus extends CustomSkinAPI { + + private static String clientID=null; + public CustomSkinAPIPlus(){ + File clientIDFile=new File(CustomSkinLoader.DATA_DIR,"CustomSkinAPIPlus-ClientID"); + + if(clientIDFile.isFile()) + try{ + clientID=FileUtils.readFileToString(clientIDFile, "UTF-8"); + }catch(Exception e){ + e.printStackTrace(); + } + if(clientID==null){ + clientID=UUID.randomUUID().toString(); + try { + FileUtils.write(clientIDFile, clientID, "UTF-8"); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public List getDefaultProfiles(JsonAPILoader loader) { + return Lists.newArrayList(); + } + + @Override + public String getPayload(SkinSiteProfile ssp) { + return new Gson().toJson(new CustomSkinAPIPlusPayload()); + } + + @Override + public String getName() { + return "CustomSKinAPIPlus"; + } + + public static class CustomSkinAPIPlusPayload{ + String gameVersion;//minecraft version + String modVersion;//mod version + String serverAddress;//ip:port + String clientID;//Minecraft Client ID + CustomSkinAPIPlusPayload(){ + gameVersion = MinecraftUtil.getMinecraftMainVersion(); + modVersion = CustomSkinLoader.CustomSkinLoader_VERSION; + serverAddress = MinecraftUtil.isLanServer() ? null : MinecraftUtil.getStandardServerAddress(); + clientID = CustomSkinAPIPlus.clientID; + } + } +} diff --git a/src/main/java/customskinloader/loader/jsonapi/ElyByAPI.java b/src/main/java/customskinloader/loader/jsonapi/ElyByAPI.java new file mode 100644 index 0000000..b0d13c4 --- /dev/null +++ b/src/main/java/customskinloader/loader/jsonapi/ElyByAPI.java @@ -0,0 +1,57 @@ +package customskinloader.loader.jsonapi; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.JsonAPILoader; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.ModelManager0; +import customskinloader.profile.UserProfile; + +public class ElyByAPI implements JsonAPILoader.IJsonAPI { + + public static class ElyBy extends JsonAPILoader.DefaultProfile { + public ElyBy(JsonAPILoader loader) { super(loader); } + @Override public String getName() { return "ElyBy"; } + @Override public int getPriority() { return 400; } + @Override public String getRoot() { return "http://skinsystem.ely.by/textures/"; } + } + + public static class TLauncher extends JsonAPILoader.DefaultProfile { + public TLauncher(JsonAPILoader loader) { super(loader); } + @Override public String getName() { return "TLauncher"; } + @Override public int getPriority() { return 550; } + @Override public String getRoot() { return "https://auth.tlauncher.org/skin/profile/texture/login/"; } + } + + @Override + public List getDefaultProfiles(JsonAPILoader loader) { + return Lists.newArrayList(new ElyBy(loader), new TLauncher(loader)); + } + + @Override + public String toJsonUrl(String root, String username) { + return root + username; + } + + @Override + public UserProfile toUserProfile(String root, String json, boolean local) { + Map result = new Gson().fromJson(json, new TypeToken>() { }.getType()); + return ModelManager0.toUserProfile(result); + } + + @Override + public String getPayload(SkinSiteProfile ssp) { + return null; + } + + @Override + public String getName() { + return "ElyByAPI"; + } +} diff --git a/src/main/java/customskinloader/loader/jsonapi/GlitchlessAPI.java b/src/main/java/customskinloader/loader/jsonapi/GlitchlessAPI.java new file mode 100644 index 0000000..c5d0e0f --- /dev/null +++ b/src/main/java/customskinloader/loader/jsonapi/GlitchlessAPI.java @@ -0,0 +1,56 @@ +package customskinloader.loader.jsonapi; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.JsonAPILoader; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.ModelManager0; +import customskinloader.profile.UserProfile; + +public class GlitchlessAPI implements JsonAPILoader.IJsonAPI { + + public static class GlitchlessGames extends JsonAPILoader.DefaultProfile { + public GlitchlessGames(JsonAPILoader loader) { super(loader); } + @Override public String getName() { return "GlitchlessGames"; } + @Override public int getPriority() { return 700; } + @Override public String getRoot() { return "https://games.glitchless.ru/api/minecraft/users/profiles/textures/?nickname="; } + } + + @Override + public List getDefaultProfiles(JsonAPILoader loader) { + return Lists.newArrayList(new GlitchlessGames(loader)); + } + + @Override + public String toJsonUrl(String root, String username) { + return root + username; + } + + @Override + public UserProfile toUserProfile(String root, String json, boolean local) { + GlitchlessApiResponse result = new Gson().fromJson(json, GlitchlessApiResponse.class); + if (!result.textures.containsKey(MinecraftProfileTexture.Type.SKIN)) + return null; + + return ModelManager0.toUserProfile(result.textures); + } + + @Override + public String getPayload(SkinSiteProfile ssp) { + return null; + } + + @Override + public String getName() { + return "GlitchlessAPI"; + } + + public static class GlitchlessApiResponse { + protected Map textures; + } +} diff --git a/src/main/java/customskinloader/loader/jsonapi/MinecraftCapesAPI.java b/src/main/java/customskinloader/loader/jsonapi/MinecraftCapesAPI.java new file mode 100644 index 0000000..aa94c6a --- /dev/null +++ b/src/main/java/customskinloader/loader/jsonapi/MinecraftCapesAPI.java @@ -0,0 +1,110 @@ +package customskinloader.loader.jsonapi; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import customskinloader.CustomSkinLoader; +import customskinloader.Logger; +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.JsonAPILoader; +import customskinloader.loader.MojangAPILoader; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.ModelManager0; +import customskinloader.profile.UserProfile; +import customskinloader.utils.HttpTextureUtil; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.util.List; +import java.util.Map; + +public class MinecraftCapesAPI implements JsonAPILoader.IJsonAPI { + + public static class MinecraftCapes extends JsonAPILoader.DefaultProfile { + public MinecraftCapes(JsonAPILoader loader) { + super(loader); + } + + @Override + public String getName() { + return "MinecraftCapes"; + } + + @Override + public int getPriority() { + return 800; + } + + @Override + public String getRoot() { + return "https://minecraftcapes.net/profile/"; + } + } + + @Override + public List getDefaultProfiles(JsonAPILoader loader) { + return Lists.newArrayList(new MinecraftCapes(loader)); + } + + @Override + public String toJsonUrl(String root, String username) { + String uuid = MojangAPILoader.getMojangUuidByUsername(username); + //If uuid cannot be found, we won't load profile in this API. + if (uuid == null) { + return null; + } + //API url is `${root}${uuid}` + return root + uuid; + } + + @Override + public UserProfile toUserProfile(String root, String json, boolean local) { + MinecraftCapesApiResponse result = new Gson().fromJson(json, MinecraftCapesApiResponse.class); + if (result.textures == null || result.textures.cape == null) { + return null; + } + + String capeBase64 = result.textures.cape; + byte[] capeBytes = Base64.decodeBase64(capeBase64); + String hash = HttpTextureUtil.getHash(capeBytes); + File cacheFile = HttpTextureUtil.getCacheFile(hash); + String fakeUrl = HttpTextureUtil.getBase64FakeUrl(hash); + + //Save base64 image to cache file + try { + FileUtils.writeByteArrayToFile(cacheFile, capeBytes); + CustomSkinLoader.logger.info("Saved base64 image to " + cacheFile); + } catch (Exception e) { + CustomSkinLoader.logger.warning("Error parsing base64 image: " + capeBase64); + return null; + } + + UserProfile profile = new UserProfile(); + profile.capeUrl = fakeUrl; + + return profile; + } + + @Override + public String getPayload(SkinSiteProfile ssp) { + return null; + } + + @Override + public String getName() { + return "MinecraftCapesAPI"; + } + + public static class MinecraftCapesApiResponse { + public boolean animatedCape; + public boolean capeGlint; + public boolean upsideDown; + public MinecraftCapesApiTexture textures; + + public static class MinecraftCapesApiTexture { + public String cape; + public String ears; + } + } +} diff --git a/src/main/java/customskinloader/loader/jsonapi/UniSkinAPI.java b/src/main/java/customskinloader/loader/jsonapi/UniSkinAPI.java new file mode 100644 index 0000000..39dca42 --- /dev/null +++ b/src/main/java/customskinloader/loader/jsonapi/UniSkinAPI.java @@ -0,0 +1,93 @@ +package customskinloader.loader.jsonapi; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Lists; +import customskinloader.CustomSkinLoader; +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.JsonAPILoader; +import customskinloader.plugin.ICustomSkinLoaderPlugin; +import customskinloader.profile.ModelManager0; +import customskinloader.profile.UserProfile; +import customskinloader.utils.HttpTextureUtil; +import org.apache.commons.lang3.StringUtils; + +public class UniSkinAPI implements JsonAPILoader.IJsonAPI { + + public static class SkinMe extends JsonAPILoader.DefaultProfile { + public SkinMe(JsonAPILoader loader) { super(loader); } + @Override public String getName() { return "SkinMe"; } + @Override public int getPriority() { return 500; } + @Override public String getRoot() { return "http://www.skinme.cc/uniskin/"; } + } + + private static final String TEXTURES="textures/"; + private static final String SUFFIX=".json"; + + @Override + public List getDefaultProfiles(JsonAPILoader loader) { + return Lists.newArrayList(new SkinMe(loader)); + } + + @Override + public String toJsonUrl(String root, String username) { + return root + username + SUFFIX; + } + + @Override + public UserProfile toUserProfile(String root, String json, boolean local) { + UniSkinAPIProfile profile=CustomSkinLoader.GSON.fromJson(json, UniSkinAPIProfile.class); + UserProfile p=new UserProfile(); + + if(StringUtils.isNotBlank(profile.cape)){ + p.capeUrl=root+TEXTURES+profile.cape; + if(local) + p.capeUrl=HttpTextureUtil.getLocalFakeUrl(p.capeUrl); + } + + if(profile.skins==null||profile.skins.isEmpty()) + return p; + if(profile.model_preference==null||profile.model_preference.isEmpty()) + return p; + + boolean hasSkin=false; + for(String model:profile.model_preference){ + ModelManager0.Model enumModel=ModelManager0.getEnumModel(model); + if(enumModel==null||StringUtils.isEmpty(profile.skins.get(model))) + continue; + if(ModelManager0.isSkin(enumModel)) + if(hasSkin) + continue; + else + hasSkin=true; + String url=root+TEXTURES+profile.skins.get(model); + if(local) + url=HttpTextureUtil.getLocalFakeUrl(url); + p.put(enumModel, url); + } + + return p; + } + /** + * Json profile for UniSkinAPI + * Source Code: https://github.com/RecursiveG/UniSkinMod/blob/master/src/main/java/org/devinprogress/uniskinmod/UniSkinApiProfile.java#L18-L22 + * @author RecursiveG + */ + private class UniSkinAPIProfile{ + public String player_name; + public long last_update; + public List model_preference; + public Map skins; + + public String cape; + } + @Override + public String getPayload(SkinSiteProfile ssp) { + return null; + } + @Override + public String getName() { + return "UniSkinAPI"; + } +} diff --git a/src/main/java/customskinloader/mixin/MixinGuiPlayerTabOverlay.java b/src/main/java/customskinloader/mixin/MixinGuiPlayerTabOverlay.java new file mode 100644 index 0000000..2619064 --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinGuiPlayerTabOverlay.java @@ -0,0 +1,24 @@ +package customskinloader.mixin; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.hud.PlayerListHud; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(PlayerListHud.class) +@SuppressWarnings("target") +public abstract class MixinGuiPlayerTabOverlay { + @Redirect( + method = { + "render(ILnet/minecraft/scoreboard/Scoreboard;Lnet/minecraft/scoreboard/ScoreboardObjective;)V", + }, + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/MinecraftClient;isIntegratedServerRunning()Z" + ) + ) + private boolean redirect_renderPlayerlist(MinecraftClient mc) { + return true; + } +} diff --git a/src/main/java/customskinloader/mixin/MixinIResource.java b/src/main/java/customskinloader/mixin/MixinIResource.java new file mode 100644 index 0000000..0e80b50 --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinIResource.java @@ -0,0 +1,10 @@ +package customskinloader.mixin; + +import customskinloader.fake.itf.IFakeIResource; +import net.minecraft.resource.Resource; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(Resource.class) +public interface MixinIResource extends IFakeIResource.V1, IFakeIResource.V2 { + +} diff --git a/src/main/java/customskinloader/mixin/MixinIResourceManager.java b/src/main/java/customskinloader/mixin/MixinIResourceManager.java new file mode 100644 index 0000000..8bafe8a --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinIResourceManager.java @@ -0,0 +1,9 @@ +package customskinloader.mixin; + +import customskinloader.fake.itf.IFakeIResourceManager; +import net.minecraft.resource.ResourceManager; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(ResourceManager.class) +public interface MixinIResourceManager extends IFakeIResourceManager { +} diff --git a/src/main/java/customskinloader/mixin/MixinMinecraft.java b/src/main/java/customskinloader/mixin/MixinMinecraft.java new file mode 100644 index 0000000..8960dfe --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinMinecraft.java @@ -0,0 +1,10 @@ +package customskinloader.mixin; + +import customskinloader.fake.itf.IFakeMinecraft; +import net.minecraft.client.MinecraftClient; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(MinecraftClient.class) +public abstract class MixinMinecraft implements IFakeMinecraft { + +} diff --git a/src/main/java/customskinloader/mixin/MixinPlayerMenuObject.java b/src/main/java/customskinloader/mixin/MixinPlayerMenuObject.java new file mode 100644 index 0000000..c83e563 --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinPlayerMenuObject.java @@ -0,0 +1,34 @@ +package customskinloader.mixin; + +import customskinloader.fake.FakeClientPlayer; +import net.minecraft.client.gui.hud.spectator.TeleportToSpecificPlayerSpectatorCommand; +import net.minecraft.client.texture.PlayerSkinTexture; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(TeleportToSpecificPlayerSpectatorCommand.class) +public abstract class MixinPlayerMenuObject { + @Redirect( + method = "(Lcom/mojang/authlib/GameProfile;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/network/AbstractClientPlayerEntity;getSkinId(Ljava/lang/String;)Lnet/minecraft/util/Identifier;" + ) + ) + private Identifier redirect_init(String username) { + return FakeClientPlayer.getLocationSkin(username); + } + + @Redirect( + method = "(Lcom/mojang/authlib/GameProfile;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/network/AbstractClientPlayerEntity;loadSkin(Lnet/minecraft/util/Identifier;Ljava/lang/String;)Lnet/minecraft/client/texture/PlayerSkinTexture;" + ) + ) + private PlayerSkinTexture redirect_init(Identifier resourceLocationIn, String username) { + return FakeClientPlayer.getDownloadImageSkin(resourceLocationIn, username); + } +} diff --git a/src/main/java/customskinloader/mixin/MixinSkinManager.java b/src/main/java/customskinloader/mixin/MixinSkinManager.java new file mode 100644 index 0000000..1dee84f --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinSkinManager.java @@ -0,0 +1,70 @@ +package customskinloader.mixin; + +import java.io.File; +import java.util.Map; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import customskinloader.fake.FakeSkinManager; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.client.texture.PlayerSkinProvider; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(PlayerSkinProvider.class) +public abstract class MixinSkinManager { + private FakeSkinManager fakeManager; + + @Inject( + method = "(Lnet/minecraft/client/texture/TextureManager;Ljava/io/File;Lcom/mojang/authlib/minecraft/MinecraftSessionService;)V", + at = @At("RETURN") + ) + private void inject_init( + TextureManager textureManagerInstance, + File skinCacheDirectory, + MinecraftSessionService sessionService, + CallbackInfo callbackInfo) { + this.fakeManager = new FakeSkinManager(textureManagerInstance, skinCacheDirectory, sessionService); + } + + @Inject( + method = "loadSkin(Lcom/mojang/authlib/minecraft/MinecraftProfileTexture;Lcom/mojang/authlib/minecraft/MinecraftProfileTexture$Type;Lnet/minecraft/client/texture/PlayerSkinProvider$SkinTextureAvailableCallback;)Lnet/minecraft/util/Identifier;", + at = @At("HEAD"), + cancellable = true + ) + private void inject_loadSkin( + MinecraftProfileTexture profileTexture, + MinecraftProfileTexture.Type textureType, + PlayerSkinProvider.SkinTextureAvailableCallback skinAvailableCallback, + CallbackInfoReturnable callbackInfoReturnable) { + callbackInfoReturnable.setReturnValue(this.fakeManager.loadSkin(profileTexture, textureType, skinAvailableCallback)); + } + + @Inject( + method = "loadSkin(Lcom/mojang/authlib/GameProfile;Lnet/minecraft/client/texture/PlayerSkinProvider$SkinTextureAvailableCallback;Z)V", + at = @At("HEAD"), + cancellable = true + ) + private void inject_loadProfileTextures( + GameProfile profile, + PlayerSkinProvider.SkinTextureAvailableCallback skinAvailableCallback, + boolean requireSecure, + CallbackInfo callbackInfo) { + this.fakeManager.loadProfileTextures(profile, skinAvailableCallback, requireSecure); + callbackInfo.cancel(); + } + + @Inject( + method = "getTextures", + at = @At("HEAD"), + cancellable = true + ) + private void inject_loadSkinFromCache(GameProfile profile, CallbackInfoReturnable> callbackInfoReturnable) { + callbackInfoReturnable.setReturnValue(this.fakeManager.loadSkinFromCache(profile)); + } +} diff --git a/src/main/java/customskinloader/mixin/MixinTextureManager.java b/src/main/java/customskinloader/mixin/MixinTextureManager.java new file mode 100644 index 0000000..c81da64 --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinTextureManager.java @@ -0,0 +1,22 @@ +package customskinloader.mixin; + +import customskinloader.fake.itf.IFakeTextureManager; +import net.minecraft.client.texture.TextureManager; +import org.spongepowered.asm.mixin.Implements; +import org.spongepowered.asm.mixin.Interface; +import org.spongepowered.asm.mixin.Mixin; + +@Implements({ + @Interface( + iface = IFakeTextureManager.V1.class, + prefix = "fake1$" + ), + @Interface( + iface = IFakeTextureManager.V2.class, + prefix = "fake2$" + ), +}) +@Mixin(TextureManager.class) +public abstract class MixinTextureManager { + +} diff --git a/src/main/java/customskinloader/mixin/MixinThreadDownloadImageDataV1.java b/src/main/java/customskinloader/mixin/MixinThreadDownloadImageDataV1.java new file mode 100644 index 0000000..eabf44b --- /dev/null +++ b/src/main/java/customskinloader/mixin/MixinThreadDownloadImageDataV1.java @@ -0,0 +1,23 @@ +package customskinloader.mixin; + +import java.awt.image.BufferedImage; + +import customskinloader.fake.itf.IFakeThreadDownloadImageData; +import net.minecraft.client.texture.PlayerSkinTexture; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(PlayerSkinTexture.class) // This mixin is only for 1.12.2- +public abstract class MixinThreadDownloadImageDataV1 implements IFakeThreadDownloadImageData { + @Shadow + private BufferedImage field_6550; + + @Shadow + private boolean field_6553; + + @Override + public void resetNewBufferedImage(BufferedImage image) { + this.field_6553 = false; + this.field_6550 = image; + } +} diff --git a/src/main/java/customskinloader/plugin/ICustomSkinLoaderPlugin.java b/src/main/java/customskinloader/plugin/ICustomSkinLoaderPlugin.java new file mode 100644 index 0000000..f2de2c4 --- /dev/null +++ b/src/main/java/customskinloader/plugin/ICustomSkinLoaderPlugin.java @@ -0,0 +1,35 @@ +package customskinloader.plugin; + +import java.util.List; + +import customskinloader.config.SkinSiteProfile; +import customskinloader.loader.ProfileLoader; + +public interface ICustomSkinLoaderPlugin { + /** + * @return return a non-null value if need to add a new profile loader, otherwise return null. + */ + ProfileLoader.IProfileLoader getProfileLoader(); + + /** + * @return the default implementations which import by {@link ICustomSkinLoaderPlugin#getProfileLoader()}. + */ + List getDefaultProfiles(); + + interface IDefaultProfile { + /** + * @return the name of {@link SkinSiteProfile#name}. + */ + String getName(); + + /** + * @return the lower the number, the first to load. + */ + int getPriority(); + + /** + * Complete the {@link SkinSiteProfile} from config and write to config. + */ + void updateSkinSiteProfile(SkinSiteProfile ssp); + } +} diff --git a/src/main/java/customskinloader/plugin/PluginLoader.java b/src/main/java/customskinloader/plugin/PluginLoader.java new file mode 100644 index 0000000..c440264 --- /dev/null +++ b/src/main/java/customskinloader/plugin/PluginLoader.java @@ -0,0 +1,54 @@ +package customskinloader.plugin; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.ServiceLoader; + +import com.google.common.collect.Lists; +import customskinloader.CustomSkinLoader; +import customskinloader.loader.JsonAPILoader; +import customskinloader.loader.LegacyLoader; +import customskinloader.loader.MojangAPILoader; +import customskinloader.loader.jsonapi.*; +import org.apache.commons.io.FileUtils; + +public class PluginLoader { + public static final ICustomSkinLoaderPlugin[] DEFAULT_PLUGINS = new ICustomSkinLoaderPlugin[]{ + new MojangAPILoader(), + new LegacyLoader(), + new JsonAPILoader(new CustomSkinAPI()), + new JsonAPILoader(new CustomSkinAPIPlus()), + new JsonAPILoader(new UniSkinAPI()), + new JsonAPILoader(new ElyByAPI()), + new JsonAPILoader(new GlitchlessAPI()), + new JsonAPILoader(new MinecraftCapesAPI()) + }; + public static final ArrayList PLUGINS = loadPlugins(); + + private static ArrayList loadPlugins() { + File pluginsDir = new File(CustomSkinLoader.DATA_DIR, "Plugins"); + ArrayList urls = new ArrayList<>(); + if (!pluginsDir.isDirectory()) { + //noinspection ResultOfMethodCallIgnored + pluginsDir.mkdirs(); + } else { + for (File plugin : FileUtils.listFiles(pluginsDir, new String[]{"jar", "zip"}, false)) { + try { + urls.add(plugin.toURI().toURL()); + CustomSkinLoader.logger.info("Found a jar or zip file: " + plugin.getName()); + } catch (MalformedURLException ignored) { + } + } + } + ArrayList plugins = Lists.newArrayList(DEFAULT_PLUGINS); + + ServiceLoader sl = ServiceLoader.load(ICustomSkinLoaderPlugin.class, new URLClassLoader(urls.toArray(new URL[0]), PluginLoader.class.getClassLoader())); + for (ICustomSkinLoaderPlugin plugin : sl) { + plugins.add(plugin); + } + return plugins; + } +} diff --git a/src/main/java/customskinloader/profile/DynamicSkullManager.java b/src/main/java/customskinloader/profile/DynamicSkullManager.java new file mode 100644 index 0000000..4d9f6d2 --- /dev/null +++ b/src/main/java/customskinloader/profile/DynamicSkullManager.java @@ -0,0 +1,160 @@ +package customskinloader.profile; + +import java.io.File; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import com.google.common.collect.Lists; +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; +import com.mojang.authlib.properties.Property; +import com.mojang.util.UUIDTypeAdapter; + +import customskinloader.CustomSkinLoader; +import customskinloader.utils.HttpRequestUtil; +import customskinloader.utils.HttpRequestUtil.HttpRequest; +import customskinloader.utils.HttpRequestUtil.HttpResponce; +import customskinloader.utils.HttpTextureUtil; +import customskinloader.utils.HttpUtil0; + +public class DynamicSkullManager { + public static class SkullTexture{ + public Map textures; + public String index;//Index File + public ArrayList skins; + public int interval; + public boolean fromZero; + //For program + public long startTime; + public int period; + } + private Map dynamicTextures = new ConcurrentHashMap<>(); + private Map> staticTextures = new ConcurrentHashMap<>(); + private List loadingList=new ArrayList(); + + private void parseGameProfile(GameProfile profile){ + Property textureProperty = Iterables.getFirst(profile.getProperties().get("textures"), null); + if (textureProperty==null){ + staticTextures.put(profile, Maps.newHashMap()); + return; + } + String value=textureProperty.getValue(); + if(StringUtils.isBlank(value)){ + staticTextures.put(profile, Maps.newHashMap()); + return; + } + @SuppressWarnings("deprecation") String json = new String(Base64.decodeBase64(value), Charsets.UTF_8); + Gson gson=new GsonBuilder().registerTypeAdapter(UUID.class,new UUIDTypeAdapter()).create(); + SkullTexture result=gson.fromJson(json,SkullTexture.class); + if(result==null){ + staticTextures.put(profile, Maps.newHashMap()); + return; + } + staticTextures.put(profile, (result.textures==null || !result.textures.containsKey(Type.SKIN)) ? + Maps.newHashMap() : parseTextures(result.textures)); + + if(StringUtils.isNotEmpty(result.index)){ + File indexFile=new File(CustomSkinLoader.DATA_DIR,result.index); + try{ + String index=FileUtils.readFileToString(indexFile, "UTF-8"); + if(StringUtils.isNotEmpty(index)){ + String[] skins = CustomSkinLoader.GSON.fromJson(index, String[].class); + if(skins != null && skins.length != 0) + result.skins = Lists.newArrayList(skins); + } + }catch(Exception e){ + CustomSkinLoader.logger.warning("Exception occurs while parsing index file: "+e.toString()); + } + } + + if(!CustomSkinLoader.config.enableDynamicSkull||result.skins==null||result.skins.isEmpty()) + return; + + CustomSkinLoader.logger.info("Try to load Dynamic Skull: "+json); + + for(int i=0;i0){ + //Skin found + String fakeUrl=HttpTextureUtil.getLocalFakeUrl(skin); + result.skins.set(i, fakeUrl); + }else{ + //Could not find skin + result.skins.remove(i--); + } + }else{ + //Online Skin + HttpResponce responce=HttpRequestUtil.makeHttpRequest(new HttpRequest(skin).setCacheFile(HttpTextureUtil.getCacheFile(FilenameUtils.getBaseName(skin))).setCacheTime(0).setLoadContent(false)); + if(!responce.success){ + //Could not load skin + result.skins.remove(i--); + } + } + } + + if(result.skins.isEmpty()){//Nothing loaded + CustomSkinLoader.logger.info("Failed: Nothing loaded."); + return; + } + result.interval=Math.max(result.interval, 50); + if(result.fromZero) + result.startTime=System.currentTimeMillis(); + result.period=result.interval*result.skins.size(); + CustomSkinLoader.logger.info("Successfully loaded Dynamic Skull: "+new Gson().toJson(result)); + dynamicTextures.put(profile, result); + staticTextures.remove(profile); + } + + //Support local skin for skull + public Map parseTextures(Map textures) { + MinecraftProfileTexture skin=textures.get(Type.SKIN); + String skinUrl=skin.getUrl(); + if(!HttpUtil0.isLocal(skinUrl)) + return textures; + File skinFile=new File(CustomSkinLoader.DATA_DIR,skinUrl); + if(!skinFile.isFile()) + return Maps.newHashMap(); + textures.put(Type.SKIN, ModelManager0.getProfileTexture(HttpTextureUtil.getLocalFakeUrl(skinUrl), null)); + return textures; + } + + public Map getTexture(final GameProfile profile){ + if(staticTextures.get(profile)!=null) + return staticTextures.get(profile); + if(loadingList.contains(profile)) + return Maps.newHashMap(); + if(dynamicTextures.containsKey(profile)){ + SkullTexture texture=dynamicTextures.get(profile); + long time=System.currentTimeMillis()-texture.startTime; + int index=(int)Math.floor((time%texture.period)/texture.interval); + Map map=Maps.newHashMap(); + map.put(Type.SKIN, ModelManager0.getProfileTexture(texture.skins.get(index), null)); + return map; + } + + loadingList.add(profile); + Thread loadThread=new Thread(){ + public void run(){ + parseGameProfile(profile);//Load in thread + loadingList.remove(profile); + } + }; + loadThread.setName("Skull "+profile.hashCode()); + loadThread.start(); + return Maps.newHashMap(); + } +} diff --git a/src/main/java/customskinloader/profile/ModelManager0.java b/src/main/java/customskinloader/profile/ModelManager0.java new file mode 100644 index 0000000..f6063b0 --- /dev/null +++ b/src/main/java/customskinloader/profile/ModelManager0.java @@ -0,0 +1,131 @@ +package customskinloader.profile; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import com.google.common.collect.Maps; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; + +/** + * Model Manager for 1.8 and higher. + * A manager to check if model is available. + * It is the only class in package 'customskinloader' which has differences in different Minecraft version. + * + * @since 13.1 + */ +public class ModelManager0 { + public enum Model { + SKIN_DEFAULT, + SKIN_SLIM, + CAPE, + ELYTRA + } + + private static HashMap models = new HashMap(); + private static Type typeElytra = null; + + static { + for (Type type : Type.values()) { + if (type.ordinal() == 2)//ELYTRA + typeElytra = type; + } + models.put("default", Model.SKIN_DEFAULT); + models.put("slim", Model.SKIN_SLIM); + models.put("cape", Model.CAPE); + if (typeElytra != null) { + models.put("elytra", Model.ELYTRA); + models.put("elytron", Model.ELYTRA); + } + } + + /** + * Get enum for the model. + * + * @param model - string model + * @since 14.5 + */ + public static Model getEnumModel(String model) { + return models.get(model); + } + + /** + * Check if model is skin. + * + * @since 14.5 + */ + public static boolean isSkin(Model model) { + return model == Model.SKIN_DEFAULT || model == Model.SKIN_SLIM; + } + + /** + * Check if elytra is supported. + * + * @since 14.5 + */ + public static boolean isElytraSupported() { + return typeElytra != null; + } + + /** + * Parse hashMapProfile to UserProfile + * + * @param profile - hashMapProfile (HashMap) + * @return profile - UserProfile instance + * @since 13.1 + */ + public static UserProfile toUserProfile(Map profile) { + UserProfile userProfile = new UserProfile(); + if (profile == null) + return userProfile; + MinecraftProfileTexture skin = profile.get(Type.SKIN); + userProfile.skinUrl = skin == null ? null : skin.getUrl();//Avoid NullPointerException + userProfile.model = skin == null ? null : skin.getMetadata("model"); + if (StringUtils.isEmpty(userProfile.model)) + userProfile.model = "default"; + + MinecraftProfileTexture cape = profile.get(Type.CAPE); + userProfile.capeUrl = cape == null ? null : cape.getUrl(); + return userProfile; + } + + /** + * Parse UserProfile to hashMapProfile + * + * @param profile - UserProfile instance + * @return profile - hashMapProfile (HashMap) + * @since 13.1 + */ + public static Map fromUserProfile(UserProfile profile) { + Map map = Maps.newHashMap(); + if (profile == null) + return map; + if (profile.skinUrl != null) { + Map metadata = null; + if ("slim".equals(profile.model) || "auto".equals(profile.model)) { + metadata = Maps.newHashMap(); + metadata.put("model", profile.model); + } + map.put(Type.SKIN, getProfileTexture(profile.skinUrl, metadata)); + } + if (profile.capeUrl != null) + map.put(Type.CAPE, getProfileTexture(profile.capeUrl, null)); + if (typeElytra != null && profile.elytraUrl != null) + map.put(typeElytra, getProfileTexture(profile.elytraUrl, null)); + return map; + } + + /** + * Parse url to MinecraftProfileTexture + * + * @param url - textureUrl + * @param metadata - metadata + * @return MinecraftProfileTexture + * @since 14.5 + */ + public static MinecraftProfileTexture getProfileTexture(String url, Map metadata) { + return new MinecraftProfileTexture(url, metadata); + } +} diff --git a/src/main/java/customskinloader/profile/ProfileCache.java b/src/main/java/customskinloader/profile/ProfileCache.java new file mode 100644 index 0000000..605bf34 --- /dev/null +++ b/src/main/java/customskinloader/profile/ProfileCache.java @@ -0,0 +1,126 @@ +package customskinloader.profile; + +import java.io.File; +import java.util.Deque; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.Consumer; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import customskinloader.CustomSkinLoader; +import customskinloader.utils.TimeUtil; +import org.apache.commons.io.FileUtils; + +public class ProfileCache { + public static File PROFILE_CACHE_DIR=new File(CustomSkinLoader.DATA_DIR,"ProfileCache"); + + private Map cachedProfiles = new ConcurrentHashMap<>(); + private Map localProfiles = new ConcurrentHashMap<>(); + private Map>>> profileLoaders = new ConcurrentHashMap<>(); + + @SuppressWarnings("ResultOfMethodCallIgnored") + public ProfileCache(){ + if(!PROFILE_CACHE_DIR.exists()) + PROFILE_CACHE_DIR.mkdir(); + } + + public boolean isExist(String username){ + return cachedProfiles.containsKey(username.toLowerCase()); + } + public boolean isLoading(String username) { + CachedProfile cp=cachedProfiles.get(username.toLowerCase()); + return cp != null && cp.loading; + } + public boolean isReady(String username){ + CachedProfile cp=cachedProfiles.get(username.toLowerCase()); + return cp != null && (cp.loading || cp.expiryTime > TimeUtil.getCurrentUnixTimestamp()); + } + public boolean isExpired(String username){ + CachedProfile cp=cachedProfiles.get(username.toLowerCase()); + return cp == null || (cp.expiryTime <= TimeUtil.getCurrentUnixTimestamp()); + } + + public UserProfile getProfile(String username){ + return getCachedProfile(username).profile; + } + public long getExpiry(String username){ + return getCachedProfile(username).expiryTime; + } + public UserProfile getLocalProfile(String username){ + if(localProfiles.containsKey(username.toLowerCase())) + return localProfiles.get(username.toLowerCase()); + return loadLocalProfile(username); + } + public Optional>> getLastLoader(String username) { + Deque>> deque = this.profileLoaders.get(username); + if (deque != null) { + return Optional.ofNullable(deque.pollLast()); + } + return Optional.empty(); + } + + public void setLoading(String username,boolean loading){ + getCachedProfile(username).loading=loading; + } + public void updateCache(String username,UserProfile profile){ + updateCache(username,profile,CustomSkinLoader.config.enableLocalProfileCache); + } + public void updateCache(String username,UserProfile profile,boolean saveLocalProfile){ + CachedProfile cp=getCachedProfile(username); + cp.profile=profile; + cp.expiryTime=TimeUtil.getUnixTimestamp(CustomSkinLoader.config.cacheExpiry); + if(!saveLocalProfile) + return; + saveLocalProfile(username,profile); + } + public void putLoader(String username, Consumer> loader) { + this.profileLoaders.putIfAbsent(username, new ConcurrentLinkedDeque<>()); + this.profileLoaders.get(username).offerLast(loader); + } + + private CachedProfile getCachedProfile(String username){ + CachedProfile cp=cachedProfiles.get(username.toLowerCase()); + if(cp!=null) + return cp; + cp=new CachedProfile(); + cachedProfiles.put(username.toLowerCase(), cp); + return cp; + } + private UserProfile loadLocalProfile(String username){ + File localProfile=new File(PROFILE_CACHE_DIR,username.toLowerCase()+".json"); + if(!localProfile.exists()){ + localProfiles.put(username.toLowerCase(), null); + } + try{ + String json = FileUtils.readFileToString(localProfile, "UTF-8"); + UserProfile profile=CustomSkinLoader.GSON.fromJson(json, UserProfile.class); + localProfiles.put(username.toLowerCase(), profile); + CustomSkinLoader.logger.info("Successfully load LocalProfile."); + return profile; + }catch(Exception e){ + CustomSkinLoader.logger.info("Failed to load LocalProfile.("+e.toString()+")"); + localProfiles.put(username.toLowerCase(), null); + } + return null; + } + @SuppressWarnings("ResultOfMethodCallIgnored") + private void saveLocalProfile(String username, UserProfile profile){ + String json=CustomSkinLoader.GSON.toJson(profile); + File localProfile=new File(PROFILE_CACHE_DIR,username+".json"); + if(localProfile.exists()) + localProfile.delete(); + try { + FileUtils.write(localProfile, json, "UTF-8"); + CustomSkinLoader.logger.info("Successfully save LocalProfile."); + } catch (Exception e) { + CustomSkinLoader.logger.info("Failed to save LocalProfile.("+e.toString()+")"); + } + } +} +class CachedProfile{ + public UserProfile profile; + public long expiryTime=0; + public boolean loading=false; +} diff --git a/src/main/java/customskinloader/profile/UserProfile.java b/src/main/java/customskinloader/profile/UserProfile.java new file mode 100644 index 0000000..5ddbd78 --- /dev/null +++ b/src/main/java/customskinloader/profile/UserProfile.java @@ -0,0 +1,103 @@ +package customskinloader.profile; + +import org.apache.commons.lang3.StringUtils; + +import customskinloader.profile.ModelManager0.Model; + +/**The instance to storage user's profile in memory temporarily. + * In this manner, it could be easier to pass profile in program. + * @since 13.1 + */ +public class UserProfile { + + /** + * Direct url for skin. + * @since 13.1 + */ + public String skinUrl=null; + + /** + * Model for skin. + * default/slim + * @since 13.1 + */ + public String model=null; + + /** + * Direct url for cape. + * @since 13.1 + */ + public String capeUrl=null; + + /** + * Direct url for elytra. + * @since 14.5 + */ + public String elytraUrl=null; + + public void put(Model model,String url){ + if(model==null||StringUtils.isEmpty(url)) + return; + switch(model){ + case SKIN_DEFAULT: + this.skinUrl=url; + this.model="default"; + return; + case SKIN_SLIM: + this.skinUrl=url; + this.model="slim"; + return; + case CAPE: + this.capeUrl=url; + return; + case ELYTRA: + this.elytraUrl = url; + } + } + + /** + * Get parsed String to output the instance. + */ + @Override + public String toString(){ + return toString(0); + } + public String toString(long expiry){ + return "(SkinUrl: "+skinUrl+ + " , Model: "+model+ + " , CapeUrl: "+capeUrl+ + (StringUtils.isBlank(elytraUrl)?" ":" , ElytraUrl: "+elytraUrl)+ + (expiry==0?"":(" , Expiry: "+expiry))+")"; + } + + /** + * Check if the instance is empty. + * @return status (true - empty) + */ + public boolean isEmpty(){ + return StringUtils.isEmpty(skinUrl) && StringUtils.isEmpty(capeUrl) && StringUtils.isEmpty(elytraUrl); + } + /** + * Check if the instance is full(Both skin and cape). + * @return status (true - full) + * @since 14.7 + */ + public boolean isFull(){ + return StringUtils.isNoneBlank(skinUrl,capeUrl); + } + public boolean hasSkinUrl(){ + return StringUtils.isNotEmpty(skinUrl); + } + public void mix(UserProfile profile){ + if(profile==null) + return; + if(StringUtils.isEmpty(this.skinUrl)) { + this.skinUrl = profile.skinUrl; + this.model = profile.model; + } + if(StringUtils.isEmpty(this.capeUrl)) + this.capeUrl=profile.capeUrl; + if(StringUtils.isEmpty(this.elytraUrl)) + this.elytraUrl=profile.elytraUrl; + } +} diff --git a/src/main/java/customskinloader/utils/HttpRequestUtil.java b/src/main/java/customskinloader/utils/HttpRequestUtil.java new file mode 100644 index 0000000..0e615fb --- /dev/null +++ b/src/main/java/customskinloader/utils/HttpRequestUtil.java @@ -0,0 +1,281 @@ +package customskinloader.utils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +import customskinloader.CustomSkinLoader; + +public class HttpRequestUtil { + public static class HttpRequest { + public String url; + public String userAgent = null; + public String payload = null; + public boolean loadContent = true; + public boolean checkPNG = false; + + /** + * Cache Time + * -1=No Cache 0=Always Cache t=Default Cache Time(second) + */ + public int cacheTime = 600; + public File cacheFile = null;//Default Cache File + + public HttpRequest(String url) { + this.url = url; + } + + public HttpRequest setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public HttpRequest setPayload(String payload) { + this.payload = payload; + return this; + } + + public HttpRequest setLoadContent(boolean loadContent) { + this.loadContent = loadContent; + return this; + } + + public HttpRequest setCheckPNG(boolean checkPNG) { + this.checkPNG = checkPNG; + return this; + } + + public HttpRequest setCacheTime(int cacheTime) { + this.cacheTime = cacheTime; + return this; + } + + public HttpRequest setCacheFile(File cacheFile) { + this.cacheFile = cacheFile; + return this; + } + } + + public static class HttpResponce { + public String content = null; + public int responceCode = -1; + public boolean success = false; + public boolean fromCache = false; + } + + public static class CacheInfo { + public String url; + public String etag = null; + + public long lastModified = -1;//ms + public long expire = -1;//UNIX timestamp + } + + public static final File CACHE_DIR = new File(CustomSkinLoader.DATA_DIR, "caches"); + + public static HttpResponce makeHttpRequest(HttpRequest request) { + return makeHttpRequest(request, 0); + } + + public static HttpResponce makeHttpRequest(HttpRequest request, int redirectTime) { + try { + //No url provided, try to load from special cache file + if (request.url == null || request.url.isEmpty()) { + CustomSkinLoader.logger.debug("Try to read cache '" + request.cacheFile + "'."); + return loadFromCache(request, new HttpResponce()); + } + CustomSkinLoader.logger.debug("Try to request '" + request.url + (request.userAgent == null ? "'." : "' with user agent '" + request.userAgent + "'.")); + //Check Cache + if (StringUtils.isNotEmpty(request.payload) || CustomSkinLoader.config.forceDisableCache) { + request.cacheTime = -1;//No Cache + } + File cacheInfoFile = null; + CacheInfo cacheInfo = new CacheInfo(); + if (request.cacheFile == null && request.cacheTime >= 0) { + String hash = DigestUtils.sha1Hex(request.url); + request.cacheFile = new File(CACHE_DIR, hash); + cacheInfoFile = new File(CACHE_DIR, hash + ".json"); + } + //noinspection BulkFileAttributesRead + if (request.cacheTime == 0 && request.cacheFile.isFile()) { + return loadFromCache(request, new HttpResponce()); + } + if (cacheInfoFile != null && cacheInfoFile.isFile()) { + String json = FileUtils.readFileToString(cacheInfoFile, "UTF-8"); + if (StringUtils.isNotEmpty(json)) { + cacheInfo = CustomSkinLoader.GSON.fromJson(json, CacheInfo.class); + } + if (cacheInfo == null) { + cacheInfo = new CacheInfo(); + } + if (cacheInfo.expire >= TimeUtil.getCurrentUnixTimestamp()) { + return loadFromCache(request, new HttpResponce(), cacheInfo.expire); + } + } + + //Init Connection + URL rawUrl = new URL(request.url); + URI uri = new URI( + rawUrl.getProtocol(), + rawUrl.getUserInfo(), + rawUrl.getHost(), + rawUrl.getPort(), + rawUrl.getPath(), + rawUrl.getQuery(), + rawUrl.getRef() + ); + String url = uri.toASCIIString(); + if (!url.equalsIgnoreCase(request.url)) { + CustomSkinLoader.logger.debug("Encoded URL: " + url); + } + HttpURLConnection c = (HttpURLConnection) (new URL(url)).openConnection(); + c.setReadTimeout(1000 * 10); + c.setConnectTimeout(1000 * 10); + c.setDoInput(true); + c.setUseCaches(false); + c.setInstanceFollowRedirects(true); + + //Make Connection + if (cacheInfo.lastModified >= 0) { + c.setIfModifiedSince(cacheInfo.lastModified); + } + if (cacheInfo.etag != null) { + c.setRequestProperty("If-None-Match", cacheInfo.etag); + } + c.setRequestProperty("Accept-Encoding", "gzip"); + if (request.userAgent != null) { + c.setRequestProperty("User-Agent", request.userAgent); + } + if (StringUtils.isNotEmpty(request.payload)) { + CustomSkinLoader.logger.info("Payload: " + request.payload); + c.setRequestProperty("Content-Type", "application/json"); + c.setDoOutput(true); + OutputStream os = c.getOutputStream(); + IOUtils.write(request.payload, os, "UTF-8"); + IOUtils.closeQuietly(os); + } + c.connect(); + + //Check Connection + HttpResponce responce = new HttpResponce(); + responce.responceCode = c.getResponseCode(); + int res = c.getResponseCode(); + if (res / 100 == 4 || res / 100 == 5) {//Failed + CustomSkinLoader.logger.debug("Failed to request (Response Code: " + res + ")"); + return responce; + } + if (res == HttpURLConnection.HTTP_MOVED_PERM || res == HttpURLConnection.HTTP_MOVED_TEMP) { + //Redirect + if (redirectTime >= 4) { + CustomSkinLoader.logger.debug("Failed to request (Too many redirection)"); + return responce; + } + request.url = c.getHeaderField("Location");//Get redirecting location + if (request.url == null) { + CustomSkinLoader.logger.debug("Failed to request (Redirecting location not found)"); + return responce; + } + CustomSkinLoader.logger.debug("Redirect to: " + request.url); + return makeHttpRequest(request, redirectTime + 1);//Recursion + } + responce.success = true; + CustomSkinLoader.logger.debug("Successfully request (Response Code: " + res + " , Content Length: " + c.getContentLength() + ")"); + if (responce.responceCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + return loadFromCache(request, responce); + } + if (responce.responceCode == HttpURLConnection.HTTP_NO_CONTENT) { + request.cacheTime = 3600; + } + + //Load Content + InputStream is = "gzip".equals(c.getContentEncoding()) ? new GZIPInputStream(c.getInputStream()) : c.getInputStream(); + byte[] bytes = IOUtils.toByteArray(is); + if (request.checkPNG && (bytes.length <= 4 || bytes[1] != (byte) 'P' || bytes[2] != (byte) 'N' || bytes[3] != (byte) 'G')) { + CustomSkinLoader.logger.debug("Failed to request (Not Standard PNG)"); + responce.success = false; + return responce; + } + if (request.cacheFile != null) { + FileUtils.writeByteArrayToFile(request.cacheFile, bytes); + if (cacheInfoFile != null) { + cacheInfo.url = request.url; + cacheInfo.etag = c.getHeaderField("ETag"); + cacheInfo.lastModified = c.getLastModified(); + cacheInfo.expire = getExpire(c, request.cacheTime); + FileUtils.write(cacheInfoFile, CustomSkinLoader.GSON.toJson(cacheInfo), "UTF-8"); + } + //noinspection BulkFileAttributesRead + CustomSkinLoader.logger.debug("Saved to cache (Length: " + request.cacheFile.length() + " , Path: '" + request.cacheFile.getAbsolutePath() + "' , Expire: " + cacheInfo.expire + ")"); + } + if (!request.loadContent) { + return responce; + } + responce.content = new String(bytes, StandardCharsets.UTF_8); + CustomSkinLoader.logger.debug("Content: " + responce.content); + return responce; + + } catch (Exception e) { + CustomSkinLoader.logger.debug("Failed to request " + request.url + " (Exception: " + e + ")"); + return loadFromCache(request, new HttpResponce()); + } + } + + public static File getCacheFile(String hash) { + return new File(CACHE_DIR, hash); + } + + private static HttpResponce loadFromCache(HttpRequest request, HttpResponce responce) { + return loadFromCache(request, responce, 0); + } + + private static HttpResponce loadFromCache(HttpRequest request, HttpResponce responce, long expireTime) { + if (request.cacheFile == null || !request.cacheFile.isFile()) { + return responce; + } + CustomSkinLoader.logger.debug("Cache file found (Length: " + request.cacheFile.length() + " , Path: '" + request.cacheFile.getAbsolutePath() + "' , Expire: " + expireTime + ")"); + responce.fromCache = true; + responce.success = true; + if (!request.loadContent) { + return responce; + } + + CustomSkinLoader.logger.info("Try to load from cache '" + request.cacheFile.getAbsolutePath() + "'."); + try { + responce.content = FileUtils.readFileToString(request.cacheFile, "UTF-8"); + CustomSkinLoader.logger.debug("Successfully load from cache"); + CustomSkinLoader.logger.debug("Content: " + responce.content); + } catch (IOException e) { + CustomSkinLoader.logger.debug("Failed to load from cache (Exception: " + e + ")"); + responce.success = false; + } + return responce; + } + + private final static Pattern MAX_AGE_PATTERN = Pattern.compile(".*?max-age=(\\d+).*?"); + + private static long getExpire(HttpURLConnection connection, int cacheTime) { + String cacheControl = connection.getHeaderField("Cache-Control"); + if (StringUtils.isNotEmpty(cacheControl)) { + Matcher m = MAX_AGE_PATTERN.matcher(cacheControl); + if (m.matches()) + return TimeUtil.getUnixTimestamp(Long.parseLong(m.group(m.groupCount()))); + } + long expires = connection.getExpiration(); + if (expires > 0) + return expires / 1000; + return TimeUtil.getUnixTimestampRandomDelay(cacheTime == 0 ? 2592000 : cacheTime); + } +} diff --git a/src/main/java/customskinloader/utils/HttpTextureUtil.java b/src/main/java/customskinloader/utils/HttpTextureUtil.java new file mode 100644 index 0000000..792a61a --- /dev/null +++ b/src/main/java/customskinloader/utils/HttpTextureUtil.java @@ -0,0 +1,107 @@ +package customskinloader.utils; + +import java.io.File; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FilenameUtils; + +import customskinloader.CustomSkinLoader; + +public class HttpTextureUtil { + public static class HttpTextureInfo { + public String url = ""; + public File cacheFile; + public String hash; + } + + private static final String + LEGACY_MARK = "(LEGACY)", + LOCAL_MARK = "(LOCAL)", + LOCAL_LEGACY_MARK = "(LOCAL_LEGACY)", + BASE64_MARK = "(BASE64)"; + public static File defaultCacheDir; + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static void cleanCacheDir() { + if (defaultCacheDir != null) { + defaultCacheDir.delete(); + defaultCacheDir.mkdirs(); + } + } + + public static File getCacheDir() { + return defaultCacheDir == null ? new File(MinecraftUtil.getMinecraftDataDir(), "assets/skins") : defaultCacheDir; + } + + public static HttpTextureInfo toHttpTextureInfo(String fakeUrl) { + HttpTextureInfo info = new HttpTextureInfo(); + if (fakeUrl.startsWith("http")) { + info.url = fakeUrl; + info.hash = FilenameUtils.getBaseName(fakeUrl); + info.cacheFile = getCacheFile(info.hash); + return info; + } + if (fakeUrl.startsWith(LOCAL_LEGACY_MARK)) { + fakeUrl = fakeUrl.replace(LOCAL_LEGACY_MARK, ""); + String[] t = fakeUrl.split(",", 2); + if (t.length != 2) + return info; + info.cacheFile = new File(CustomSkinLoader.DATA_DIR, t[1]); + info.hash = t[0]; + return info; + } + if (fakeUrl.startsWith(LOCAL_MARK)) { + fakeUrl = fakeUrl.replace(LOCAL_MARK, ""); + info.cacheFile = new File(CustomSkinLoader.DATA_DIR, fakeUrl); + info.hash = FilenameUtils.getBaseName(fakeUrl); + return info; + } + if (fakeUrl.startsWith(LEGACY_MARK)) { + fakeUrl = fakeUrl.replace(LEGACY_MARK, ""); + info.url = fakeUrl; + info.hash = DigestUtils.sha1Hex(info.url); + info.cacheFile = HttpRequestUtil.getCacheFile(info.hash); + return info; + } + //Base64 fake url format: `(BASE64)${hash}` + if (fakeUrl.startsWith(BASE64_MARK)) { + fakeUrl = fakeUrl.replace(BASE64_MARK, ""); + info.hash = fakeUrl; + info.cacheFile = getCacheFile(info.hash); + return info; + } + return info; + } + + public static String getLegacyFakeUrl(String url) { + return LEGACY_MARK + url; + } + + public static String getLocalFakeUrl(String path) { + return LOCAL_MARK + path; + } + + public static String getLocalLegacyFakeUrl(String path, String hash) { + return LOCAL_LEGACY_MARK + hash + "," + path; + } + + public static String getBase64FakeUrl(String hash) { + return BASE64_MARK + hash; + } + + public static String getHash(String url, long size, long lastModified) { + return DigestUtils.sha1Hex(size + url + lastModified); + } + + public static String getHash(byte[] bytes) { + return DigestUtils.sha256Hex(bytes); + } + + public static File getCacheFile(String hash) { + return getCacheFile(defaultCacheDir, hash); + } + + public static File getCacheFile(File cacheDir, String hash) { + return new File(new File(cacheDir, hash.length() > 2 ? hash.substring(0, 2) : "xx"), hash); + } +} diff --git a/src/main/java/customskinloader/utils/HttpUtil0.java b/src/main/java/customskinloader/utils/HttpUtil0.java new file mode 100644 index 0000000..d744c5b --- /dev/null +++ b/src/main/java/customskinloader/utils/HttpUtil0.java @@ -0,0 +1,127 @@ +package customskinloader.utils; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.commons.lang3.StringUtils; + +public class HttpUtil0 { + public static boolean isLocal(String url){ + return url != null && !url.startsWith("http://") && !url.startsWith("https://"); + } + + //From: http://blog.csdn.net/xiyushiyi/article/details/46685387 + public static void ignoreHttpsCertificate(){ + HostnameVerifier doNotVerify = new HostnameVerifier() { + + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + public void checkClientTrusted(X509Certificate[] chain, String authType){} + public void checkServerTrusted(X509Certificate[] chain, String authType){} + }}; + + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + //HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultSSLSocketFactory(new SSLSocketFactoryFacade()); + HttpsURLConnection.setDefaultHostnameVerifier(doNotVerify); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static String parseAddress(String address) { + if(StringUtils.isEmpty(address)) + return null; + String[] addresses=address.split(":"); + InetAddress add; + try { + add = InetAddress.getByName(addresses[0]); + } catch (UnknownHostException e) { + e.printStackTrace(); + return null; + } + return add.getHostAddress()+(addresses.length==2 ? addresses[1] : "25565"); + } + public static boolean isLanServer(String standardAddress){ + if(StringUtils.isEmpty(standardAddress)) + return true; + String[] addresses=standardAddress.split(":"); + int numIp=getNumIp(addresses[0]); + return numIp==0||numIp==getNumIp("127.0.0.1")|| + (numIp>=getNumIp("192.168.0.0")&&numIp<=getNumIp("192.168.255.255"))|| + (numIp>=getNumIp("10.0.0.0")&&numIp<=getNumIp("10.255.255.255"))|| + (numIp>=getNumIp("172.16.0.0")&&numIp<=getNumIp("172.31.255.255")); + } + public static int getNumIp(String ip){ + int num=0; + String[] ips=ip.split("\\."); + if(ips.length!=4) + return 0; + for(int i=0;i<4;i++) + num+=Integer.parseInt(ips[i])*(256^(3-i)); + return num; + } + + //https://stackoverflow.com/questions/30817934/extended-server-name-sni-extension-not-sent-with-jdk1-8-0-but-send-with-jdk1-7 + public static class SSLSocketFactoryFacade extends SSLSocketFactory { + + SSLSocketFactory sslsf; + + public SSLSocketFactoryFacade() { + sslsf = (SSLSocketFactory) SSLSocketFactory.getDefault();; + } + + @Override + public String[] getDefaultCipherSuites() { + return sslsf.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslsf.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException { + return sslsf.createSocket(socket, s, i, b); + } + + @Override + public Socket createSocket(String s, int i) throws IOException, UnknownHostException { + return sslsf.createSocket(s, i); + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException { + return sslsf.createSocket(s, i, inetAddress, i1); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + return createSocket(inetAddress, i); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + return createSocket(inetAddress, i, inetAddress1, i1); + } + } +} diff --git a/src/main/java/customskinloader/utils/JavaUtil.java b/src/main/java/customskinloader/utils/JavaUtil.java new file mode 100644 index 0000000..bf88f5b --- /dev/null +++ b/src/main/java/customskinloader/utils/JavaUtil.java @@ -0,0 +1,34 @@ +package customskinloader.utils; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.LinkedList; + +public class JavaUtil { + public static URL[] getClasspath() { + if (JavaUtil.class.getClassLoader() instanceof URLClassLoader) { + //Java 8- + //Use URLClassLoader Directly + URLClassLoader ucl = (URLClassLoader) JavaUtil.class.getClassLoader(); + return ucl.getURLs(); + } else { + //Java 9+ + //It uses APPClassLoader instead of URLClassLoader + //Get Classpath from properties and parse it to URL array + String classpath = System.getProperty("java.class.path"); + String[] elements = classpath.split(File.pathSeparator); + if (elements.length == 0) + return new URL[0]; + LinkedList urls = new LinkedList(); + for (String ele : elements) { + try { + urls.add(new File(ele).toURI().toURL()); + } catch (Exception ignored) { + } + } + return urls.toArray(new URL[0]); + } + } +} diff --git a/src/main/java/customskinloader/utils/MinecraftUtil.java b/src/main/java/customskinloader/utils/MinecraftUtil.java new file mode 100644 index 0000000..93097f6 --- /dev/null +++ b/src/main/java/customskinloader/utils/MinecraftUtil.java @@ -0,0 +1,99 @@ +package customskinloader.utils; + +import java.io.File; +import java.io.FileReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.authlib.GameProfile; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.client.texture.PlayerSkinProvider; +import net.minecraft.realms.RealmsSharedConstants; +import org.apache.commons.lang3.StringUtils; + +/** + * @author Alexander Xia + * @since 13.6 + */ +public class MinecraftUtil { + public static File getMinecraftDataDir() { + return MinecraftClient.getInstance().runDirectory; + } + + public static TextureManager getTextureManager() { + return MinecraftClient.getInstance().getTextureManager(); + } + + public static PlayerSkinProvider getSkinManager() { + return MinecraftClient.getInstance().getSkinProvider(); + } + private static String minecraftMainVersion = null; + + public static String getMinecraftMainVersion() { + //Check if cached version found + if (minecraftMainVersion != null) { + return minecraftMainVersion; + } + + //version.json can be found in 1.14+ + URL versionFile = MinecraftUtil.class.getResource("/version.json"); + if (versionFile != null) { + try ( + InputStream is = versionFile.openStream(); + InputStreamReader isr = new InputStreamReader(is) + ) { + JsonObject obj = new JsonParser().parse(isr).getAsJsonObject(); + minecraftMainVersion = obj.get("name").getAsString(); + return minecraftMainVersion; + } catch (Exception ignored) { + + } + } + + //RealmsSharedConstants.VERSION_STRING is available in 1.16- + try { + Class realmsSharedConstants = Class.forName("net.minecraft.realms.RealmsSharedConstants"); + MethodHandle mh = MethodHandles.publicLookup().findStaticGetter(realmsSharedConstants, "VERSION_STRING", String.class); + minecraftMainVersion = (String) mh.invoke(); + return minecraftMainVersion; + } catch (Throwable ignored) { + } + + //No version can be found + return "unknown"; + } + + // (domain|ip)(:port) + public static String getServerAddress() { + net.minecraft.client.network.ServerInfo data = MinecraftClient.getInstance().getCurrentServerEntry(); + if (data == null)//Single Player + return null; + return data.address; + } + + // ip:port + public static String getStandardServerAddress() { + return HttpUtil0.parseAddress(getServerAddress()); + } + + public static boolean isLanServer() { + return HttpUtil0.isLanServer(getStandardServerAddress()); + } + + public static String getCredential(GameProfile profile) { + return (profile == null || profile.hashCode() == 0) ? null : + (profile.getId() == null ? profile.getName() : String.format("%s-%s", profile.getName(), profile.getId())); + } +} diff --git a/src/main/java/customskinloader/utils/TimeUtil.java b/src/main/java/customskinloader/utils/TimeUtil.java new file mode 100644 index 0000000..38e4501 --- /dev/null +++ b/src/main/java/customskinloader/utils/TimeUtil.java @@ -0,0 +1,17 @@ +package customskinloader.utils; + +public class TimeUtil { + public static long getCurrentUnixTimestamp(){ + return System.currentTimeMillis() / 1000L; + } + public static long getUnixTimestamp(long offset){ + return getCurrentUnixTimestamp()+offset; + } + public static long getUnixTimestampRandomDelay(long offset){ + return getCurrentUnixTimestamp()+offset+random(0,5); + } + + private static int random(int min,int max){ + return (int)(Math.random()*(max-min+1))+min; + } +} diff --git a/src/main/java/customskinloader/utils/Version.java b/src/main/java/customskinloader/utils/Version.java new file mode 100644 index 0000000..df00a55 --- /dev/null +++ b/src/main/java/customskinloader/utils/Version.java @@ -0,0 +1,50 @@ +package customskinloader.utils; + +import org.apache.commons.lang3.StringUtils; + +public class Version implements Comparable { + private String version; + private int sub[]; + + public Version(String version) { + if (version == null) version = ""; + this.version = version; + + //Parse Subversion + String[] split = version.split("\\."); + sub = new int[split.length]; + for (int i = 0; i < split.length; i++) { + if (StringUtils.isNumeric(split[i])) + sub[i] = Integer.parseInt(split[i]); + } + } + + public static Version of(String version) { + return new Version(version); + } + + public static int compare(String version, String anotherVersion) { + return Version.of(version).compareTo(anotherVersion); + } + + @Override + public String toString() { + return version; + } + + @Override + public int compareTo(Object o) { + if (o instanceof String) o = Version.of((String) o);//Parse String version + if (!(o instanceof Version)) throw new IllegalArgumentException(String.format("'%s' is not a Version.", o)); + + Version v = (Version) o; + int i = 0; + //Skip same subversion + while (i < sub.length && i < v.sub.length && sub[i] == v.sub[i]) i++; + //Compare first non-equal number + if (i < sub.length && i < v.sub.length) + return (sub[i] < v.sub[i]) ? -1 : ((sub[i] == v.sub[i]) ? 0 : 1); //Integer.compare(sub[i], v.sub[i]); + //Compare length if all subversion is same + return Integer.signum(sub.length - v.sub.length); + } +} diff --git a/src/main/java/net/minecraft/client/render/BufferedImageSkinProvider.java b/src/main/java/net/minecraft/client/render/BufferedImageSkinProvider.java new file mode 100644 index 0000000..01d9da5 --- /dev/null +++ b/src/main/java/net/minecraft/client/render/BufferedImageSkinProvider.java @@ -0,0 +1,16 @@ +package net.minecraft.client.render; + +import java.awt.image.BufferedImage; +import net.minecraft.client.render.BufferedImageSkinProvider; +import net.minecraft.client.texture.NativeImage; + +public interface BufferedImageSkinProvider extends Runnable { + BufferedImage parseSkin(BufferedImage image); + + void setAvailable(); + + @Override + default void run() { + this.setAvailable(); + } +} diff --git a/src/main/java/net/minecraft/client/texture/DownloadingTexture.java b/src/main/java/net/minecraft/client/texture/DownloadingTexture.java new file mode 100644 index 0000000..87b18de --- /dev/null +++ b/src/main/java/net/minecraft/client/texture/DownloadingTexture.java @@ -0,0 +1,17 @@ +package net.minecraft.client.texture; + +import java.io.File; + +import net.minecraft.client.render.BufferedImageSkinProvider; +import net.minecraft.client.texture.ResourceTexture; +import net.minecraft.util.Identifier; + +public class DownloadingTexture extends ResourceTexture { + public DownloadingTexture(File cacheFileIn, String imageUrlIn, Identifier textureResourceLocation, BufferedImageSkinProvider imageBufferIn) { + super(textureResourceLocation); + } + + public DownloadingTexture(File cacheFileIn, String imageUrlIn, Identifier textureResourceLocation, boolean legacySkinIn, Runnable processTaskIn) { + super(textureResourceLocation); + } +} diff --git a/src/main/java/net/minecraft/client/texture/NativeImage.java b/src/main/java/net/minecraft/client/texture/NativeImage.java new file mode 100644 index 0000000..95d30ec --- /dev/null +++ b/src/main/java/net/minecraft/client/texture/NativeImage.java @@ -0,0 +1,63 @@ +package net.minecraft.client.texture; + +import java.io.InputStream; + +/** + * Empty stack for NativeImage + */ +public class NativeImage { + public static NativeImage func_195713_a(InputStream inputStreamIn) { + return null; + } + + public NativeImage(int p_i48122_1_, int p_i48122_2_, boolean p_i48122_3_) { + } + + /** + * getWidth + */ + public int func_195702_a() { + return 0; + } + + /** + * getHeight + */ + public int func_195714_b() { + return 0; + } + + /** + * copyImage image + */ + public void func_195703_a(NativeImage p_195703_1_) { + } + + /** + * fillAreaRGBA x0 y0 weight height rgba + */ + public void func_195715_a(int p_195715_1_, int p_195715_2_, int p_195715_3_, int p_195715_4_, int p_195715_5_) { + } + + /** + * copyAreaRGBA x0 y0 dx dy weight height reversex reversey + */ + public void func_195699_a(int p_195699_1_, int p_195699_2_, int p_195699_3_, int p_195699_4_, int p_195699_5_, int p_195699_6_, boolean p_195699_7_, boolean p_195699_8_) { + } + + /** + * getPixelRGBA x y + */ + public int func_195709_a(int p_195709_1_, int p_195709_2_) { + return 0; + } + + /** + * setPixelRGBA x y rgba + */ + public void func_195700_a(int p_195700_1_, int p_195700_2_, int p_195700_3_) { + } + + public void close() { + } +} diff --git a/src/main/java/net/minecraft/client/texture/ThreadDownloadImageData.java b/src/main/java/net/minecraft/client/texture/ThreadDownloadImageData.java new file mode 100644 index 0000000..39b79f3 --- /dev/null +++ b/src/main/java/net/minecraft/client/texture/ThreadDownloadImageData.java @@ -0,0 +1,13 @@ +package net.minecraft.client.texture; + +import java.io.File; + +import net.minecraft.client.render.BufferedImageSkinProvider; +import net.minecraft.client.texture.ResourceTexture; +import net.minecraft.util.Identifier; + +public class ThreadDownloadImageData extends ResourceTexture { + public ThreadDownloadImageData(File cacheFileIn, String imageUrlIn, Identifier textureResourceLocation, BufferedImageSkinProvider imageBufferIn) { + super(textureResourceLocation); + } +} diff --git a/src/main/resources/BuildInfo.json b/src/main/resources/BuildInfo.json new file mode 100644 index 0000000..3301656 --- /dev/null +++ b/src/main/resources/BuildInfo.json @@ -0,0 +1,8 @@ +{ + "name": "CustomSkinLoader", + "version": "14.14-LF", + "fullVersion": "${modFullVersion}", + "mcVersion": "1.8.9", + "mcFullVersions": "${mcFullVersions}", + "fabricVersion": "0.14.9", +} diff --git a/src/main/resources/CSL_LICENSE b/src/main/resources/CSL_LICENSE new file mode 100644 index 0000000..24a453b --- /dev/null +++ b/src/main/resources/CSL_LICENSE @@ -0,0 +1,36 @@ +English(en) + +LICENSE - Binary File +You could not modify binary file. +Feel free to use and share this mod and unmodified file in anyway like modpack. +When using in modpack, you must put 'CustomSkinLoader' in mod list. +You could not repost this mod to any website without permission. +You could not earn money with this mod excluding modpack. + +LICENSE - Source Code +GPLv3: http://www.gnu.org/licenses/gpl.html +When using the code, you should change the name of package to avoid others' misunderstanding. + +OTHER - Useful Link +Source Code: https://github.com/xfl03/MCCustomSkinLoader +Release Page: http://www.mcbbs.net/thread-269807-1-1.html +Build Server: https://ci.infstudio.net/job/CustomSkinLoader/ + + +中文简体(zh_cn) + +许可 - 编译产物 +您不得对CustomSkinLoader的成品jar内部内容进行任何修改 +您可以在不经我许可的情况下,以任意方式传播这一mod以及jar文件(例如加入整合包)。 +在加入整合包时,请必须在帖子内mod列表中明确写上"万用皮肤补丁"或者"CustomSkinLoader"或者附上发布页面地址。 +您不得在未经许可的情况下,在国内外任何网站进行二次发布(包括皮肤站)。 +您不得以任何形式出售CustomSkinLoader或任何使用或参考CustomSkinLoader源代码的作品。 + +许可 - 源代码 +GPLv3: http://www.gnu.org/licenses/gpl.html +为避免他人误解,您在使用代码时必须修改包名。 + +其他 - 有用的链接 +源代码: https://github.com/xfl03/MCCustomSkinLoader +发布页面: http://www.mcbbs.net/thread-269807-1-1.html +构建服务器: https://ci.infstudio.net/job/CustomSkinLoader/ diff --git a/src/main/resources/LICENSE b/src/main/resources/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/src/main/resources/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..2056e1c --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "id": "customskinloader", + "version": "14.14-LF", + "name": "CustomSkinLoader", + "description": "Custom Skin Loader for Minecraft", + "authors": ["xfl03", "JLChnToZ"], + "contact": { + "homepage": "https://github.com/xfl03/MCCustomSkinLoader", + "issues": "https://github.com/xfl03/MCCustomSkinLoader/issues", + "sources": "https://github.com/xfl03/MCCustomSkinLoader" + }, + "license": "GPL-3.0-only", + "environment": "client", + "mixins": [ + "mixins.customskinloader.json" + ] +} diff --git a/src/main/resources/mixins.customskinloader.json b/src/main/resources/mixins.customskinloader.json new file mode 100644 index 0000000..58a40ee --- /dev/null +++ b/src/main/resources/mixins.customskinloader.json @@ -0,0 +1,19 @@ +{ + "compatibilityLevel": "JAVA_8", + "mixinPriority": 1010, + "package": "customskinloader.mixin", + "client": [ + "MixinGuiPlayerTabOverlay", + "MixinIResource", + "MixinIResourceManager", + "MixinMinecraft", + "MixinPlayerMenuObject", + "MixinSkinManager", + "MixinTextureManager", + "MixinThreadDownloadImageDataV1" + ], + "refmap": "customskinloader-refmap.json", + "injectors": { + "defaultRequire": 1 + } +}