diff --git a/src/main/java/com/falsepattern/lib/config/ConfigurationManager.java b/src/main/java/com/falsepattern/lib/config/ConfigurationManager.java index b9eaa70..a4c4b76 100644 --- a/src/main/java/com/falsepattern/lib/config/ConfigurationManager.java +++ b/src/main/java/com/falsepattern/lib/config/ConfigurationManager.java @@ -1,33 +1,13 @@ package com.falsepattern.lib.config; import com.falsepattern.lib.StableAPI; -import com.falsepattern.lib.internal.FalsePatternLib; -import com.falsepattern.lib.util.FileUtil; -import cpw.mods.fml.client.config.IConfigElement; -import cpw.mods.fml.client.event.ConfigChangedEvent; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; +import com.falsepattern.lib.internal.impl.config.ConfigurationManagerImpl; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import lombok.SneakyThrows; -import lombok.val; -import lombok.var; -import net.minecraft.launchwrapper.Launch; -import net.minecraftforge.common.config.ConfigElement; -import net.minecraftforge.common.config.Configuration; + +import cpw.mods.fml.client.config.IConfigElement; + +import java.util.List; /** * Class for controlling the loading of configuration files. @@ -35,15 +15,6 @@ import net.minecraftforge.common.config.Configuration; @NoArgsConstructor(access = AccessLevel.PRIVATE) @StableAPI(since = "0.6.0") public class ConfigurationManager { - private static final Map configs = new HashMap<>(); - - private static final Map>> configToClassMap = new HashMap<>(); - - private static final ConfigurationManager instance = new ConfigurationManager(); - - private static boolean initialized = false; - - private static Path configDir; /** * Registers a configuration class to be loaded. This should be done in preInit. @@ -51,139 +22,7 @@ public class ConfigurationManager { * @param configClass The class to register. */ public static void registerConfig(Class configClass) throws ConfigException { - init(); - val cfg = Optional.ofNullable(configClass.getAnnotation(Config.class)) - .orElseThrow(() -> new ConfigException( - "Class " + configClass.getName() + " does not have a @Config annotation!")); - val category = Optional.of(cfg.category().trim()) - .map((cat) -> cat.length() == 0 ? null : cat) - .orElseThrow(() -> new ConfigException( - "Config class " + configClass.getName() + " has an empty category!")); - val rawConfig = configs.computeIfAbsent(cfg.modid(), (ignored) -> { - val c = new Configuration(configDir.resolve(cfg.modid() + ".cfg").toFile()); - c.load(); - return c; - }); - configToClassMap.computeIfAbsent(rawConfig, (ignored) -> new HashSet<>()).add(configClass); - try { - processConfigInternal(configClass, category, rawConfig); - rawConfig.save(); - } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException | NoSuchFieldException e) { - throw new ConfigException(e); - } - } - - private static void processConfigInternal(Class configClass, String category, Configuration rawConfig) - throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, - ConfigException { - val cat = rawConfig.getCategory(category); - for (val field : configClass.getDeclaredFields()) { - if (field.getAnnotation(Config.Ignore.class) != null) { - continue; - } - field.setAccessible(true); - val comment = Optional.ofNullable(field.getAnnotation(Config.Comment.class)) - .map(Config.Comment::value) - .map((lines) -> String.join("\n", lines)) - .orElse(""); - val name = Optional.ofNullable(field.getAnnotation(Config.Name.class)) - .map(Config.Name::value) - .orElse(field.getName()); - val langKey = Optional.ofNullable(field.getAnnotation(Config.LangKey.class)) - .map(Config.LangKey::value) - .orElse(name); - val fieldClass = field.getType(); - var boxed = false; - if ((boxed = fieldClass.equals(Boolean.class)) || fieldClass.equals(boolean.class)) { - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultBoolean.class)) - .map(Config.DefaultBoolean::value) - .orElse(boxed ? (Boolean) field.get(null) : field.getBoolean(null)); - field.setBoolean(null, rawConfig.getBoolean(name, category, defaultValue, comment, langKey)); - } else if ((boxed = fieldClass.equals(Integer.class)) || fieldClass.equals(int.class)) { - val range = Optional.ofNullable(field.getAnnotation(Config.RangeInt.class)); - val min = range.map(Config.RangeInt::min).orElse(Integer.MIN_VALUE); - val max = range.map(Config.RangeInt::max).orElse(Integer.MAX_VALUE); - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultInt.class)) - .map(Config.DefaultInt::value) - .orElse(boxed ? (Integer) field.get(null) : field.getInt(null)); - field.setInt(null, rawConfig.getInt(name, category, defaultValue, min, max, comment, langKey)); - } else if ((boxed = fieldClass.equals(Float.class)) || fieldClass.equals(float.class)) { - val range = Optional.ofNullable(field.getAnnotation(Config.RangeFloat.class)); - val min = range.map(Config.RangeFloat::min).orElse(Float.MIN_VALUE); - val max = range.map(Config.RangeFloat::max).orElse(Float.MAX_VALUE); - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultFloat.class)) - .map(Config.DefaultFloat::value) - .orElse(boxed ? (Float) field.get(null) : field.getFloat(null)); - field.setFloat(null, rawConfig.getFloat(name, category, defaultValue, min, max, comment, langKey)); - } else if (fieldClass.equals(String.class)) { - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultString.class)) - .map(Config.DefaultString::value) - .orElse((String) field.get(null)); - val pattern = Optional.ofNullable(field.getAnnotation(Config.Pattern.class)) - .map(Config.Pattern::value) - .map(Pattern::compile) - .orElse(null); - field.set(null, rawConfig.getString(name, category, defaultValue, comment, langKey, pattern)); - } else if (fieldClass.isEnum()) { - val enumValues = Arrays.stream((Object[]) fieldClass.getDeclaredMethod("values").invoke(null)) - .map((obj) -> (Enum) obj) - .collect(Collectors.toList()); - val defaultValue = (Enum) Optional.ofNullable(field.getAnnotation(Config.DefaultEnum.class)) - .map(Config.DefaultEnum::value) - .map((defName) -> extractField(fieldClass, defName)) - .map(ConfigurationManager::extractValue) - .orElse(field.get(null)); - val possibleValues = enumValues.stream().map(Enum::name).toArray(String[]::new); - var value = rawConfig.getString(name, - category, - defaultValue.name(), - comment + "\nPossible values: " + Arrays.toString(possibleValues) + - "\n", - possibleValues, - langKey); - - try { - if (!Arrays.asList(possibleValues).contains(value)) { - throw new NoSuchFieldException(); - } - val enumField = fieldClass.getDeclaredField(value); - if (!enumField.isEnumConstant()) { - throw new NoSuchFieldException(); - } - field.set(null, enumField.get(null)); - } catch (NoSuchFieldException e) { - FalsePatternLib.getLog() - .warn("Invalid value " + value + " for enum configuration field " + field.getName() + - " of type " + fieldClass.getName() + " in config class " + - configClass.getName() + "! Using default value of " + defaultValue + "!"); - field.set(null, defaultValue); - } - } else if (fieldClass.isArray() && fieldClass.getComponentType().equals(String.class)) { - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultStringList.class)).map(Config.DefaultStringList::value).orElse((String[])field.get(null)); - var value = rawConfig.getStringList(name, category, defaultValue, comment, null, langKey); - field.set(null, value); - } else { - throw new ConfigException("Illegal config field: " + field.getName() + " in " + configClass.getName() + - ": Unsupported type " + fieldClass.getName() + - "! Did you forget an @Ignore annotation?"); - } - if (field.isAnnotationPresent(Config.RequiresMcRestart.class)) { - cat.setRequiresMcRestart(true); - } - if (field.isAnnotationPresent(Config.RequiresWorldRestart.class)) { - cat.setRequiresWorldRestart(true); - } - } - } - - @SneakyThrows - private static Field extractField(Class clazz, String field) { - return clazz.getDeclaredField(field); - } - - @SneakyThrows - private static Object extractValue(Field field) { - return field.get(null); + ConfigurationManagerImpl.registerConfig(configClass); } /** @@ -193,72 +32,8 @@ public class ConfigurationManager { * * @return The configuration elements. */ - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings("rawtypes") public static List getConfigElements(Class configClass) throws ConfigException { - init(); - val cfg = Optional.ofNullable(configClass.getAnnotation(Config.class)) - .orElseThrow(() -> new ConfigException( - "Class " + configClass.getName() + " does not have a @Config annotation!")); - val rawConfig = Optional.ofNullable(configs.get(cfg.modid())) - .map((conf) -> Optional.ofNullable(configToClassMap.get(conf)) - .map((l) -> l.contains(configClass)) - .orElse(false) ? conf : null) - .orElseThrow(() -> new ConfigException( - "Tried to get config elements for non-registered config class!")); - val category = cfg.category(); - val elements = new ConfigElement<>(rawConfig.getCategory(category)).getChildElements(); - return elements.stream().map((element) -> new IConfigElementProxy(element, () -> { - try { - processConfigInternal(configClass, category, rawConfig); - rawConfig.save(); - } catch (IllegalAccessException | - NoSuchMethodException | - InvocationTargetException | - NoSuchFieldException | - ConfigException e) { - e.printStackTrace(); - } - })).collect(Collectors.toList()); - } - - private static void init() { - if (initialized) { - return; - } - configDir = FileUtil.getMinecraftHome().toPath().resolve("config"); - initialized = true; - } - - /** - * Internal, do not use. - */ - public static void registerBus() { - FMLCommonHandler.instance().bus().register(instance); - } - - /** - * Internal, do not use. - * - * @param event The event. - */ - @SubscribeEvent - public void onConfigChanged(ConfigChangedEvent.OnConfigChangedEvent event) { - init(); - val config = configs.get(event.modID); - if (config == null) { - return; - } - val configClasses = configToClassMap.get(config); - configClasses.forEach((configClass) -> { - try { - val category = Optional.ofNullable(configClass.getAnnotation(Config.class)) - .map(Config::category) - .orElseThrow(() -> new ConfigException( - "Failed to get config category for class " + configClass.getName())); - processConfigInternal(configClass, category, config); - } catch (Exception e) { - e.printStackTrace(); - } - }); + return ConfigurationManagerImpl.getConfigElements(configClass); } } diff --git a/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java b/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java index e9ea3fd..6f1efc6 100644 --- a/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java +++ b/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java @@ -1,46 +1,16 @@ package com.falsepattern.lib.dependencies; import com.falsepattern.lib.StableAPI; -import com.falsepattern.lib.internal.*; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.MessageDigest; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import com.falsepattern.lib.util.FileUtil; -import lombok.*; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import net.minecraft.launchwrapper.LaunchClassLoader; +import com.falsepattern.lib.internal.impl.dependencies.DependencyLoaderImpl; +import lombok.Builder; +import lombok.NonNull; @StableAPI(since = "0.6.0") public class DependencyLoader { - /** - * Dependency checksum formats in a decreasing order of quality. - */ - private static final String[] CHECKSUM_TYPES = new String[]{"sha512", "sha256", "sha1", "md5"}; - - private static final Map loadedLibraries = new HashMap<>(); - private static final Map loadedLibraryMods = new HashMap<>(); - private static final Set mavenRepositories = new HashSet<>(); - - private static final Logger log = LogManager.getLogger(Tags.MODNAME + " Dependency Downloader"); - public static void addMavenRepo(String url) { - mavenRepositories.add(url); + DependencyLoaderImpl.addMavenRepo(url); } @Builder @@ -52,226 +22,7 @@ public class DependencyLoader { @NonNull Version preferredVersion, String regularSuffix, String devSuffix) { - val suffix = FalsePatternLib.isDeveloperEnvironment() ? devSuffix : regularSuffix; - val artifactLogName = String.format("%s:%s:%s%s", groupId, artifactId, preferredVersion, suffix != null ? "-" + suffix : ""); - log.info("Adding library {}, requested by mod {}", artifactLogName, loadingModId); - var artifact = groupId + ":" + artifactId + ":" + suffix; - if (loadedLibraries.containsKey(artifact)) { - val currentVer = loadedLibraries.get(artifact); - if (currentVer.equals(preferredVersion)) { - return; - } - val rangeString = "(minimum: " + minVersion + (maxVersion == null ? "" : ", maximum: " + maxVersion) + ")"; - if (minVersion.compareTo(currentVer) > 0 || (maxVersion != null && maxVersion.compareTo(currentVer) < 0)) { - for (int i = 0; i < 16; i++) { - log.fatal("ALERT VVVVVVVVVVVV ALERT"); - } - log.fatal("Library {}:{}{} already loaded with version {}, " + - "but a version in the range {} was requested! Thing may go horribly wrong! " + - "Requested by mod: {}, previously loaded by mod: {}", - groupId, - artifactId, - suffix != null ? ":" + suffix : "", - currentVer, - rangeString, - loadingModId, - loadedLibraryMods.get(artifact)); - for (int i = 0; i < 16; i++) { - log.fatal("ALERT ^^^^^^^^^^^^ ALERT"); - } - } else { - log.info("Attempted loading of library {}:{}{} with preferred version {}, " + - "but version {} was already loaded, which matched the range {}. This is not an " + - "error. " + "Requested by mod: {}, previously loaded by mod: {}", - groupId, - artifactId, - suffix != null ? ":" + suffix : "", - preferredVersion, - currentVer, - rangeString, - loadingModId, - loadedLibraryMods.get(artifact)); - } - return; - } - String homeDir = System.getProperty("minecraft.sharedDataDir"); - if (homeDir == null) { - homeDir = System.getenv("MINECRAFT_SHARED_DATA_DIR"); - if (homeDir == null) { - homeDir = FileUtil.getMinecraftHome().getAbsolutePath(); - } - } - val modsDir = Paths.get(homeDir, "mods").toFile(); - val mavenJarName = - String.format("%s-%s%s.jar", artifactId, preferredVersion, (suffix != null) ? ("-" + suffix) : ""); - val jarName = groupId + "-" + mavenJarName; - val libDir = new File(modsDir, "falsepattern"); - if (!libDir.exists()) { - if (!libDir.mkdirs()) { - log.fatal("Failed to create directory {}", libDir); - throw new RuntimeException("Failed to create directory " + libDir); - } - } - val file = new File(libDir, jarName); - if (file.exists()) { - try { - addToClasspath(file); - loadedLibraries.put(artifact, preferredVersion); - log.debug("Library {} successfully loaded from disk!", artifactLogName); - return; - } catch (RuntimeException e) { - log.warn("Failed to load library {} from file! Re-downloading...", artifactLogName); - checkedDelete(file); - } - } - if (!LibraryConfig.ENABLE_LIBRARY_DOWNLOADS) { - val errorMessage = "Failed to load library " + groupId + ":" + artifactId + ":" + preferredVersion + - ((suffix != null) ? ":" + suffix : "") + ": " + Tags.MODNAME + - " library downloading has been disabled in the config, and the library is not present " + - "on disk! Requested by mod: " + loadingModId; - log.fatal(errorMessage); - throw new IllegalStateException(errorMessage); - } - for (var repo : mavenRepositories) { - try { - if (!repo.endsWith("/")) { - repo = repo + "/"; - } - val url = String.format("%s%s/%s/%s/%s", repo, groupId.replace('.', '/'), artifactId, - preferredVersion, mavenJarName); - String finalRepo = repo; - boolean retry = true; - int retryCount = 0; - redownload: - while (retry) { - retry = false; - retryCount++; - if (retryCount >= 3) { - break; - } - val success = new AtomicBoolean(false); - Internet.connect(new URL(url), - (ex) -> log.debug("Artifact {} could not be downloaded from repo {}: {}", artifactLogName, - finalRepo, ex.getMessage()), - (input) -> { - log.debug("Downloading {} from {}", artifactLogName, finalRepo); - download(input, file); - log.debug("Downloaded {} from {}", artifactLogName, finalRepo); - success.set(true); - }); - if (success.get()) { - log.debug("Validating checksum for {}", artifactLogName); - boolean hadChecksum = false; - for (val checksumType : CHECKSUM_TYPES) { - val checksumURL = url + "." + checksumType; - val checksumFile = new File(libDir, jarName + "." + checksumType); - log.debug("Attempting to get {} checksum...", checksumType); - success.set(false); - Internet.connect(new URL(checksumURL), - (ex) -> log.debug("Could not get {} checksum for {}: {}", checksumType, - artifactLogName, ex.getMessage()), - (input) -> { - log.debug("Downloading {} checksum for {}", checksumType, artifactLogName); - download(input, checksumFile); - log.debug("Downloaded {} checksum for {}", checksumType, artifactLogName); - success.set(true); - }); - if (success.get()) { - val fileHash = hash(checksumType, file); - val referenceHash = new String(Files.readAllBytes(checksumFile.toPath())); - if (!fileHash.equals(referenceHash)) { - log.error("Failed {} checksum validation for {}. Retrying download...", - checksumType, artifactLogName); - checkedDelete(file); - checkedDelete(checksumFile); - retry = true; - continue redownload; - } - log.debug("Successfully validated {} checksum for {}", checksumType, - artifactLogName); - hadChecksum = true; - break; - } - } - if (!hadChecksum) { - log.warn("The library {} had no checksum available on the repository.\n" + - "There's a chance it might have gotten corrupted during download,\n" + - "but we're loading it anyways.", artifactLogName); - } - loadedLibraries.put(artifact, preferredVersion); - loadedLibraryMods.put(artifact, loadingModId); - addToClasspath(file); - return; - } - } - } catch (IOException ignored) { - } - } - val errorMessage = "Failed to download library " + groupId + ":" + artifactId + ":" + preferredVersion + ((suffix != null) ? ":" + suffix : "") + " from any repository! Requested by mod: " + - loadingModId; - log.fatal(errorMessage); - throw new IllegalStateException(errorMessage); - } - - private static String bytesToHex(byte[] hash) { - StringBuilder hexString = new StringBuilder(2 * hash.length); - for (byte b : hash) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) { - hexString.append('0'); - } - hexString.append(hex); - } - return hexString.toString(); - } - - @SneakyThrows - private static String digest(String algo, byte[] data) { - return bytesToHex(MessageDigest.getInstance(algo).digest(data)); - } - - @SneakyThrows - private static String hash(String algo, File file) { - byte[] data = Files.readAllBytes(file.toPath()); - switch (algo) { - case "md5": - algo = "MD5"; - break; - case "sha1": - algo = "SHA-1"; - break; - case "sha256": - algo = "SHA-256"; - break; - case "sha512": - algo = "SHA-512"; - break; - } - return digest(algo, data); - } - - private static void checkedDelete(File file) { - if (!file.delete()) { - log.fatal("Failed to delete file {}", file); - throw new RuntimeException("Failed to delete file " + file); - } - } - - private static void addToClasspath(File file) { - try { - val cl = (LaunchClassLoader) DependencyLoader.class.getClassLoader(); - cl.addURL(file.toURI().toURL()); - log.debug("Injected file {} into classpath!", file.getPath()); - } catch (Exception e) { - throw new RuntimeException("Failed to add library to classpath: " + file.getAbsolutePath(), e); - } - } - - @SneakyThrows - private static void download(InputStream is, File target) { - if (target.exists()) { - return; - } - Internet.transferAndClose(is, new BufferedOutputStream(Files.newOutputStream(target.toPath()))); + DependencyLoaderImpl.loadLibrary(loadingModId, groupId, artifactId, minVersion, maxVersion, preferredVersion, + regularSuffix, devSuffix); } } diff --git a/src/main/java/com/falsepattern/lib/internal/impl/config/ConfigurationManagerImpl.java b/src/main/java/com/falsepattern/lib/internal/impl/config/ConfigurationManagerImpl.java new file mode 100644 index 0000000..dbb889a --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/impl/config/ConfigurationManagerImpl.java @@ -0,0 +1,237 @@ +package com.falsepattern.lib.internal.impl.config; + +import com.falsepattern.lib.config.Config; +import com.falsepattern.lib.config.ConfigException; +import com.falsepattern.lib.config.IConfigElementProxy; +import com.falsepattern.lib.internal.FalsePatternLib; +import com.falsepattern.lib.util.FileUtil; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.val; +import lombok.var; + +import net.minecraftforge.common.config.ConfigElement; +import net.minecraftforge.common.config.Configuration; +import cpw.mods.fml.client.config.IConfigElement; +import cpw.mods.fml.client.event.ConfigChangedEvent; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * The actual implementation of ConfigurationManager. Migrated stuff here so that we don't unnecessarily expose + * internal-use functionality. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ConfigurationManagerImpl { + private static final Map configs = new HashMap<>(); + private static final Map>> configToClassMap = new HashMap<>(); + private static final ConfigurationManagerImpl instance = new ConfigurationManagerImpl(); + private static boolean initialized = false; + private static Path configDir; + + public static void registerConfig(Class configClass) throws ConfigException { + init(); + val cfg = Optional.ofNullable(configClass.getAnnotation(Config.class)) + .orElseThrow(() -> new ConfigException( + "Class " + configClass.getName() + " does not have a @Config annotation!")); + val category = Optional.of(cfg.category().trim()) + .map((cat) -> cat.length() == 0 ? null : cat) + .orElseThrow(() -> new ConfigException( + "Config class " + configClass.getName() + " has an empty category!")); + val rawConfig = configs.computeIfAbsent(cfg.modid(), (ignored) -> { + val c = new Configuration(configDir.resolve(cfg.modid() + ".cfg").toFile()); + c.load(); + return c; + }); + configToClassMap.computeIfAbsent(rawConfig, (ignored) -> new HashSet<>()).add(configClass); + try { + processConfigInternal(configClass, category, rawConfig); + rawConfig.save(); + } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException | NoSuchFieldException e) { + throw new ConfigException(e); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static List getConfigElements(Class configClass) throws ConfigException { + init(); + val cfg = Optional.ofNullable(configClass.getAnnotation(Config.class)) + .orElseThrow(() -> new ConfigException( + "Class " + configClass.getName() + " does not have a @Config annotation!")); + val rawConfig = Optional.ofNullable(configs.get(cfg.modid())) + .map((conf) -> Optional.ofNullable(configToClassMap.get(conf)) + .map((l) -> l.contains(configClass)) + .orElse(false) ? conf : null) + .orElseThrow(() -> new ConfigException( + "Tried to get config elements for non-registered config class!")); + val category = cfg.category(); + val elements = new ConfigElement<>(rawConfig.getCategory(category)).getChildElements(); + return elements.stream().map((element) -> new IConfigElementProxy(element, () -> { + try { + processConfigInternal(configClass, category, rawConfig); + rawConfig.save(); + } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException | NoSuchFieldException | + ConfigException e) { + e.printStackTrace(); + } + })).collect(Collectors.toList()); + } + + private static void processConfigInternal(Class configClass, String category, Configuration rawConfig) + throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, + ConfigException { + val cat = rawConfig.getCategory(category); + for (val field : configClass.getDeclaredFields()) { + if (field.getAnnotation(Config.Ignore.class) != null) { + continue; + } + field.setAccessible(true); + val comment = Optional.ofNullable(field.getAnnotation(Config.Comment.class)) + .map(Config.Comment::value) + .map((lines) -> String.join("\n", lines)) + .orElse(""); + val name = Optional.ofNullable(field.getAnnotation(Config.Name.class)) + .map(Config.Name::value) + .orElse(field.getName()); + val langKey = Optional.ofNullable(field.getAnnotation(Config.LangKey.class)) + .map(Config.LangKey::value) + .orElse(name); + val fieldClass = field.getType(); + var boxed = false; + if ((boxed = fieldClass.equals(Boolean.class)) || fieldClass.equals(boolean.class)) { + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultBoolean.class)) + .map(Config.DefaultBoolean::value) + .orElse(boxed ? (Boolean) field.get(null) : field.getBoolean(null)); + field.setBoolean(null, rawConfig.getBoolean(name, category, defaultValue, comment, langKey)); + } else if ((boxed = fieldClass.equals(Integer.class)) || fieldClass.equals(int.class)) { + val range = Optional.ofNullable(field.getAnnotation(Config.RangeInt.class)); + val min = range.map(Config.RangeInt::min).orElse(Integer.MIN_VALUE); + val max = range.map(Config.RangeInt::max).orElse(Integer.MAX_VALUE); + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultInt.class)) + .map(Config.DefaultInt::value) + .orElse(boxed ? (Integer) field.get(null) : field.getInt(null)); + field.setInt(null, rawConfig.getInt(name, category, defaultValue, min, max, comment, langKey)); + } else if ((boxed = fieldClass.equals(Float.class)) || fieldClass.equals(float.class)) { + val range = Optional.ofNullable(field.getAnnotation(Config.RangeFloat.class)); + val min = range.map(Config.RangeFloat::min).orElse(Float.MIN_VALUE); + val max = range.map(Config.RangeFloat::max).orElse(Float.MAX_VALUE); + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultFloat.class)) + .map(Config.DefaultFloat::value) + .orElse(boxed ? (Float) field.get(null) : field.getFloat(null)); + field.setFloat(null, rawConfig.getFloat(name, category, defaultValue, min, max, comment, langKey)); + } else if (fieldClass.equals(String.class)) { + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultString.class)) + .map(Config.DefaultString::value) + .orElse((String) field.get(null)); + val pattern = Optional.ofNullable(field.getAnnotation(Config.Pattern.class)) + .map(Config.Pattern::value) + .map(Pattern::compile) + .orElse(null); + field.set(null, rawConfig.getString(name, category, defaultValue, comment, langKey, pattern)); + } else if (fieldClass.isEnum()) { + val enumValues = Arrays.stream((Object[]) fieldClass.getDeclaredMethod("values").invoke(null)) + .map((obj) -> (Enum) obj) + .collect(Collectors.toList()); + val defaultValue = (Enum) Optional.ofNullable(field.getAnnotation(Config.DefaultEnum.class)) + .map(Config.DefaultEnum::value) + .map((defName) -> extractField(fieldClass, defName)) + .map(ConfigurationManagerImpl::extractValue) + .orElse(field.get(null)); + val possibleValues = enumValues.stream().map(Enum::name).toArray(String[]::new); + var value = rawConfig.getString(name, category, defaultValue.name(), + comment + "\nPossible values: " + Arrays.toString(possibleValues) + + "\n", possibleValues, langKey); + + try { + if (!Arrays.asList(possibleValues).contains(value)) { + throw new NoSuchFieldException(); + } + val enumField = fieldClass.getDeclaredField(value); + if (!enumField.isEnumConstant()) { + throw new NoSuchFieldException(); + } + field.set(null, enumField.get(null)); + } catch (NoSuchFieldException e) { + FalsePatternLib.getLog() + .warn("Invalid value " + value + " for enum configuration field " + field.getName() + + " of type " + fieldClass.getName() + " in config class " + + configClass.getName() + "! Using default value of " + defaultValue + "!"); + field.set(null, defaultValue); + } + } else if (fieldClass.isArray() && fieldClass.getComponentType().equals(String.class)) { + val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultStringList.class)) + .map(Config.DefaultStringList::value) + .orElse((String[]) field.get(null)); + var value = rawConfig.getStringList(name, category, defaultValue, comment, null, langKey); + field.set(null, value); + } else { + throw new ConfigException("Illegal config field: " + field.getName() + " in " + configClass.getName() + + ": Unsupported type " + fieldClass.getName() + + "! Did you forget an @Ignore annotation?"); + } + if (field.isAnnotationPresent(Config.RequiresMcRestart.class)) { + cat.setRequiresMcRestart(true); + } + if (field.isAnnotationPresent(Config.RequiresWorldRestart.class)) { + cat.setRequiresWorldRestart(true); + } + } + } + + @SneakyThrows + private static Field extractField(Class clazz, String field) { + return clazz.getDeclaredField(field); + } + + @SneakyThrows + private static Object extractValue(Field field) { + return field.get(null); + } + + private static void init() { + if (initialized) { + return; + } + configDir = FileUtil.getMinecraftHome().toPath().resolve("config"); + initialized = true; + } + + public static void registerBus() { + FMLCommonHandler.instance().bus().register(instance); + } + + @SubscribeEvent + public void onConfigChanged(ConfigChangedEvent.OnConfigChangedEvent event) { + init(); + val config = configs.get(event.modID); + if (config == null) { + return; + } + val configClasses = configToClassMap.get(config); + configClasses.forEach((configClass) -> { + try { + val category = Optional.ofNullable(configClass.getAnnotation(Config.class)) + .map(Config::category) + .orElseThrow(() -> new ConfigException( + "Failed to get config category for class " + configClass.getName())); + processConfigInternal(configClass, category, config); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } +} diff --git a/src/main/java/com/falsepattern/lib/internal/impl/dependencies/DependencyLoaderImpl.java b/src/main/java/com/falsepattern/lib/internal/impl/dependencies/DependencyLoaderImpl.java new file mode 100644 index 0000000..94e5506 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/impl/dependencies/DependencyLoaderImpl.java @@ -0,0 +1,261 @@ +package com.falsepattern.lib.internal.impl.dependencies; + +import com.falsepattern.lib.dependencies.DependencyLoader; +import com.falsepattern.lib.dependencies.Version; +import com.falsepattern.lib.internal.FalsePatternLib; +import com.falsepattern.lib.internal.Internet; +import com.falsepattern.lib.internal.LibraryConfig; +import com.falsepattern.lib.internal.Tags; +import com.falsepattern.lib.util.FileUtil; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.val; +import lombok.var; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class DependencyLoaderImpl { + + /** + * Dependency checksum formats in a decreasing order of quality. + */ + private static final String[] CHECKSUM_TYPES = new String[]{"sha512", "sha256", "sha1", "md5"}; + private static final Map loadedLibraries = new HashMap<>(); + private static final Map loadedLibraryMods = new HashMap<>(); + private static final Set mavenRepositories = new HashSet<>(); + private static final Logger log = LogManager.getLogger(Tags.MODNAME + " Dependency Downloader"); + + public static void addMavenRepo(String url) { + mavenRepositories.add(url); + } + + public static void loadLibrary(@NonNull String loadingModId, @NonNull String groupId, @NonNull String artifactId, + @NonNull Version minVersion, Version maxVersion, @NonNull Version preferredVersion, + String regularSuffix, String devSuffix) { + val suffix = FalsePatternLib.isDeveloperEnvironment() ? devSuffix : regularSuffix; + val artifactLogName = + String.format("%s:%s:%s%s", groupId, artifactId, preferredVersion, suffix != null ? "-" + suffix : ""); + log.info("Adding library {}, requested by mod {}", artifactLogName, loadingModId); + var artifact = groupId + ":" + artifactId + ":" + suffix; + if (loadedLibraries.containsKey(artifact)) { + val currentVer = loadedLibraries.get(artifact); + if (currentVer.equals(preferredVersion)) { + return; + } + val rangeString = "(minimum: " + minVersion + (maxVersion == null ? "" : ", maximum: " + maxVersion) + ")"; + if (minVersion.compareTo(currentVer) > 0 || (maxVersion != null && maxVersion.compareTo(currentVer) < 0)) { + for (int i = 0; i < 16; i++) { + log.fatal("ALERT VVVVVVVVVVVV ALERT"); + } + log.fatal("Library {}:{}{} already loaded with version {}, " + + "but a version in the range {} was requested! Thing may go horribly wrong! " + + "Requested by mod: {}, previously loaded by mod: {}", groupId, artifactId, + suffix != null ? ":" + suffix : "", currentVer, rangeString, loadingModId, + loadedLibraryMods.get(artifact)); + for (int i = 0; i < 16; i++) { + log.fatal("ALERT ^^^^^^^^^^^^ ALERT"); + } + } else { + log.info("Attempted loading of library {}:{}{} with preferred version {}, " + + "but version {} was already loaded, which matched the range {}. This is not an " + "error. " + + "Requested by mod: {}, previously loaded by mod: {}", groupId, artifactId, + suffix != null ? ":" + suffix : "", preferredVersion, currentVer, rangeString, loadingModId, + loadedLibraryMods.get(artifact)); + } + return; + } + String homeDir = System.getProperty("minecraft.sharedDataDir"); + if (homeDir == null) { + homeDir = System.getenv("MINECRAFT_SHARED_DATA_DIR"); + if (homeDir == null) { + homeDir = FileUtil.getMinecraftHome().getAbsolutePath(); + } + } + val modsDir = Paths.get(homeDir, "mods").toFile(); + val mavenJarName = + String.format("%s-%s%s.jar", artifactId, preferredVersion, (suffix != null) ? ("-" + suffix) : ""); + val jarName = groupId + "-" + mavenJarName; + val libDir = new File(modsDir, "falsepattern"); + if (!libDir.exists()) { + if (!libDir.mkdirs()) { + log.fatal("Failed to create directory {}", libDir); + throw new RuntimeException("Failed to create directory " + libDir); + } + } + val file = new File(libDir, jarName); + if (file.exists()) { + try { + addToClasspath(file); + loadedLibraries.put(artifact, preferredVersion); + log.debug("Library {} successfully loaded from disk!", artifactLogName); + return; + } catch (RuntimeException e) { + log.warn("Failed to load library {} from file! Re-downloading...", artifactLogName); + checkedDelete(file); + } + } + if (!LibraryConfig.ENABLE_LIBRARY_DOWNLOADS) { + val errorMessage = "Failed to load library " + groupId + ":" + artifactId + ":" + preferredVersion + + ((suffix != null) ? ":" + suffix : "") + ": " + Tags.MODNAME + + " library downloading has been disabled in the config, and the library is not present " + + "on disk! Requested by mod: " + loadingModId; + log.fatal(errorMessage); + throw new IllegalStateException(errorMessage); + } + for (var repo : mavenRepositories) { + try { + if (!repo.endsWith("/")) { + repo = repo + "/"; + } + val url = String.format("%s%s/%s/%s/%s", repo, groupId.replace('.', '/'), artifactId, preferredVersion, + mavenJarName); + String finalRepo = repo; + boolean retry = true; + int retryCount = 0; + redownload: + while (retry) { + retry = false; + retryCount++; + if (retryCount >= 3) { + break; + } + val success = new AtomicBoolean(false); + Internet.connect(new URL(url), + (ex) -> log.debug("Artifact {} could not be downloaded from repo {}: {}", + artifactLogName, finalRepo, ex.getMessage()), (input) -> { + log.debug("Downloading {} from {}", artifactLogName, finalRepo); + download(input, file); + log.debug("Downloaded {} from {}", artifactLogName, finalRepo); + success.set(true); + }); + if (success.get()) { + log.debug("Validating checksum for {}", artifactLogName); + boolean hadChecksum = false; + for (val checksumType : CHECKSUM_TYPES) { + val checksumURL = url + "." + checksumType; + val checksumFile = new File(libDir, jarName + "." + checksumType); + log.debug("Attempting to get {} checksum...", checksumType); + success.set(false); + Internet.connect(new URL(checksumURL), + (ex) -> log.debug("Could not get {} checksum for {}: {}", checksumType, + artifactLogName, ex.getMessage()), (input) -> { + log.debug("Downloading {} checksum for {}", checksumType, artifactLogName); + download(input, checksumFile); + log.debug("Downloaded {} checksum for {}", checksumType, artifactLogName); + success.set(true); + }); + if (success.get()) { + val fileHash = hash(checksumType, file); + val referenceHash = new String(Files.readAllBytes(checksumFile.toPath())); + if (!fileHash.equals(referenceHash)) { + log.error("Failed {} checksum validation for {}. Retrying download...", + checksumType, artifactLogName); + checkedDelete(file); + checkedDelete(checksumFile); + retry = true; + continue redownload; + } + log.debug("Successfully validated {} checksum for {}", checksumType, artifactLogName); + hadChecksum = true; + break; + } + } + if (!hadChecksum) { + log.warn("The library {} had no checksum available on the repository.\n" + + "There's a chance it might have gotten corrupted during download,\n" + + "but we're loading it anyways.", artifactLogName); + } + loadedLibraries.put(artifact, preferredVersion); + loadedLibraryMods.put(artifact, loadingModId); + addToClasspath(file); + return; + } + } + } catch (IOException ignored) { + } + } + val errorMessage = "Failed to download library " + groupId + ":" + artifactId + ":" + preferredVersion + + ((suffix != null) ? ":" + suffix : "") + " from any repository! Requested by mod: " + + loadingModId; + log.fatal(errorMessage); + throw new IllegalStateException(errorMessage); + } + + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(2 * hash.length); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + @SneakyThrows + private static String digest(String algo, byte[] data) { + return bytesToHex(MessageDigest.getInstance(algo).digest(data)); + } + + @SneakyThrows + private static String hash(String algo, File file) { + byte[] data = Files.readAllBytes(file.toPath()); + switch (algo) { + case "md5": + algo = "MD5"; + break; + case "sha1": + algo = "SHA-1"; + break; + case "sha256": + algo = "SHA-256"; + break; + case "sha512": + algo = "SHA-512"; + break; + } + return digest(algo, data); + } + + private static void checkedDelete(File file) { + if (!file.delete()) { + log.fatal("Failed to delete file {}", file); + throw new RuntimeException("Failed to delete file " + file); + } + } + + private static void addToClasspath(File file) { + try { + val cl = (LaunchClassLoader) DependencyLoader.class.getClassLoader(); + cl.addURL(file.toURI().toURL()); + log.debug("Injected file {} into classpath!", file.getPath()); + } catch (Exception e) { + throw new RuntimeException("Failed to add library to classpath: " + file.getAbsolutePath(), e); + } + } + + @SneakyThrows + private static void download(InputStream is, File target) { + if (target.exists()) { + return; + } + Internet.transferAndClose(is, new BufferedOutputStream(Files.newOutputStream(target.toPath()))); + } +} diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java index 24bcfbd..8627b2f 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java @@ -4,6 +4,7 @@ import com.falsepattern.lib.config.ConfigException; import com.falsepattern.lib.config.ConfigurationManager; import com.falsepattern.lib.internal.FalsePatternLib; import com.falsepattern.lib.internal.LibraryConfig; +import com.falsepattern.lib.internal.impl.config.ConfigurationManagerImpl; import com.falsepattern.lib.updates.ModUpdateInfo; import com.falsepattern.lib.updates.UpdateChecker; import cpw.mods.fml.common.event.FMLPostInitializationEvent; @@ -19,7 +20,7 @@ public class CommonProxy { protected CompletableFuture> updatesFuture; public void preInit(FMLPreInitializationEvent e) { - ConfigurationManager.registerBus(); + ConfigurationManagerImpl.registerBus(); try { ConfigurationManager.registerConfig(LibraryConfig.class); } catch (ConfigException ex) {