From ed7ca483218b0a18e3c6d144b92109468adcbdcb Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Thu, 9 Jun 2022 20:14:11 +0200 Subject: [PATCH 01/11] Begin working on the update checker Also added a config for toggling internet capabilities --- .../lib/internal/FalsePatternLib.java | 17 ++++ .../lib/internal/LibraryConfig.java | 18 ++++ .../lib/updates/ModUpdateInfo.java | 11 +++ .../lib/updates/UpdateChecker.java | 85 +++++++++++++++++++ .../assets/falsepatternlib/lang/en_US.lang | 4 + 5 files changed, 135 insertions(+) create mode 100644 src/main/java/com/falsepattern/lib/internal/LibraryConfig.java create mode 100644 src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java create mode 100644 src/main/java/com/falsepattern/lib/updates/UpdateChecker.java create mode 100644 src/main/resources/assets/falsepatternlib/lang/en_US.lang diff --git a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java index 9548202..eb7b795 100644 --- a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java +++ b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java @@ -1,14 +1,20 @@ 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.Loader; import cpw.mods.fml.common.MetadataCollection; import cpw.mods.fml.common.event.FMLConstructionEvent; +import cpw.mods.fml.common.event.FMLInterModComms; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; import lombok.Getter; +import lombok.SneakyThrows; import net.minecraft.launchwrapper.Launch; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -17,6 +23,7 @@ import org.apache.logging.log4j.Logger; * Utility class used by FalsePatternLib's internal code. This can change between versions without notice, so do not use * this in your code! */ +@SuppressWarnings("UnstableApiUsage") public class FalsePatternLib extends DummyModContainer { @Getter private static final Logger log = LogManager.getLogger(Tags.MODNAME); @@ -33,6 +40,16 @@ public class FalsePatternLib extends DummyModContainer { @Subscribe public void construct(FMLConstructionEvent e) { ConfigurationManager.init(); + try { + ConfigurationManager.registerConfig(LibraryConfig.class); + } catch (ConfigException ex) { + getLog().error("Failed to register " + Tags.MODNAME + " config!", ex); + } + } + + @Subscribe + public void postInit(FMLPostInitializationEvent e) { + } @SuppressWarnings("UnstableApiUsage") 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/updates/ModUpdateInfo.java b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java new file mode 100644 index 0000000..2b363a0 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java @@ -0,0 +1,11 @@ +package com.falsepattern.lib.updates; + +import lombok.Data; + +@Data +public class ModUpdateInfo { + public final String modID; + public final String currentVersion; + public final String latestVersion; + public final String 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..7a6837e --- /dev/null +++ b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java @@ -0,0 +1,85 @@ +package com.falsepattern.lib.updates; + +import com.falsepattern.lib.dependencies.DependencyLoader; +import com.falsepattern.lib.dependencies.SemanticVersion; +import com.falsepattern.lib.internal.FalsePatternLib; +import com.falsepattern.lib.internal.LibraryConfig; +import com.falsepattern.lib.internal.Tags; +import lombok.val; +import lombok.var; + +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.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; + +public class UpdateChecker { + private static AtomicBoolean jsonLibraryLoaded = new AtomicBoolean(false); + private static final ExecutorService asyncExecutor = Executors.newSingleThreadExecutor((runnable) -> { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + thread.setName(Tags.MODNAME + " Asynchronous Update Check Thread"); + return thread; + }); + /** + * 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 asyncExecutor.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; + } + if (!jsonLibraryLoaded.get()) { + DependencyLoader.addMavenRepo("https://maven.falsepattern.com/"); + try { + DependencyLoader.builder() + .groupId("com.falsepattern") + .artifactId("json") + .minVersion(new SemanticVersion(0, 4, 0)) + .maxVersion(new SemanticVersion(0, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .build(); + } catch (Exception e) { + FalsePatternLib.getLog().error("Failed to load json library for update checker!", e); + return null; + } + jsonLibraryLoaded.set(true); + } + //TODO + return null; + } +} 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..bbe9189 --- /dev/null +++ b/src/main/resources/assets/falsepatternlib/lang/en_US.lang @@ -0,0 +1,4 @@ +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. \ No newline at end of file From c02b78778639c061ce066fd7007d9813928fd46a Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Thu, 9 Jun 2022 22:18:49 +0200 Subject: [PATCH 02/11] use the config in the dependency loader for toggling downloads --- .../lib/dependencies/DependencyLoader.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java b/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java index a3adb2c..6bed9ec 100644 --- a/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java +++ b/src/main/java/com/falsepattern/lib/dependencies/DependencyLoader.java @@ -15,6 +15,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.net.ssl.HttpsURLConnection; + +import com.falsepattern.lib.internal.LibraryConfig; import lombok.Builder; import lombok.NonNull; import lombok.val; @@ -127,6 +129,14 @@ public class DependencyLoader { } } } + if (!LibraryConfig.ENABLE_LIBRARY_DOWNLOADS) { + val errorMessage = "Failed to load library " + groupId + ":" + artifactId + ":" + preferredVersion + + ((suffix != null) ? ":" + suffix : "") + ": FalsePatternLib 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("/")) { From dd170e7e5cee959cc1e925c2a57a7a70c6c5195b Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 00:09:13 +0200 Subject: [PATCH 03/11] added update checks and separate internet library --- dependencies.gradle | 1 + repositories.gradle | 4 + .../lib/dependencies/DependencyLoader.java | 106 ++++++----------- .../lib/internal/FalsePatternLib.java | 29 ++++- .../falsepattern/lib/internal/Internet.java | 40 +++++++ .../lib/updates/ModUpdateInfo.java | 19 ++- .../lib/updates/UpdateChecker.java | 109 ++++++++++++++---- 7 files changed, 207 insertions(+), 101 deletions(-) create mode 100644 src/main/java/com/falsepattern/lib/internal/Internet.java 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/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 6bed9ec..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,13 +13,8 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import javax.net.ssl.HttpsURLConnection; -import com.falsepattern.lib.internal.LibraryConfig; -import lombok.Builder; -import lombok.NonNull; -import lombok.val; -import lombok.var; +import lombok.*; import net.minecraft.launchwrapper.LaunchClassLoader; @@ -36,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)) { @@ -110,19 +101,11 @@ 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); @@ -131,9 +114,9 @@ public class DependencyLoader { } if (!LibraryConfig.ENABLE_LIBRARY_DOWNLOADS) { val errorMessage = "Failed to load library " + groupId + ":" + artifactId + ":" + preferredVersion + - ((suffix != null) ? ":" + suffix : "") + ": FalsePatternLib library downloading has " + - "been disabled in the config, and the library is not present on disk! Requested by mod: " + - loadingModId; + ((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); } @@ -148,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) { } @@ -202,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/FalsePatternLib.java b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java index eb7b795..1b78588 100644 --- a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java +++ b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java @@ -2,6 +2,8 @@ package com.falsepattern.lib.internal; import com.falsepattern.lib.config.ConfigException; import com.falsepattern.lib.config.ConfigurationManager; +import com.falsepattern.lib.updates.ModUpdateInfo; +import com.falsepattern.lib.updates.UpdateChecker; import com.falsepattern.lib.util.ResourceUtil; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; @@ -15,10 +17,15 @@ import cpw.mods.fml.common.event.FMLPostInitializationEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import lombok.Getter; import lombok.SneakyThrows; +import lombok.val; import net.minecraft.launchwrapper.Launch; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + /** * Utility class used by FalsePatternLib's internal code. This can change between versions without notice, so do not use * this in your code! @@ -30,6 +37,8 @@ public class FalsePatternLib extends DummyModContainer { @Getter private static final boolean developerEnvironment = (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); + private Future> updateCheckFuture; + public FalsePatternLib() { super(MetadataCollection.from(ResourceUtil.getResourceFromJar("/mcmod.info", FalsePatternLib.class), Tags.MODID) .getMetadataForId(Tags.MODID, null)); @@ -48,8 +57,26 @@ public class FalsePatternLib extends DummyModContainer { } @Subscribe - public void postInit(FMLPostInitializationEvent e) { + public void preInit(FMLPreInitializationEvent e) { + if (LibraryConfig.ENABLE_UPDATE_CHECKER) { + getLog().info("Launching asynchronous update check. I'll check back on it during postInit."); + updateCheckFuture = UpdateChecker.fetchUpdatesAsync("https://falsepattern.com/mc/versions.json"); + } + } + @Subscribe + public void postInit(FMLPostInitializationEvent e) { + if (updateCheckFuture != null && !updateCheckFuture.isCancelled()) { + try { + val updates = updateCheckFuture.get(); + if (updates != null) + for (val update: updates) + update.log(getLog()); + } catch (InterruptedException | ExecutionException ex) { + getLog().warn("Error while checking updates", ex); + } + + } } @SuppressWarnings("UnstableApiUsage") 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/updates/ModUpdateInfo.java b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java index 2b363a0..1a42604 100644 --- a/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java +++ b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java @@ -1,11 +1,22 @@ package com.falsepattern.lib.updates; import lombok.Data; +import lombok.NonNull; +import org.apache.logging.log4j.Logger; @Data public class ModUpdateInfo { - public final String modID; - public final String currentVersion; - public final String latestVersion; - public final String updateURL; + @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 index 7a6837e..a92c3f2 100644 --- a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java +++ b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java @@ -1,27 +1,30 @@ package com.falsepattern.lib.updates; +import com.falsepattern.json.node.JsonNode; 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 cpw.mods.fml.common.Loader; import lombok.val; -import lombok.var; -import java.io.BufferedOutputStream; -import java.io.File; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URL; -import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; public class UpdateChecker { - private static AtomicBoolean jsonLibraryLoaded = new AtomicBoolean(false); + private static AtomicInteger jsonLibraryLoaded = new AtomicInteger(0); private static final ExecutorService asyncExecutor = Executors.newSingleThreadExecutor((runnable) -> { Thread thread = new Thread(runnable); thread.setDaemon(true); @@ -64,22 +67,82 @@ public class UpdateChecker { if (!LibraryConfig.ENABLE_UPDATE_CHECKER) { return null; } - if (!jsonLibraryLoaded.get()) { - DependencyLoader.addMavenRepo("https://maven.falsepattern.com/"); - try { - DependencyLoader.builder() - .groupId("com.falsepattern") - .artifactId("json") - .minVersion(new SemanticVersion(0, 4, 0)) - .maxVersion(new SemanticVersion(0, Integer.MAX_VALUE, Integer.MAX_VALUE)) - .build(); - } catch (Exception e) { - FalsePatternLib.getLog().error("Failed to load json library for update checker!", e); - return null; - } - jsonLibraryLoaded.set(true); + URL URL; + try { + URL = new URL(url); + } catch (MalformedURLException e) { + FalsePatternLib.getLog().error("Invalid URL: {}", url, e); + return null; } - //TODO - 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; } } From f22d81ff42048713d6509b5ff8c2265a7a4e1b82 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 00:15:08 +0200 Subject: [PATCH 04/11] mark the classes API-ready --- .../java/com/falsepattern/lib/updates/ModUpdateInfo.java | 2 ++ .../java/com/falsepattern/lib/updates/UpdateChecker.java | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java index 1a42604..6e23de6 100644 --- a/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java +++ b/src/main/java/com/falsepattern/lib/updates/ModUpdateInfo.java @@ -1,10 +1,12 @@ 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; diff --git a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java index a92c3f2..c9219f2 100644 --- a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java +++ b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java @@ -1,6 +1,7 @@ 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; @@ -23,8 +24,9 @@ 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 AtomicInteger jsonLibraryLoaded = new AtomicInteger(0); + private static final AtomicInteger jsonLibraryLoaded = new AtomicInteger(0); private static final ExecutorService asyncExecutor = Executors.newSingleThreadExecutor((runnable) -> { Thread thread = new Thread(runnable); thread.setDaemon(true); @@ -97,9 +99,7 @@ public class UpdateChecker { return null; } AtomicReference loadedData = new AtomicReference<>(null); - Internet.connect(URL, (ex) -> { - FalsePatternLib.getLog().warn("Failed to check for updates from URL {}", url, ex); - }, (input) -> { + 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); From c49bf13d46c9b6b11e4cd12dd25ac606dd379531 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 00:15:14 +0200 Subject: [PATCH 05/11] update readme --- README.MD | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.MD b/README.MD index 32f1d75..3c7a3da 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: @@ -18,6 +19,11 @@ The contents of the [util](src/main/java/com/falsepattern/lib/util) package so f 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) From fd105861559a16377077481fe2f0f2a157347075 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 00:16:09 +0200 Subject: [PATCH 06/11] update buildscript --- build.gradle | 5 +++-- gradle.properties | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) 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/gradle.properties b/gradle.properties index b2202c7..b675e7a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -68,6 +68,8 @@ 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 From acd210f31718f4b51f01287194facc45c84e8630 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 02:53:23 +0200 Subject: [PATCH 07/11] added update checks for my mods, turned fplib into a regular mod, added async lib --- gradle.properties | 2 +- .../lib/internal/CoreLoadingPlugin.java | 2 +- .../lib/internal/FalsePatternLib.java | 85 +++++++++---------- .../lib/internal/proxy/ClientProxy.java | 83 ++++++++++++++++++ .../lib/internal/proxy/CommonProxy.java | 67 +++++++++++++++ .../lib/updates/UpdateChecker.java | 8 +- .../assets/falsepatternlib/lang/en_US.lang | 10 ++- src/main/resources/pack.mcmeta | 1 + 8 files changed, 204 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java create mode 100644 src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java create mode 100644 src/main/resources/pack.mcmeta diff --git a/gradle.properties b/gradle.properties index b675e7a..8b2dc9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -76,7 +76,7 @@ mixinPreinitConfig = 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/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 1b78588..45995b6 100644 --- a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java +++ b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java @@ -2,87 +2,84 @@ package com.falsepattern.lib.internal; import com.falsepattern.lib.config.ConfigException; import com.falsepattern.lib.config.ConfigurationManager; +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 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.Loader; -import cpw.mods.fml.common.MetadataCollection; +import cpw.mods.fml.common.*; import cpw.mods.fml.common.event.FMLConstructionEvent; -import cpw.mods.fml.common.event.FMLInterModComms; 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.SneakyThrows; 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! */ @SuppressWarnings("UnstableApiUsage") -public class FalsePatternLib extends DummyModContainer { +@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"); - private Future> updateCheckFuture; + @Getter + private static final ExecutorService asyncWorker = Executors.newSingleThreadExecutor((runnable) -> { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + thread.setName(Tags.MODNAME + " Async Worker"); + return thread; + }); + + @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(); - try { - ConfigurationManager.registerConfig(LibraryConfig.class); - } catch (ConfigException ex) { - getLog().error("Failed to register " + Tags.MODNAME + " config!", ex); - } + proxy.construct(e); } - @Subscribe + @Mod.EventHandler public void preInit(FMLPreInitializationEvent e) { - if (LibraryConfig.ENABLE_UPDATE_CHECKER) { - getLog().info("Launching asynchronous update check. I'll check back on it during postInit."); - updateCheckFuture = UpdateChecker.fetchUpdatesAsync("https://falsepattern.com/mc/versions.json"); - } + proxy.preInit(e); } - @Subscribe + @Mod.EventHandler public void postInit(FMLPostInitializationEvent e) { - if (updateCheckFuture != null && !updateCheckFuture.isCancelled()) { - try { - val updates = updateCheckFuture.get(); - if (updates != null) - for (val update: updates) - update.log(getLog()); - } catch (InterruptedException | ExecutionException ex) { - getLog().warn("Error while checking updates", ex); - } - - } + proxy.postInit(e); } - @SuppressWarnings("UnstableApiUsage") - @Override - public boolean registerBus(EventBus bus, LoadController controller) { - bus.register(this); - return 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..9abd853 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java @@ -0,0 +1,83 @@ +package com.falsepattern.lib.internal.proxy; + +import com.falsepattern.lib.internal.FalsePatternLib; +import com.falsepattern.lib.text.FormattedText; +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.Future; + +@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 = FalsePatternLib.getAsyncWorker().submit(() -> { + long start = System.nanoTime(); + 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); + } + } + long end = System.nanoTime(); + FalsePatternLib.getLog().info("Constructed in {} ms", (end - start) / 1000000L); + 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()) { + 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..7960909 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java @@ -0,0 +1,67 @@ +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 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.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 = FalsePatternLib.getAsyncWorker().submit(() -> { + if (!updateCheckFuture.isCancelled()) { + 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/UpdateChecker.java b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java index c9219f2..3f6fd86 100644 --- a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java +++ b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java @@ -27,12 +27,6 @@ import java.util.concurrent.atomic.AtomicReference; @StableAPI(since = "0.8.0") public class UpdateChecker { private static final AtomicInteger jsonLibraryLoaded = new AtomicInteger(0); - private static final ExecutorService asyncExecutor = Executors.newSingleThreadExecutor((runnable) -> { - Thread thread = new Thread(runnable); - thread.setDaemon(true); - thread.setName(Tags.MODNAME + " Asynchronous Update Check Thread"); - return thread; - }); /** * 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. @@ -40,7 +34,7 @@ public class UpdateChecker { * @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 asyncExecutor.submit(() -> fetchUpdates(url)); + return FalsePatternLib.getAsyncWorker().submit(() -> fetchUpdates(url)); } /** diff --git a/src/main/resources/assets/falsepatternlib/lang/en_US.lang b/src/main/resources/assets/falsepatternlib/lang/en_US.lang index bbe9189..31079e4 100644 --- a/src/main/resources/assets/falsepatternlib/lang/en_US.lang +++ b/src/main/resources/assets/falsepatternlib/lang/en_US.lang @@ -1,4 +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. \ No newline at end of file +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 From 4f12582a414d0c9bc6d9441e400037febfc5a2f4 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 02:55:46 +0200 Subject: [PATCH 08/11] moved async worker to public package --- .../lib/internal/FalsePatternLib.java | 8 -------- .../lib/internal/proxy/ClientProxy.java | 3 ++- .../lib/internal/proxy/CommonProxy.java | 3 ++- .../lib/updates/UpdateChecker.java | 5 ++--- .../java/com/falsepattern/lib/util/Async.java | 18 ++++++++++++++++++ 5 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/falsepattern/lib/util/Async.java diff --git a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java index 45995b6..65a0b8e 100644 --- a/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java +++ b/src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java @@ -52,14 +52,6 @@ public class FalsePatternLib { @Getter private static final boolean developerEnvironment = (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); - @Getter - private static final ExecutorService asyncWorker = Executors.newSingleThreadExecutor((runnable) -> { - Thread thread = new Thread(runnable); - thread.setDaemon(true); - thread.setName(Tags.MODNAME + " Async Worker"); - return thread; - }); - @SidedProxy(clientSide = Tags.GROUPNAME + ".internal.proxy.ClientProxy", serverSide = Tags.GROUPNAME + ".internal.proxy.CommonProxy") private static CommonProxy proxy; diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java index 9abd853..1eea1e5 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java @@ -2,6 +2,7 @@ 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; @@ -33,7 +34,7 @@ public class ClientProxy extends CommonProxy { @Override public void postInit(FMLPostInitializationEvent e) { super.postInit(e); - chatFuture = FalsePatternLib.getAsyncWorker().submit(() -> { + chatFuture = Async.asyncWorker.submit(() -> { long start = System.nanoTime(); val updates = updatesFuture.get(); if (updates == null || updates.size() == 0) return null; 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 7960909..8010849 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java @@ -7,6 +7,7 @@ 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; @@ -35,7 +36,7 @@ public class CommonProxy { if (LibraryConfig.ENABLE_UPDATE_CHECKER) { FalsePatternLib.getLog().info("Launching asynchronous update check."); val updateCheckFuture = UpdateChecker.fetchUpdatesAsync("https://falsepattern.com/mc/versions.json"); - updatesFuture = FalsePatternLib.getAsyncWorker().submit(() -> { + updatesFuture = Async.asyncWorker.submit(() -> { if (!updateCheckFuture.isCancelled()) { try { var updates = updateCheckFuture.get(); diff --git a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java index 3f6fd86..1b10185 100644 --- a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java +++ b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java @@ -8,6 +8,7 @@ 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; @@ -18,8 +19,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -34,7 +33,7 @@ public class UpdateChecker { * @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 FalsePatternLib.getAsyncWorker().submit(() -> fetchUpdates(url)); + return Async.asyncWorker.submit(() -> fetchUpdates(url)); } /** 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..eac02ee --- /dev/null +++ b/src/main/java/com/falsepattern/lib/util/Async.java @@ -0,0 +1,18 @@ +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; + +@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; + }); +} From 5cc487f9a9f949dd586ab16811db3de37171a486 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 03:00:23 +0200 Subject: [PATCH 09/11] add async library documentation --- README.MD | 1 + src/main/java/com/falsepattern/lib/util/Async.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/README.MD b/README.MD index 3c7a3da..4b6ce91 100644 --- a/README.MD +++ b/README.MD @@ -15,6 +15,7 @@ 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. diff --git a/src/main/java/com/falsepattern/lib/util/Async.java b/src/main/java/com/falsepattern/lib/util/Async.java index eac02ee..5313d30 100644 --- a/src/main/java/com/falsepattern/lib/util/Async.java +++ b/src/main/java/com/falsepattern/lib/util/Async.java @@ -7,6 +7,11 @@ 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) -> { From 0703368cb4109370b0ee48734817df45c18336f3 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 03:01:29 +0200 Subject: [PATCH 10/11] remove debug code --- .../java/com/falsepattern/lib/internal/proxy/ClientProxy.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java index 1eea1e5..ab6ad54 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java @@ -35,7 +35,6 @@ public class ClientProxy extends CommonProxy { public void postInit(FMLPostInitializationEvent e) { super.postInit(e); chatFuture = Async.asyncWorker.submit(() -> { - long start = System.nanoTime(); val updates = updatesFuture.get(); if (updates == null || updates.size() == 0) return null; val updateText = new ArrayList(FormattedText.parse(I18n.format("falsepatternlib.chat.updatesavailable")).toChatText()); @@ -61,8 +60,6 @@ public class ClientProxy extends CommonProxy { updateText.addAll(post); } } - long end = System.nanoTime(); - FalsePatternLib.getLog().info("Constructed in {} ms", (end - start) / 1000000L); return updateText; }); } From d19d310fd96882a0f9927d50313bace048f73057 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Fri, 10 Jun 2022 03:08:15 +0200 Subject: [PATCH 11/11] deadlock avoidance --- .../lib/internal/proxy/ClientProxy.java | 67 ++++++++++++------- .../lib/internal/proxy/CommonProxy.java | 19 ++++-- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java index ab6ad54..a334361 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java @@ -19,7 +19,9 @@ 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 { @@ -34,33 +36,46 @@ public class ClientProxy extends CommonProxy { @Override public void postInit(FMLPostInitializationEvent e) { super.postInit(e); - chatFuture = Async.asyncWorker.submit(() -> { - 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); + 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; } - return updateText; }); } @@ -70,7 +85,7 @@ public class ClientProxy extends CommonProxy { !(e.entity instanceof EntityPlayerSP)) return; val player = (EntityPlayerSP) e.entity; try { - for (val line: chatFuture.get()) { + for (val line: chatFuture.get(1, TimeUnit.SECONDS)) { player.addChatMessage(line); } chatFuture = null; 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 8010849..5bb0d3f 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java @@ -16,6 +16,7 @@ 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; @@ -36,12 +37,22 @@ public class CommonProxy { 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(() -> { - if (!updateCheckFuture.isCancelled()) { + 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) { + for (val update : updates) { update.log(FalsePatternLib.getLog()); } } else if (updates == null) { @@ -54,8 +65,8 @@ public class CommonProxy { } catch (InterruptedException | ExecutionException ex) { FalsePatternLib.getLog().warn("Error while checking updates", ex); } + return null; } - return null; }); } }