diff --git a/README.MD b/README.MD index 32f1d75..4b6ce91 100644 --- a/README.MD +++ b/README.MD @@ -7,6 +7,7 @@ A library mod for 1.7.10 with lots of useful stuff. See the table below for more | [dependencies](src/main/java/com/falsepattern/lib/dependencies) | Runtime dependency loader which uses Maven | | [mixin](src/main/java/com/falsepattern/lib/mixin) | Mixin loader plugin boilerplate code | | [text](src/main/java/com/falsepattern/lib/text) | Better Chat and GUI text processing | +| [updates](src/main/java/com/falsepattern/lib/updates) | Helper code for update checking | | [util](src/main/java/com/falsepattern/lib/util) | Additional utilities that do not fit the above categories, see below for more information | The contents of the [util](src/main/java/com/falsepattern/lib/util) package so far: @@ -14,10 +15,16 @@ The contents of the [util](src/main/java/com/falsepattern/lib/util) package so f | Class | Purpose | |---------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| | [ResourceUtil](src/main/java/com/falsepattern/lib/util/ResourceUtil.java) | Provides methods for efficiently retrieving resources from SPECIFIC jar files instead of the entire classpath | +| [Async](src/main/java/com/falsepattern/lib/util/Async.java) | Provides asynchronous execution utilities. | Anything annotated with [@StableAPI](src/main/java/com/falsepattern/lib/StableAPI.java) is guaranteed to not change in patch versions. +The update checker module and the library checker module can be toggled with the `ENABLE_UPDATE_CHECKER` and +`ENABLE_LIBRARY_DOWNLOADS` booleans in the `falsepatternlib.cfg` config file respectively. Note that the update checker +depends on the module downloader to grab [FJson](https://github.com/FalsePattern/FJson), so if it's not already present +on disk, disabling the library loader breaks the update checker. This is only required the first time though. + ### This project WILL NOT be ported to any version beyond 1.7.10, don't even ask [![POOP badge](https://raw.githubusercontent.com/gist/poop-person/991e80f390384bbeef09d208bff208f4/raw/a9ef83add84a70f2202896c2d81117ff7b169be1/poop-badge.svg)](https://gist.github.com/poop-person/991e80f390384bbeef09d208bff208f4) diff --git a/build.gradle b/build.gradle index e245554..ff1cf25 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -//version: 1641429628falsepattern16 +//version: 1641429628falsepattern18 /* DO NOT CHANGE THIS FILE! @@ -95,6 +95,7 @@ checkPropertyExists("repositoryName") checkPropertyExists("mavenGroupId") checkPropertyExists("mavenArtifactId") checkPropertyExists("hasMixinDeps") +checkPropertyExists("mixinPreinitConfig") String javaSourceDir = "src/main/java/" @@ -441,7 +442,7 @@ def getManifestAttributes() { if(usesMixins.toBoolean()) { manifestAttributes += [ "TweakClass" : "org.spongepowered.asm.launch.MixinTweaker", - "MixinConfigs" : "mixins." + modId + ".json", + "MixinConfigs" : "mixins." + modId + ".json" + (mixinPreinitConfig ? "," + mixinPreinitConfig : ""), "ForceLoadAsMod" : containsMixinsAndOrCoreModOnly.toBoolean() == false ] } diff --git a/dependencies.gradle b/dependencies.gradle index 587c204..49e56ff 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -5,5 +5,6 @@ dependencies { compileOnly("org.spongepowered:mixin:0.7.11-SNAPSHOT") { transitive = false } + compileOnly("com.falsepattern:json:0.4.1") annotationProcessor("org.projectlombok:lombok:1.18.22") } diff --git a/gradle.properties b/gradle.properties index b2202c7..8b2dc9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -68,13 +68,15 @@ hasMixinDeps = false mixinPlugin = # Specify the package that contains all of your Mixins. You may only place Mixins in this package or the build will fail! mixinsPackage = +# Specify a preinit mixin here. Preinit mixins should be used very rarely, if at all, so this mixin config will not be managed by the buildscript, only included. +mixinPreinitConfig = # Specify the core mod entry class if you use a core mod. This class must implement IFMLLoadingPlugin! # This parameter is for legacy compatability only # Example value: coreModClass = asm.FMLPlugin + modGroup = com.myname.mymodid -> com.myname.mymodid.asm.FMLPlugin coreModClass = internal.CoreLoadingPlugin # If your project is only a consolidation of mixins or a core mod and does NOT contain a 'normal' mod ( = some class # that is annotated with @Mod) you want this to be true. When in doubt: leave it on false! -containsMixinsAndOrCoreModOnly = true +containsMixinsAndOrCoreModOnly = false # If enabled, you may use 'shadowImplementation' for dependencies. They will be integrated in your jar. It is your # responsibility check the licence and request permission for distribution, if required. diff --git a/repositories.gradle b/repositories.gradle index aa8bcb1..d265b7a 100644 --- a/repositories.gradle +++ b/repositories.gradle @@ -3,4 +3,8 @@ repositories { name = "sponge" url = "https://sponge.falsepattern.com/" } + maven { + name = "mavenpattern" + url = "https://maven.falsepattern.com/" + } } diff --git a/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java b/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java index a3adb2c..220614a 100644 --- a/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java +++ b/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java @@ -1,11 +1,10 @@ package com.falsepattern.lib.dependencies; import com.falsepattern.lib.StableAPI; -import com.falsepattern.lib.internal.CoreLoadingPlugin; -import com.falsepattern.lib.internal.FalsePatternLib; +import com.falsepattern.lib.internal.*; + import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -14,11 +13,8 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import javax.net.ssl.HttpsURLConnection; -import lombok.Builder; -import lombok.NonNull; -import lombok.val; -import lombok.var; + +import lombok.*; import net.minecraft.launchwrapper.LaunchClassLoader; @@ -34,21 +30,18 @@ public class DependencyLoader { } @Builder - public static void loadLibrary(String loadingModId, - String groupId, - String artifactId, + 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 : ""); FalsePatternLib.getLog() - .info("Adding library {}:{}:{}{}, requested by mod {}", - groupId, - artifactId, - preferredVersion, - suffix != null ? "-" + suffix : "", + .info("Adding library {}, requested by mod {}", artifactLogName, loadingModId); var artifact = groupId + ":" + artifactId + ":" + suffix; if (loadedLibraries.containsKey(artifact)) { @@ -108,25 +101,25 @@ public class DependencyLoader { addToClasspath(file); loadedLibraries.put(artifact, preferredVersion); FalsePatternLib.getLog() - .info("Library {}:{}:{}{} successfully loaded from disk!", - groupId, - artifactId, - preferredVersion, - (suffix != null) ? ":" + suffix : ""); + .info("Library {} successfully loaded from disk!", artifactLogName); return; } catch (RuntimeException e) { FalsePatternLib.getLog() - .warn("Failed to load library {}:{}:{}{} from file! Redownloading...", - groupId, - artifactId, - preferredVersion, - (suffix != null) ? ":" + suffix : ""); + .warn("Failed to load library {} from file! Redownloading...", artifactLogName); if (!file.delete()) { FalsePatternLib.getLog().fatal("Failed to delete file {}", file); throw new RuntimeException("Failed to delete file " + 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; + FalsePatternLib.getLog().fatal(errorMessage); + throw new IllegalStateException(errorMessage); + } for (var repo : mavenRepositories) { try { if (!repo.endsWith("/")) { @@ -138,39 +131,24 @@ public class DependencyLoader { artifactId, preferredVersion, mavenJarName)); - - val connection = (HttpsURLConnection) url.openConnection(); - connection.setConnectTimeout(1500); - connection.setReadTimeout(1500); - connection.setRequestProperty("User-Agent", "FalsePatternLib Downloader"); - if (connection.getResponseCode() != 200) { + String finalRepo = repo; + Internet.connect(url, (ex) -> { FalsePatternLib.getLog() - .info("Artifact {}:{}:{}{} was not found on repo {}", - groupId, - artifactId, - preferredVersion, - (suffix != null) ? ":" + suffix : "", - repo); - connection.disconnect(); - continue; - } - FalsePatternLib.getLog() - .info("Downloading {}:{}:{}{} from {}", - groupId, - artifactId, - preferredVersion, - (suffix != null) ? ":" + suffix : "", - repo); - download(connection.getInputStream(), file); - FalsePatternLib.getLog() - .info("Downloaded {}:{}:{}{}", - groupId, - artifactId, - preferredVersion, - (suffix != null) ? ":" + suffix : ""); - loadedLibraries.put(artifact, preferredVersion); - loadedLibraryMods.put(artifact, loadingModId); - addToClasspath(file); + .info("Artifact {} could not be downloaded from repo {}: {}", + artifactLogName, + finalRepo, + ex.getMessage()); + }, (input) -> { + FalsePatternLib.getLog() + .info("Downloading {} from {}", + artifactLogName, + finalRepo); + download(input, file); + FalsePatternLib.getLog().info("Downloaded {}", artifactLogName); + loadedLibraries.put(artifact, preferredVersion); + loadedLibraryMods.put(artifact, loadingModId); + addToClasspath(file); + }); return; } catch (IOException ignored) { } @@ -192,19 +170,11 @@ public class DependencyLoader { } } - private static void download(InputStream is, File target) throws IOException { + @SneakyThrows + private static void download(InputStream is, File target) { if (target.exists()) { return; } - - var bytesRead = 0; - - val fileOutput = new BufferedOutputStream(Files.newOutputStream(target.toPath())); - byte[] smallBuffer = new byte[4096]; - while ((bytesRead = is.read(smallBuffer)) >= 0) { - fileOutput.write(smallBuffer, 0, bytesRead); - } - fileOutput.close(); - is.close(); + Internet.transferAndClose(is, new BufferedOutputStream(Files.newOutputStream(target.toPath()))); } } diff --git a/src/main/java/com/falsepattern/lib/internal/CoreLoadingPlugin.java b/src/main/java/com/falsepattern/lib/internal/CoreLoadingPlugin.java index aa8b191..18c8957 100644 --- a/src/main/java/com/falsepattern/lib/internal/CoreLoadingPlugin.java +++ b/src/main/java/com/falsepattern/lib/internal/CoreLoadingPlugin.java @@ -20,7 +20,7 @@ public class CoreLoadingPlugin implements IFMLLoadingPlugin { @Override public String getModContainerClass() { - return Tags.GROUPNAME + ".internal.FalsePatternLib"; + return null; } @Override diff --git a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java index 9548202..65a0b8e 100644 --- a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java +++ b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java @@ -1,44 +1,77 @@ package com.falsepattern.lib.internal; +import com.falsepattern.lib.config.ConfigException; import com.falsepattern.lib.config.ConfigurationManager; -import com.falsepattern.lib.util.ResourceUtil; -import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; -import cpw.mods.fml.common.DummyModContainer; -import cpw.mods.fml.common.LoadController; -import cpw.mods.fml.common.MetadataCollection; +import com.falsepattern.lib.internal.proxy.CommonProxy; +import com.falsepattern.lib.text.FormattedText; +import com.falsepattern.lib.updates.ModUpdateInfo; +import com.falsepattern.lib.updates.UpdateChecker; +import cpw.mods.fml.common.*; import cpw.mods.fml.common.event.FMLConstructionEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import lombok.Getter; +import lombok.val; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.resources.I18n; +import net.minecraft.event.ClickEvent; import net.minecraft.launchwrapper.Launch; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatStyle; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; + /** * Utility class used by FalsePatternLib's internal code. This can change between versions without notice, so do not use * this in your code! */ -public class FalsePatternLib extends DummyModContainer { +@SuppressWarnings("UnstableApiUsage") +@Mod(modid = Tags.MODID, + name = Tags.MODNAME, + version = Tags.VERSION, + acceptedMinecraftVersions = "[1.7.10]", + acceptableRemoteVersions = "*") +public class FalsePatternLib { @Getter private static final Logger log = LogManager.getLogger(Tags.MODNAME); @Getter private static final boolean developerEnvironment = (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); + @SidedProxy(clientSide = Tags.GROUPNAME + ".internal.proxy.ClientProxy", serverSide = Tags.GROUPNAME + ".internal.proxy.CommonProxy") + private static CommonProxy proxy; + public FalsePatternLib() { - super(MetadataCollection.from(ResourceUtil.getResourceFromJar("/mcmod.info", FalsePatternLib.class), Tags.MODID) - .getMetadataForId(Tags.MODID, null)); log.info("Version " + Tags.VERSION + " initialized!"); } - @SuppressWarnings("UnstableApiUsage") - @Subscribe + @Mod.EventHandler public void construct(FMLConstructionEvent e) { - ConfigurationManager.init(); + proxy.construct(e); } - @SuppressWarnings("UnstableApiUsage") - @Override - public boolean registerBus(EventBus bus, LoadController controller) { - bus.register(this); - return true; + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent e) { + proxy.preInit(e); } + + @Mod.EventHandler + public void postInit(FMLPostInitializationEvent e) { + proxy.postInit(e); + } + } diff --git a/src/main/java/com/falsepattern/lib/internal/Internet.java b/src/main/java/com/falsepattern/lib/internal/Internet.java new file mode 100644 index 0000000..a3421ae --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/Internet.java @@ -0,0 +1,40 @@ +package com.falsepattern.lib.internal; + +import lombok.val; +import lombok.var; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.function.Consumer; + +public class Internet { + public static void connect(URL URL, Consumer onError, Consumer onSuccess) { + try { + val connection = (HttpURLConnection) URL.openConnection(); + connection.setConnectTimeout(3500); + connection.setReadTimeout(5000); + connection.setRequestProperty("User-Agent", "FalsePatternLib Internet Connector"); + if (connection.getResponseCode() != 200) { + onError.accept(new Exception("HTTP response code " + connection.getResponseCode())); + } else { + onSuccess.accept(connection.getInputStream()); + } + connection.disconnect(); + } catch (Exception e) { + onError.accept(e); + } + } + + + public static void transferAndClose(InputStream is, OutputStream target) throws IOException { + var bytesRead = 0; + + byte[] smallBuffer = new byte[4096]; + while ((bytesRead = is.read(smallBuffer)) >= 0) { + target.write(smallBuffer, 0, bytesRead); + } + target.close(); + is.close(); + } +} diff --git a/src/main/java/com/falsepattern/lib/internal/LibraryConfig.java b/src/main/java/com/falsepattern/lib/internal/LibraryConfig.java new file mode 100644 index 0000000..99a9d38 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/LibraryConfig.java @@ -0,0 +1,18 @@ +package com.falsepattern.lib.internal; + +import com.falsepattern.lib.config.Config; + +@Config(modid = Tags.MODID) +public class LibraryConfig { + @Config.Comment({"Used to control whether FalsePatternLib should check for outdated mods.", + "If you're building a public modpack, you should turn this off so that your users don't " + + "get nagged about outdated mods."}) + @Config.LangKey("config.falsepatternlib.updatecheck") + public static boolean ENABLE_UPDATE_CHECKER = true; + + @Config.Comment({"Used to control whether FalsePatternLib should be allowed to use the internet.", + "If this is enabled, library downloads will be blocked.", + "Note that if a mod tries to download a library that is not downloaded yet, the game will crash."}) + @Config.LangKey("config.falsepatternlib.disableinternet") + public static boolean ENABLE_LIBRARY_DOWNLOADS = true; +} diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java new file mode 100644 index 0000000..a334361 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java @@ -0,0 +1,96 @@ +package com.falsepattern.lib.internal.proxy; + +import com.falsepattern.lib.internal.FalsePatternLib; +import com.falsepattern.lib.text.FormattedText; +import com.falsepattern.lib.util.Async; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import lombok.val; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.resources.I18n; +import net.minecraft.event.ClickEvent; +import net.minecraft.util.IChatComponent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +@SideOnly(Side.CLIENT) +public class ClientProxy extends CommonProxy { + private Future> chatFuture; + + @Override + public void preInit(FMLPreInitializationEvent e) { + super.preInit(e); + MinecraftForge.EVENT_BUS.register(this); + } + + @Override + public void postInit(FMLPostInitializationEvent e) { + super.postInit(e); + chatFuture = Async.asyncWorker.submit(new Callable>() { + @Override + public List call() throws Exception { + //Deadlock avoidance + if (updatesFuture == null || updatesFuture.isCancelled()) { + chatFuture = null; + return null; + } + if (!updatesFuture.isDone()) { + chatFuture = Async.asyncWorker.submit(this); + return null; + } + val updates = updatesFuture.get(); + if (updates == null || updates.size() == 0) + return null; + val updateText = new ArrayList(FormattedText.parse(I18n.format("falsepatternlib.chat.updatesavailable")).toChatText()); + val mods = Loader.instance().getIndexedModList(); + for (val update : updates) { + val mod = mods.get(update.modID); + updateText.addAll(FormattedText.parse(I18n.format("falsepatternlib.chat.modname", mod.getName())).toChatText()); + updateText.addAll(FormattedText.parse(I18n.format("falsepatternlib.chat.currentversion", update.currentVersion)).toChatText()); + updateText.addAll(FormattedText.parse(I18n.format("falsepatternlib.chat.latestversion", update.latestVersion)).toChatText()); + if (!update.updateURL.isEmpty()) { + val pre = FormattedText.parse(I18n.format("falsepatternlib.chat.updateurlpre")).toChatText(); + val link = FormattedText.parse(I18n.format("falsepatternlib.chat.updateurl")).toChatText(); + val post = FormattedText.parse(I18n.format("falsepatternlib.chat.updateurlpost")).toChatText(); + pre.get(pre.size() - 1).appendSibling(link.get(0)); + link.get(link.size() - 1).appendSibling(post.get(0)); + for (val l : link) { + l.getChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, update.updateURL)); + } + link.remove(0); + post.remove(0); + updateText.addAll(pre); + updateText.addAll(link); + updateText.addAll(post); + } + } + return updateText; + } + }); + } + + @SubscribeEvent + public void onSinglePlayer(EntityJoinWorldEvent e) { + if (chatFuture == null || + !(e.entity instanceof EntityPlayerSP)) return; + val player = (EntityPlayerSP) e.entity; + try { + for (val line: chatFuture.get(1, TimeUnit.SECONDS)) { + player.addChatMessage(line); + } + chatFuture = null; + } catch (Exception ex) { + FalsePatternLib.getLog().warn(ex); + } + } +} diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java new file mode 100644 index 0000000..5bb0d3f --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java @@ -0,0 +1,79 @@ +package com.falsepattern.lib.internal.proxy; + +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.Tags; +import com.falsepattern.lib.updates.ModUpdateInfo; +import com.falsepattern.lib.updates.UpdateChecker; +import com.falsepattern.lib.util.Async; +import cpw.mods.fml.common.event.FMLConstructionEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import lombok.val; +import lombok.var; +import net.minecraftforge.common.MinecraftForge; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +public class CommonProxy { + + protected Future> updatesFuture; + + public void construct(FMLConstructionEvent e) { + ConfigurationManager.init(); + } + + public void preInit(FMLPreInitializationEvent e) { + try { + ConfigurationManager.registerConfig(LibraryConfig.class); + } catch (ConfigException ex) { + FalsePatternLib.getLog().error("Failed to register " + Tags.MODNAME + " config!", ex); + } + if (LibraryConfig.ENABLE_UPDATE_CHECKER) { + FalsePatternLib.getLog().info("Launching asynchronous update check."); + val updateCheckFuture = UpdateChecker.fetchUpdatesAsync("https://falsepattern.com/mc/versions.json"); + updatesFuture = Async.asyncWorker.submit(new Callable>() { + @Override + public List call() { + //Deadlock avoidance + if (updateCheckFuture.isCancelled()) { + updatesFuture = null; + return null; + } + if (!updateCheckFuture.isDone()) { + updatesFuture = Async.asyncWorker.submit(this); + return null; + } + try { + var updates = updateCheckFuture.get(); + if (updates != null && updates.size() > 0) { + for (val update : updates) { + update.log(FalsePatternLib.getLog()); + } + } else if (updates == null) { + FalsePatternLib.getLog().warn("Unknown error while checking updates"); + } else { + FalsePatternLib.getLog().info("All FalsePattern mods up to date!"); + updates = null; + } + return updates; + } catch (InterruptedException | ExecutionException ex) { + FalsePatternLib.getLog().warn("Error while checking updates", ex); + } + return null; + } + }); + } + } + + public void postInit(FMLPostInitializationEvent e) { + + } + + +} diff --git a/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java new file mode 100644 index 0000000..6e23de6 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java @@ -0,0 +1,24 @@ +package com.falsepattern.lib.updates; + +import com.falsepattern.lib.StableAPI; +import lombok.Data; +import lombok.NonNull; +import org.apache.logging.log4j.Logger; + +@Data +@StableAPI(since = "0.8.0") +public class ModUpdateInfo { + @NonNull public final String modID; + @NonNull public final String currentVersion; + @NonNull public final String latestVersion; + @NonNull public final String updateURL; + + public void log(Logger logger) { + logger.info("Updates are available for mod {}: Currently installed version is {}, " + + "latest available version is {}. Update URL: {}", + modID, + currentVersion, + latestVersion, + updateURL.isEmpty() ? "unavailable": updateURL); + } +} diff --git a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java new file mode 100644 index 0000000..1b10185 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java @@ -0,0 +1,141 @@ +package com.falsepattern.lib.updates; + +import com.falsepattern.json.node.JsonNode; +import com.falsepattern.lib.StableAPI; +import com.falsepattern.lib.dependencies.DependencyLoader; +import com.falsepattern.lib.dependencies.SemanticVersion; +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.Async; +import cpw.mods.fml.common.Loader; +import lombok.val; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +@StableAPI(since = "0.8.0") +public class UpdateChecker { + private static final AtomicInteger jsonLibraryLoaded = new AtomicInteger(0); + /** + * Same this as {@link #fetchUpdates(String)}, but defers the check to a different thread. Useful for asynchronous + * update checks, if you don't want to block loading. + * @param url The URL to check + * @return A future that will contain the update info about mods that were both available on the URL and installed + */ + public static Future> fetchUpdatesAsync(String url) { + return Async.asyncWorker.submit(() -> fetchUpdates(url)); + } + + /** + * Checks for updates. The URL should be a JSON file that contains a list of mods, each with a mod ID, one or more + * versions, and a URL for the user to check for updates in case the current and latest versions are different. + * The JSON file must have the following format: + *
{@code
+     *  [
+     *      {
+     *          "modID": "modid",
+     *          "latestVersion": ["1.0.0", "1.0.0-foo"],
+     *          "updateURL": "https://example.com/mods/mymod"
+     *      },
+     *      {
+     *          "modID": "modid2",
+     *          "latestVersion": ["0.2.0", "0.3.0-alpha"],
+     *          "updateURL": "https://example.com/mods/mymod2"
+     *      },
+     *      ...etc, one json object per mod.
+     *  ]
+     * }
+ * @param url The URL to check + * @return A list of mods that were both available on the URL and installed + */ + public static List fetchUpdates(String url) { + if (!LibraryConfig.ENABLE_UPDATE_CHECKER) { + return null; + } + URL URL; + try { + URL = new URL(url); + } catch (MalformedURLException e) { + FalsePatternLib.getLog().error("Invalid URL: {}", url, e); + return null; + } + switch (jsonLibraryLoaded.get()) { + case 0: + DependencyLoader.addMavenRepo("https://maven.falsepattern.com/"); + try { + DependencyLoader.builder() + .loadingModId(Tags.MODID) + .groupId("com.falsepattern") + .artifactId("json") + .minVersion(new SemanticVersion(0, 4, 0)) + .maxVersion(new SemanticVersion(0, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .preferredVersion(new SemanticVersion(0, 4, 1)) + .build(); + } catch (Exception e) { + FalsePatternLib.getLog().error("Failed to load json library for update checker!", e); + jsonLibraryLoaded.set(-1); + return null; + } + jsonLibraryLoaded.set(1); + break; + case -1: + return null; + } + AtomicReference loadedData = new AtomicReference<>(null); + Internet.connect(URL, (ex) -> FalsePatternLib.getLog().warn("Failed to check for updates from URL {}", url, ex), (input) -> { + val data = new ByteArrayOutputStream(); + try { + Internet.transferAndClose(input, data); + } catch (IOException e) { + throw new RuntimeException(e); + } + loadedData.set(data.toString()); + }); + if (loadedData.get() == null) return null; + val result = new ArrayList(); + val parsed = JsonNode.parse(loadedData.get()); + List modList; + if (parsed.isList()) { + modList = parsed.getJavaList(); + } else { + modList = Collections.singletonList(parsed); + } + val installedMods = Loader.instance().getIndexedModList(); + for (val node: modList) { + if (!node.isObject()) continue; + if (!node.containsKey("modid")) continue; + if (!node.containsKey("latestVersion")) continue; + val modid = node.getString("modid"); + if (!installedMods.containsKey(modid)) continue; + val mod = installedMods.get(modid); + val latestVersionsNode = node.get("latestVersion"); + List latestVersions; + if (latestVersionsNode.isString()) { + latestVersions = Collections.singletonList(latestVersionsNode.stringValue()); + } else if (latestVersionsNode.isList()) { + latestVersions = new ArrayList<>(); + for (val version: latestVersionsNode.getJavaList()) { + if (!version.isString()) continue; + latestVersions.add(version.stringValue()); + } + } else { + continue; + } + val currentVersion = mod.getVersion(); + if (latestVersions.contains(currentVersion)) continue; + val updateURL = node.containsKey("updateURL") && node.get("updateURL").isString() ? node.getString("updateURL") : ""; + result.add(new ModUpdateInfo(modid, currentVersion, latestVersions.get(0), updateURL)); + } + return result; + } +} diff --git a/src/main/java/com/falsepattern/lib/util/Async.java b/src/main/java/com/falsepattern/lib/util/Async.java new file mode 100644 index 0000000..5313d30 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/util/Async.java @@ -0,0 +1,23 @@ +package com.falsepattern.lib.util; + +import com.falsepattern.lib.StableAPI; +import com.falsepattern.lib.internal.Tags; +import lombok.Getter; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Asynchronous utilities. Notice: you should not abuse this for anything performance-sensitive. + * The asynchronous worker exists primarily to run code independently of the main thread during loading. + * You must make sure to solve any potential deadlocks yourself! + */ +@StableAPI(since = "0.8.0") +public class Async { + public static final ExecutorService asyncWorker = Executors.newSingleThreadExecutor((runnable) -> { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + thread.setName(Tags.MODNAME + " Async Worker"); + return thread; + }); +} diff --git a/src/main/resources/assets/falsepatternlib/lang/en_US.lang b/src/main/resources/assets/falsepatternlib/lang/en_US.lang new file mode 100644 index 0000000..31079e4 --- /dev/null +++ b/src/main/resources/assets/falsepatternlib/lang/en_US.lang @@ -0,0 +1,12 @@ +config.falsepatternlib.updatecheck=Enable update checks +config.falsepatternlib.updatecheck.tooltip=Used to control whether FalsePatternLib should check for outdated mods. +config.falsepatternlib.disableinternet=FalsePatternLib Offline Mode +config.falsepatternlib.disableinternet.tooltip=Used to control whether FalsePatternLib should be allowed to use the internet. If this is enabled, update checks and library downloads will be completely blocked. +falsepatternlib.chat.updatesavailable=§4Outdated mods detected! §r +falsepatternlib.chat.modname=§b§l[%s]§r +falsepatternlib.chat.currentversion= Current Version: §6%s§r +falsepatternlib.chat.latestversion= Latest Version: §2%s§r +falsepatternlib.chat.updateurlpre= Click +falsepatternlib.chat.updateurl= §n§o§dHERE§r +falsepatternlib.chat.updateurlpost= to visit update page +falsepatternlib.chat.updateavailablefor=§1§l[%s]§r Current version: §6%s§r Latest version: §2%s§r diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1 @@ +{} \ No newline at end of file