diff --git a/build.gradle b/build.gradle deleted file mode 100644 index d9639b2..0000000 --- a/build.gradle +++ /dev/null @@ -1,100 +0,0 @@ -plugins { - id 'fabric-loom' version '1.0.17' - id 'io.github.juuxel.loom-quiltflower' version '1.8.0' - id 'maven-publish' -} - -version = project.mod_version -group = project.maven_group - -sourceSets { - api { - compileClasspath += main.compileClasspath - } -} - -repositories { - maven { - url "https://maven.lenni0451.net/releases" - } -} - -dependencies { - // To change the versions see the gradle.properties file - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - - include implementation("net.lenni0451:Reflect:1.0.2") - include implementation("com.formdev:flatlaf:3.0") -} - -jar { - includeEmptyDirs = false -} - -task apiJar(type: Jar) { - from(sourceSets.api.output) - archiveClassifier.set("api") -} -tasks.build.dependsOn(apiJar) - -processResources { - inputs.property "version", project.version - filteringCharset "UTF-8" - - filesMatching("fabric.mod.json") { - expand "version": project.version - } -} - -def targetJavaVersion = 8 -tasks.withType(JavaCompile).configureEach { - // ensure that the encoding is set to UTF-8, no matter what the system default is - // this fixes some edge cases with special characters not displaying correctly - // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html - // If Javadoc is generated, this must be specified in that task too. - it.options.encoding = "UTF-8" - if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { - it.options.release = targetJavaVersion - } -} - -java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - if (JavaVersion.current() < javaVersion) { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - } - archivesBaseName = project.archives_base_name - // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task - // if it is present. - // If you remove this line, sources will not be generated. - withSourcesJar() -} - -tasks.sourcesJar { - from(sourceSets.api.allSource) -} - -jar { - from("LICENSE") { - rename { "${it}_${project.archivesBaseName}" } - } -} - -// configure the maven publication -publishing { - publications { - mavenJava(MavenPublication) { - from components.java - } - } - - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b8976a4 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,96 @@ +plugins { + id("fabric-loom") version "1.0.17" + id("io.github.juuxel.loom-quiltflower") version "1.8.0" + `maven-publish` +} + +operator fun Project.get(prop: String) = extra[prop] as String + +version = project["mod_version"] +group = project["maven_group"] +base.archivesName.set(project["archives_base_name"]) + +val api by sourceSets.registering { + compileClasspath += sourceSets.main.get().compileClasspath +} + +repositories { + maven("https://maven.lenni0451.net/releases") +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft("com.mojang:minecraft:${project.extra["minecraft_version"]}") + mappings("net.fabricmc:yarn:${project.extra["yarn_mappings"]}:v2") + modImplementation("net.fabricmc:fabric-loader:${project.extra["loader_version"]}") + + include(implementation("net.lenni0451:Reflect:1.0.2")!!) + include(implementation("com.formdev:flatlaf:3.0")!!) +} + +tasks.jar { + includeEmptyDirs = false +} + +val apiJar by tasks.registering(Jar::class) { + from(api.get().output) + archiveClassifier.set("api") +} +tasks.build { + dependsOn(apiJar) +} + +tasks.processResources { + inputs.property("version", project.version) + filteringCharset = "UTF-8" + + filesMatching("fabric.mod.json") { + expand("version" to project.version) + } +} + +val targetJavaVersion = 8 +tasks.withType().configureEach { + // ensure that the encoding is set to UTF-8, no matter what the system default is + // this fixes some edge cases with special characters not displaying correctly + // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html + // If Javadoc is generated, this must be specified in that task too. + options.encoding = "UTF-8" + if (JavaVersion.current().isJava10Compatible) { + options.release.set(targetJavaVersion) + } +} + +java { + val javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion.set(JavaLanguageVersion.of(targetJavaVersion)) + } + withSourcesJar() +} + +tasks.named("sourcesJar") { + from(api.get().allSource) +} + +tasks.jar { + from("LICENSE") { + rename { "${it}_${base.archivesName.get()}" } + } +} + +// configure the maven publication +publishing { + publications { + create("maven") { + from(components["java"]) + artifact(apiJar.get().archiveFile) { + classifier = "api" + } + } + } + + repositories { + mavenLocal() + } +} diff --git a/gradle.properties b/gradle.properties index cc3a484..ce083d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,6 @@ minecraft_version=1.19.2 yarn_mappings=1.19.2+build.28 loader_version=0.14.12 # Mod Properties -mod_version=1.0.3 +mod_version=1.0.4 maven_group=io.github.gaming32 archives_base_name=mod-loading-screen diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index f91a4fe..0000000 --- a/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - gradlePluginPortal() - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..eb34993 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + maven("https://maven.fabricmc.net/") { + name = "Fabric" + } + gradlePluginPortal() + } +} + +rootProject.name = "mod-loading-screen" diff --git a/src/api/java/io/github/gaming32/modloadingscreen/api/AvailableFeatures.java b/src/api/java/io/github/gaming32/modloadingscreen/api/AvailableFeatures.java index 01fdeb9..580f0e4 100644 --- a/src/api/java/io/github/gaming32/modloadingscreen/api/AvailableFeatures.java +++ b/src/api/java/io/github/gaming32/modloadingscreen/api/AvailableFeatures.java @@ -2,7 +2,7 @@ import java.util.StringJoiner; -public class AvailableFeatures { +public final class AvailableFeatures { /** * @since 1.0.3 * @see LoadingScreenApi#getFinalEntrypoints @@ -36,6 +36,13 @@ public class AvailableFeatures { */ public static final long OPEN_CHECK = 1L << 4; + /** + * @since 1.0.4 + * @see LoadingScreenApi#getCustomProgressBar + * @see CustomProgressBar + */ + public static final long CUSTOM_PROGRESS_BARS = 1L << 5; + /** * All the features that should be available on version 1.0.3. * @@ -43,8 +50,18 @@ public class AvailableFeatures { */ public static final long V1_0_3 = FINAL_ENTRYPOINTS | HEADLESS_CHECK | IPC_CHECK | GET_PROGRESS | OPEN_CHECK; + /** + * All the features that should be available on version 1.0.4. + * + * @since 1.0.4 + */ + public static final long V1_0_4 = V1_0_3 | CUSTOM_PROGRESS_BARS; + private static final long MIN_FEATURE = FINAL_ENTRYPOINTS; - private static final long MAX_FEATURE = OPEN_CHECK; + private static final long MAX_FEATURE = CUSTOM_PROGRESS_BARS; + + private AvailableFeatures() { + } public static String toString(long features) { if (Long.bitCount(features) <= 1L) { @@ -59,6 +76,8 @@ public static String toString(long features) { return "GET_PROGRESS"; case (int)OPEN_CHECK: return "OPEN_CHECK"; + case (int)CUSTOM_PROGRESS_BARS: + return "CUSTOM_PROGRESS_BARS"; } return ""; } diff --git a/src/api/java/io/github/gaming32/modloadingscreen/api/CustomProgressBar.java b/src/api/java/io/github/gaming32/modloadingscreen/api/CustomProgressBar.java new file mode 100644 index 0000000..764a7a1 --- /dev/null +++ b/src/api/java/io/github/gaming32/modloadingscreen/api/CustomProgressBar.java @@ -0,0 +1,155 @@ +package io.github.gaming32.modloadingscreen.api; + +import java.io.Closeable; +import java.util.Objects; + +/** + * A reference to a custom progress bar. Recommended to be used with try-with-resources. + * + * @see LoadingScreenApi#getCustomProgressBar + */ +public final class CustomProgressBar implements Closeable { + private final String id; + private final boolean isReal; + private boolean closed; + + private String title; + private int progress; + private int minimum; + private int maximum; + + CustomProgressBar(String id, boolean isReal, String title, int maximum) { + this.id = id; + this.isReal = isReal; + this.title = title; + this.maximum = maximum; + } + + public String getId() { + return id; + } + + public boolean isClosed() { + return !LoadingScreenApi.CUSTOM_PROGRESS_BARS.containsKey(id); + } + + private void checkClosed() { + if (isClosed()) { + throw new IllegalStateException("CustomProgressBar is closed!"); + } + } + + /** + * Close the progress bar and remove it from the loading screen. + */ + @Override + public void close() { + if (LoadingScreenApi.CUSTOM_PROGRESS_BARS.remove(id) == this) { + LoadingScreenApi.customProgressBarOp(id, "close"); + } + } + + /** + * Sets the progress of the progress bar. The new progress will be clamped to the {@code [minimum, maximum]} range. + * @param progress The new progress. + */ + public void setProgress(int progress) { + progress = Math.min(maximum, Math.max(minimum, progress)); + checkClosed(); + this.progress = progress; + LoadingScreenApi.customProgressBarOp(id, "progress", Integer.toString(progress)); + } + + /** + * Gets the current progress of the progress bar. + */ + public int getProgress() { + return progress; + } + + /** + * Steps the progress bar forward one step. + * + * @return The new progress of the progress bar, which may be clamped into range. + */ + public int step() { + return step(1); + } + + /** + * Steps the progress bar forward {@code n} steps. + * + * @return The new progress of the progress bar, which may be clamped into range. + */ + public int step(int n) { + setProgress(progress + n); + return progress; + } + + /** + * Sets the maximum value of the progress bar. + * @throws IllegalArgumentException If {@code maximum < minimum} + */ + public void setMaximum(int maximum) { + if (maximum < minimum) { + throw new IllegalArgumentException("maximum may not be less than minimum"); + } + checkClosed(); + this.maximum = maximum; + LoadingScreenApi.customProgressBarOp(id, "maximum", Integer.toString(maximum)); + } + + /** + * Gets the maximum value of the progress bar. + */ + public int getMaximum() { + return maximum; + } + + + /** + * Sets the minimum value of the progress bar. + * @throws IllegalArgumentException If {@code minimum > maximum} + */ + public void setMinimum(int minimum) { + if (minimum > maximum) { + throw new IllegalArgumentException("minimum may not be greater than maximum"); + } + checkClosed(); + this.minimum = minimum; + LoadingScreenApi.customProgressBarOp(id, "minimum", Integer.toString(minimum)); + } + + + /** + * Gets the minimum value of the progress bar. + */ + public int getMinimum() { + return minimum; + } + + /** + * Sets the display text of the progress bar. + */ + public void setTitle(String title) { + Objects.requireNonNull(title, "title"); + checkClosed(); + this.title = title; + LoadingScreenApi.customProgressBarOp(id, "title", title); + } + + /** + * Gets the display text of the progress bar. + */ + public String getTitle() { + return title; + } + + /** + * Returns whether the progress bar is a real progress bar that can show up. This returns {@code true} if a version + * of Mod Loading Screen that supports custom progress bars is installed. + */ + public boolean isReal() { + return isReal; + } +} diff --git a/src/api/java/io/github/gaming32/modloadingscreen/api/LoadingScreenApi.java b/src/api/java/io/github/gaming32/modloadingscreen/api/LoadingScreenApi.java index 33ee70f..e87bcd1 100644 --- a/src/api/java/io/github/gaming32/modloadingscreen/api/LoadingScreenApi.java +++ b/src/api/java/io/github/gaming32/modloadingscreen/api/LoadingScreenApi.java @@ -5,6 +5,7 @@ import net.fabricmc.loader.api.VersionParsingException; import net.fabricmc.loader.api.metadata.version.VersionPredicate; import net.fabricmc.loader.impl.entrypoint.EntrypointUtils; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; @@ -12,19 +13,20 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Consumer; -public class LoadingScreenApi { +public final class LoadingScreenApi { + static final Map CUSTOM_PROGRESS_BARS = new HashMap<>(); + private static final long FEATURES; private static final MethodHandle FINAL_ENTRYPOINTS; private static final MethodHandle IS_HEADLESS; private static final MethodHandle ENABLE_IPC; private static final MethodHandle PROGRESS; private static final MethodHandle IS_OPEN; + private static final MethodHandle CREATE_CUSTOM_PROGRESS_BAR; + private static final MethodHandle CUSTOM_PROGRESS_BAR_OP; static { long features = 0; @@ -33,6 +35,8 @@ public class LoadingScreenApi { MethodHandle enableIpc = null; MethodHandle progress = null; MethodHandle isOpen = null; + MethodHandle createCustomProgressBar = null; + MethodHandle customProgressBarOp = null; final MethodHandles.Lookup lookup = MethodHandles.lookup(); try { @@ -74,6 +78,23 @@ public class LoadingScreenApi { } catch (Exception e) { loadFailed(">=1.0.3", AvailableFeatures.OPEN_CHECK, e); } + + try { + createCustomProgressBar = lookup.findStatic( + alsClass, "createCustomProgressBar", + MethodType.methodType(void.class, String.class, String.class, int.class) + ); + customProgressBarOp = lookup.findStatic( + alsClass, "customProgressBarOp", + MethodType.methodType(void.class, String[].class) + ); + features |= AvailableFeatures.CUSTOM_PROGRESS_BARS; + } catch (Exception e) { + createCustomProgressBar = null; + loadFailed(">=1.0.4", AvailableFeatures.CUSTOM_PROGRESS_BARS, e); + } + + System.out.println("[ModLoadingScreen] API loaded with features: " + AvailableFeatures.toString(features)); } catch (Exception e) { final String message = "[ModLoadingScreen] Failed to load LoadingScreenApi. No API features are available."; if (FabricLoader.getInstance().isModLoaded("mod-loading-screen")) { @@ -83,7 +104,7 @@ public class LoadingScreenApi { // This API could be called with Mod Loading Screen simply absent, in which case this is *not* an error // condition System.out.println(message); - System.out.println("[ModLoadingScreen] This is not an error, because Mod Loading Screen isn't installed anyway."); + System.out.println("[ModLoadingScreen] This is not an error, because Mod Loading Screen is not installed."); } } @@ -93,8 +114,8 @@ public class LoadingScreenApi { ENABLE_IPC = enableIpc; PROGRESS = progress; IS_OPEN = isOpen; - - System.out.println("[ModLoadingScreen] API loaded with features: " + AvailableFeatures.toString(FEATURES)); + CREATE_CUSTOM_PROGRESS_BAR = createCustomProgressBar; + CUSTOM_PROGRESS_BAR_OP = customProgressBarOp; } private static void loadFailed(String mlsVersionRequired, long feature, Exception e) { @@ -119,6 +140,9 @@ private static void loadFailed(String mlsVersionRequired, long feature, Exceptio }); } + private LoadingScreenApi() { + } + /** * Returns the features of the API that are available to use, as a bit mask of flags from * {@link AvailableFeatures}. Any features not available will default to no-op fallback implementations. @@ -178,7 +202,7 @@ public static Set getFinalEntrypoints() { return new HashSet<>(); } try { - return (Set)FINAL_ENTRYPOINTS.invoke(); + return (Set)FINAL_ENTRYPOINTS.invokeExact(); } catch (Throwable t) { return rethrow(t); } @@ -201,7 +225,7 @@ public static boolean isHeadless() { return GraphicsEnvironment.isHeadless(); } try { - return (boolean)IS_HEADLESS.invoke(); + return (boolean)IS_HEADLESS.invokeExact(); } catch (Throwable t) { return rethrow(t); } @@ -228,7 +252,7 @@ public static boolean isUsingIpc() { return false; } try { - return (boolean)ENABLE_IPC.invoke(); + return (boolean)ENABLE_IPC.invokeExact(); } catch (Throwable t) { return rethrow(t); } @@ -240,7 +264,7 @@ private static Map getAllProgress() { return Collections.emptyMap(); } try { - return (Map)PROGRESS.invoke(); + return (Map)PROGRESS.invokeExact(); } catch (Throwable t) { return rethrow(t); } @@ -250,6 +274,8 @@ private static Map getAllProgress() { * Returns an {@link Set} of progress bar names. This will be updated dynamically when bars are updated. If * {@link #getFeatures} doesn't return {@link AvailableFeatures#GET_PROGRESS}, this will return an empty set. * + * @apiNote Custom progress bars start with "custom:", followed by their ID. + * * @see AvailableFeatures#GET_PROGRESS * * @since 1.0.3 @@ -267,16 +293,17 @@ public static Set getActiveProgressBars() { * * @param barName The name of the progress bar. In the case of entrypoints, this is the name of the entrypoint. * + * @apiNote Custom progress bars start with "custom:", followed by their ID. + * * @see AvailableFeatures#GET_PROGRESS * * @since 1.0.3 */ @Nullable - public static Integer getProgress(String barName) { + public static Integer getProgress(@NotNull String barName) { return getAllProgress().get(barName); } - // TODO: Put a note in this when custom progress bars are added. /** * Returns whether a loading screen is currently active. If {@link #getFeatures} doesn't return * {@link AvailableFeatures#OPEN_CHECK}, this will always return {@code false}. @@ -292,12 +319,50 @@ public static boolean isOpen() { return false; } try { - return (boolean)IS_OPEN.invoke(); + return (boolean)IS_OPEN.invokeExact(); } catch (Throwable t) { return rethrow(t); } } + /** + * Creates a custom progress bar. + * @param title The title of the progress bar. This is the full string to display. + * @param max The maximum value of the progress bar. It will not be removed automatically when this is + * reached. + * @return The {@link CustomProgressBar} reference to the progress bar. + */ + public static CustomProgressBar getCustomProgressBar(@NotNull String id, @NotNull String title, int max) { + CustomProgressBar bar = CUSTOM_PROGRESS_BARS.get(id); + if (bar == null) { + CUSTOM_PROGRESS_BARS.put(id, bar = createCustomProgressBar(id, title, max)); + } + return bar; + } + + private static CustomProgressBar createCustomProgressBar(String id, String title, int max) { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(title, "title"); + if (CREATE_CUSTOM_PROGRESS_BAR == null) { + return new CustomProgressBar(id, false, title, max); + } + try { + CREATE_CUSTOM_PROGRESS_BAR.invokeExact(id, title, max); + } catch (Throwable t) { + rethrow(t); + } + return new CustomProgressBar(id, true, title, max); + } + + static void customProgressBarOp(String... args) { + if (CUSTOM_PROGRESS_BAR_OP == null) return; + try { + CUSTOM_PROGRESS_BAR_OP.invokeExact(args); + } catch (Throwable t) { + rethrow(t); + } + } + @SuppressWarnings("unchecked") private static R rethrow(Throwable t) throws T { throw (T)t; diff --git a/src/main/java/io/github/gaming32/modloadingscreen/ActualLoadingScreen.java b/src/main/java/io/github/gaming32/modloadingscreen/ActualLoadingScreen.java index eba0a74..1d22697 100644 --- a/src/main/java/io/github/gaming32/modloadingscreen/ActualLoadingScreen.java +++ b/src/main/java/io/github/gaming32/modloadingscreen/ActualLoadingScreen.java @@ -283,10 +283,60 @@ public static void maybeCloseAfter(String type) { !FabricLoader.getInstance().getEntrypointContainers(type + "_init", Object.class).isEmpty() ) ) return; - sendIpc(4); + sendIpc(255); close(); } + public static void createCustomProgressBar(String id, String title, int max) { + final String fullId = "custom:" + id; + progress.put(fullId, 0); + + if (sendIpc(4, id, title, Integer.toString(max))) return; + + final JProgressBar progressBar = new JProgressBar(0, max); + progressBar.setStringPainted(true); + progressBar.setString(title); + progressBars.put(fullId, progressBar); + label.add(progressBar, BorderLayout.SOUTH); + dialog.pack(); + } + + public static void customProgressBarOp(String... args) { + final String fullId = "custom:" + args[0]; + switch (args[1]) { + case "progress": + progress.put(fullId, Integer.parseInt(args[2])); + break; + case "close": + progress.remove(fullId); + break; + } + + if (sendIpc(5, args)) return; + + if (args[1].equals("close")) { + label.remove(progressBars.remove(fullId)); + dialog.pack(); + return; + } + + final JProgressBar progressBar = progressBars.get(fullId); + switch (args[1]) { + case "progress": + progressBar.setValue(Integer.parseInt(args[2])); + break; + case "maximum": + progressBar.setMaximum(Integer.parseInt(args[2])); + break; + case "minimum": + progressBar.setMinimum(Integer.parseInt(args[2])); + break; + case "title": + progressBar.setString(args[2]); + break; + } + } + private static void close() { if (memoryThread != null) { memoryThread.interrupt(); @@ -400,7 +450,7 @@ public static void main(String[] args) { final DataInputStream in = new DataInputStream(System.in); mainLoop: while (true) { - final int packetId = in.readByte(); + final int packetId = in.readByte() & 0xff; final String[] packetArgs = new String[in.readByte()]; for (int i = 0; i < packetArgs.length; i++) { packetArgs[i] = in.readUTF(); @@ -419,6 +469,12 @@ public static void main(String[] args) { updateMemoryUsage0(Long.parseLong(packetArgs[0]), Long.parseLong(packetArgs[1])); break; case 4: + createCustomProgressBar(packetArgs[0], packetArgs[1], Integer.parseInt(packetArgs[2])); + break; + case 5: + customProgressBarOp(packetArgs); + break; + case 255: break mainLoop; } }