diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/DisplayRemote.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/DisplayRemote.java new file mode 100644 index 000000000..7e3e62d9b --- /dev/null +++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/DisplayRemote.java @@ -0,0 +1,10 @@ +package net.fabricmc.minecraft.test.info; + +import net.fabricmc.loader.api.info.ProgressBar; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +public interface DisplayRemote extends Remote { + void progressBars(ProgressBar[] progressBars) throws RemoteException; +} diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/DisplayRemoteObject.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/DisplayRemoteObject.java new file mode 100644 index 000000000..9b142df3f --- /dev/null +++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/DisplayRemoteObject.java @@ -0,0 +1,68 @@ +package net.fabricmc.minecraft.test.info; + +import net.fabricmc.loader.api.info.ProgressBar; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JProgressBar; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; + +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +public class DisplayRemoteObject extends UnicastRemoteObject implements DisplayRemote { + private ProgressBar[] progressBars = new ProgressBar[0]; + protected DisplayRemoteObject() throws RemoteException { + new Thread(() -> { + JFrame frame = new JFrame(); + frame.setVisible(false); + System.setProperty("apple.awt.application.appearance", "system"); + System.setProperty("apple.awt.application.name", "Loading Window"); + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | + UnsupportedLookAndFeelException e) { + throw new RuntimeException(e); + } + frame.setSize(480, 300); + frame.setVisible(true); + ConcurrentHashMap cache = new ConcurrentHashMap<>(); + try { + while (true) { + int i = 0; + for (ProgressBar progressBar : progressBars) { + JProgressBar jProgressBar = cache.computeIfAbsent(progressBar.title(), f -> { + JProgressBar bar = progressBar.steps() == 0 ? new JProgressBar(0, 1) : new JProgressBar(0, progressBar.steps()); + frame.add(bar); + return bar; + }); + jProgressBar.setStringPainted(true); + if (progressBar.steps() == 0) jProgressBar.setValue(1); + jProgressBar.setString(progressBar.title() + " (" + progressBar.progress() + "/" + progressBar.steps() + ")"); + jProgressBar.setBounds(10, 30 + i * 10, 300, 30 * 10); + if (progressBar.steps() != 0) jProgressBar.setValue(progressBar.progress()); + i++; + } + frame.repaint(); + cache.forEach((s, jProgressBar) -> { + if (Arrays.stream(progressBars).map(ProgressBar::title).noneMatch(s::equals)) { + frame.remove(jProgressBar); + cache.remove(s); + } + }); + } + } catch (Throwable t) { + t.printStackTrace(); + } + }).start(); + } + + @Override + public void progressBars(ProgressBar[] progressBars) { + this.progressBars = progressBars; + } +} diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/EarlyDisplayEntrypoint.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/EarlyDisplayEntrypoint.java new file mode 100644 index 000000000..d7695b36c --- /dev/null +++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/EarlyDisplayEntrypoint.java @@ -0,0 +1,138 @@ +package net.fabricmc.minecraft.test.info; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; +import net.fabricmc.loader.api.info.EntrypointInfoReceiver; +import net.fabricmc.loader.api.info.EntrypointInvocationSession; +import net.fabricmc.loader.api.info.Message; +import net.fabricmc.loader.api.info.ModMessageReceiver; +import net.fabricmc.loader.api.info.ProgressBar; +import net.fabricmc.loader.impl.util.LoaderUtil; +import net.fabricmc.loader.impl.util.UrlUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.Arrays; +import java.util.concurrent.ConcurrentLinkedDeque; + +@SuppressWarnings("InfiniteLoopStatement") +public class EarlyDisplayEntrypoint implements PreLaunchEntrypoint, Runnable, EntrypointInfoReceiver, ModMessageReceiver { + + private DisplayRemote remote; + + @Override + public void onPreLaunch() { + try { + openForked(); + Thread.sleep(1000); + Registry registry = LocateRegistry.getRegistry(null, 1099); + System.out.println(Arrays.toString(registry.list())); + remote = (DisplayRemote) registry.lookup("Remote"); + } catch (NotBoundException | InterruptedException | IOException e) { + throw new RuntimeException(e); + } + new Thread(this).start(); + + new Thread(() -> { + ProgressBar progressBar = FabricLoader.getInstance().getModMessageSession().progressBar("Test Progress Bar", 1000); + for (int i = 0; i < 1000; i++) { + progressBar.increment(); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + progressBar.close(); + }).start(); + FabricLoader.getInstance().invokeEntrypoints("test", Runnable.class, Runnable::run); + } + + private static void openForked() throws IOException, InterruptedException { + Path javaBinDir = LoaderUtil.normalizePath(Paths.get(System.getProperty("java.home"), "bin")); + String[] executables = { "javaw.exe", "java.exe", "java" }; + Path javaPath = null; + + for (String executable : executables) { + Path path = javaBinDir.resolve(executable); + + if (Files.isRegularFile(path)) { + javaPath = path; + break; + } + } + + if (javaPath == null) throw new RuntimeException("can't find java executable in " + javaBinDir); + Process process = new ProcessBuilder(javaPath.toString(), "-Xmx1G", "-cp", UrlUtil.getCodeSource(EarlyDisplayInit.class).toString() + ":" + UrlUtil.LOADER_CODE_SOURCE.toString(), EarlyDisplayInit.class.getName()) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + + final Thread shutdownHook = new Thread(process::destroy); + + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + private static final ConcurrentLinkedDeque progressBars = new ConcurrentLinkedDeque<>(); + private static final ConcurrentLinkedDeque pinnedMessages = new ConcurrentLinkedDeque<>(); + private static final ConcurrentLinkedDeque messages = new ConcurrentLinkedDeque<>(); + + @Override + public void run() { + while (true) { + progressBars.removeIf(ProgressBar::isCompleted); + try { + remote.progressBars(progressBars.toArray(new ProgressBar[0])); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public EntrypointInvocationSession createEntrypointInvocationSession(String entrypointName, int size) { + TestEntrypointInvocationSession testEntrypointInvocationSession = new TestEntrypointInvocationSession(); + testEntrypointInvocationSession.progressBar = FabricLoader.getInstance().getModMessageSession().progressBar(entrypointName, size); + return testEntrypointInvocationSession; + } + + @Override + public void progressBar(ProgressBar progressBar) { + synchronized (progressBars) { + progressBars.add(progressBar); + } + } + + @Override + public void message(Message message) { + synchronized (pinnedMessages) { + pinnedMessages.add(message); + } + } + + private static class TestEntrypointInvocationSession implements EntrypointInvocationSession { + private ProgressBar progressBar; + + @Override + public void preInvoke(ModContainer mod, int index, int size) { + progressBar.set(index); + } + + @Override + public Throwable error(ModContainer mod, Throwable throwable, int index, int size) { + return throwable; + } + + @Override + public void close() { + progressBar.close(); + } + } +} diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/EarlyDisplayInit.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/EarlyDisplayInit.java new file mode 100644 index 000000000..ef9871901 --- /dev/null +++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/EarlyDisplayInit.java @@ -0,0 +1,12 @@ +package net.fabricmc.minecraft.test.info; + +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class EarlyDisplayInit { + public static void main(String[] args) throws RemoteException { + Registry registry = LocateRegistry.createRegistry(1099); + registry.rebind("Remote", new DisplayRemoteObject()); + } +} diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/TestEntrypoint.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/TestEntrypoint.java new file mode 100644 index 000000000..feadd440d --- /dev/null +++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/info/TestEntrypoint.java @@ -0,0 +1,12 @@ +package net.fabricmc.minecraft.test.info; + +public class TestEntrypoint implements Runnable { + @Override + public void run() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/minecraft/minecraft-test/src/main/resources/fabric.mod.json b/minecraft/minecraft-test/src/main/resources/fabric.mod.json index 2a07a0ee6..1529c6b0e 100644 --- a/minecraft/minecraft-test/src/main/resources/fabric.mod.json +++ b/minecraft/minecraft-test/src/main/resources/fabric.mod.json @@ -12,6 +12,40 @@ "entrypoints": { "main": [ "net.fabricmc.minecraft.test.TestEntrypoint" + ], + "preLaunch": [ + "net.fabricmc.minecraft.test.info.EarlyDisplayEntrypoint" + ], + "entrypointReceiver": [ + "net.fabricmc.minecraft.test.info.EarlyDisplayEntrypoint" + ], + "modMessageReceiver": [ + "net.fabricmc.minecraft.test.info.EarlyDisplayEntrypoint" + ], + "test": [ + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint", + "net.fabricmc.minecraft.test.info.TestEntrypoint" ] } } diff --git a/src/main/java/net/fabricmc/loader/api/FabricLoader.java b/src/main/java/net/fabricmc/loader/api/FabricLoader.java index e83b54033..d6655934b 100644 --- a/src/main/java/net/fabricmc/loader/api/FabricLoader.java +++ b/src/main/java/net/fabricmc/loader/api/FabricLoader.java @@ -25,6 +25,8 @@ import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; +import net.fabricmc.loader.api.info.EntrypointInfoReceiver; +import net.fabricmc.loader.api.info.ModMessageSender; import net.fabricmc.loader.impl.FabricLoaderImpl; /** @@ -236,4 +238,7 @@ static FabricLoader getInstance() { * @return the launch arguments for the game */ String[] getLaunchArguments(boolean sanitize); + + List getEntrypointInfoReceivers(); + ModMessageSender getModMessageSession(); } diff --git a/src/main/java/net/fabricmc/loader/api/info/EntrypointInfoReceiver.java b/src/main/java/net/fabricmc/loader/api/info/EntrypointInfoReceiver.java new file mode 100644 index 000000000..8e3a040b2 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/info/EntrypointInfoReceiver.java @@ -0,0 +1,5 @@ +package net.fabricmc.loader.api.info; + +public interface EntrypointInfoReceiver { + EntrypointInvocationSession createEntrypointInvocationSession(String entrypointName, int size); +} diff --git a/src/main/java/net/fabricmc/loader/api/info/EntrypointInvocationSession.java b/src/main/java/net/fabricmc/loader/api/info/EntrypointInvocationSession.java new file mode 100644 index 000000000..23cd64119 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/info/EntrypointInvocationSession.java @@ -0,0 +1,15 @@ +package net.fabricmc.loader.api.info; + +import net.fabricmc.loader.api.ModContainer; + +import java.io.Closeable; + +public interface EntrypointInvocationSession extends Closeable { + /** + * Called before an entrypoint is invoked. + */ + void preInvoke(ModContainer mod, int index, int size); + Throwable error(ModContainer mod, Throwable throwable, int index, int size); + @Override + void close(); +} diff --git a/src/main/java/net/fabricmc/loader/api/info/Message.java b/src/main/java/net/fabricmc/loader/api/info/Message.java new file mode 100644 index 000000000..b96140fa5 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/info/Message.java @@ -0,0 +1,14 @@ +package net.fabricmc.loader.api.info; + +import java.io.Closeable; + +public interface Message extends Closeable { + boolean pinned(); + /** + * Sets a new message. Should only be used when pinned. + */ + void title(String message); + String title(); + @Override + void close(); +} diff --git a/src/main/java/net/fabricmc/loader/api/info/ModMessageReceiver.java b/src/main/java/net/fabricmc/loader/api/info/ModMessageReceiver.java new file mode 100644 index 000000000..550257889 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/info/ModMessageReceiver.java @@ -0,0 +1,6 @@ +package net.fabricmc.loader.api.info; + +public interface ModMessageReceiver { + void progressBar(ProgressBar progressBar); + void message(Message message); +} diff --git a/src/main/java/net/fabricmc/loader/api/info/ModMessageSender.java b/src/main/java/net/fabricmc/loader/api/info/ModMessageSender.java new file mode 100644 index 000000000..81474ed97 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/info/ModMessageSender.java @@ -0,0 +1,7 @@ +package net.fabricmc.loader.api.info; + +public interface ModMessageSender { + Message message(String string); + + ProgressBar progressBar(String name, int steps); +} diff --git a/src/main/java/net/fabricmc/loader/api/info/ProgressBar.java b/src/main/java/net/fabricmc/loader/api/info/ProgressBar.java new file mode 100644 index 000000000..a0483d209 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/info/ProgressBar.java @@ -0,0 +1,34 @@ +package net.fabricmc.loader.api.info; + +import org.jetbrains.annotations.Nullable; + +import java.io.Closeable; + +/** + * A progress bar with 0 is an indeterminate progress bar which can only be completed. + */ +public interface ProgressBar extends Closeable { + void increment(); + float percentage(); + int progress(); + + /** + * @return The total amount of steps the progress bar has. + */ + int steps(); + void set(int steps); + String title(); + void title(String title); + @Override + void close(); + + boolean isCompleted(); + + /** + * Create a child progress bar. + */ + ProgressBar progressBar(String name, int steps); + + @Nullable + ProgressBar getParent(); +} diff --git a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java index 9ab01efd3..dbc0cb5d8 100644 --- a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java +++ b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java @@ -35,6 +35,12 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import net.fabricmc.loader.api.info.EntrypointInfoReceiver; +import net.fabricmc.loader.api.info.EntrypointInvocationSession; +import net.fabricmc.loader.api.info.ModMessageSender; + +import net.fabricmc.loader.impl.info.ModMessageSenderImpl; + import org.objectweb.asm.Opcodes; import net.fabricmc.accesswidener.AccessWidener; @@ -369,20 +375,35 @@ public List> getEntrypointContainers(String key, Clas @Override public void invokeEntrypoints(String key, Class type, Consumer invoker) { + RuntimeException exception = null; + Collection> entrypoints = FabricLoaderImpl.INSTANCE.getEntrypointContainers(key, type); + List sessions = new ArrayList<>(); + int size = entrypoints.size(); + for (EntrypointInfoReceiver entrypointInfoReceiver : FabricLoaderImpl.INSTANCE.getEntrypointInfoReceivers()) { + sessions.add(entrypointInfoReceiver.createEntrypointInvocationSession(key, size)); + } + if (!hasEntrypoints(key)) { Log.debug(LogCategory.ENTRYPOINT, "No subscribers for entrypoint '%s'", key); return; } - RuntimeException exception = null; - Collection> entrypoints = FabricLoaderImpl.INSTANCE.getEntrypointContainers(key, type); - Log.debug(LogCategory.ENTRYPOINT, "Iterating over entrypoint '%s'", key); + int index = 0; for (EntrypointContainer container : entrypoints) { + index++; + for (EntrypointInvocationSession session : sessions) { + session.preInvoke(container.getProvider(), index, size); + } try { invoker.accept(container.getEntrypoint()); } catch (Throwable t) { + for (EntrypointInvocationSession session : sessions) { + //noinspection ReassignedVariable,AssignmentToCatchBlockParameter + t = session.error(container.getProvider(), t, index, size); + } + if (t == null) continue; exception = ExceptionUtil.gatherExceptions(t, exception, exc -> new RuntimeException(String.format("Could not execute entrypoint stage '%s' due to errors, provided by '%s'!", @@ -391,6 +412,10 @@ public void invokeEntrypoints(String key, Class type, Consumer } } + for (EntrypointInvocationSession session : sessions) { + session.close(); + } + if (exception != null) { throw exception; } @@ -595,6 +620,16 @@ public String[] getLaunchArguments(boolean sanitize) { return getGameProvider().getLaunchArguments(sanitize); } + @Override + public List getEntrypointInfoReceivers() { + return getEntrypoints("entrypointReceiver", EntrypointInfoReceiver.class); + } + + @Override + public ModMessageSender getModMessageSession() { + return new ModMessageSenderImpl(); + } + @Override protected Path getModsDirectory0() { String directory = System.getProperty(SystemProperties.MODS_FOLDER); diff --git a/src/main/java/net/fabricmc/loader/impl/info/MessageImpl.java b/src/main/java/net/fabricmc/loader/impl/info/MessageImpl.java new file mode 100644 index 000000000..e2fc35714 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/info/MessageImpl.java @@ -0,0 +1,36 @@ +package net.fabricmc.loader.impl.info; + +import net.fabricmc.loader.api.info.Message; + +import java.io.Serializable; + +class MessageImpl implements Message, Serializable { + private boolean closed; + private String title; + + MessageImpl(String title) { + this.title = title; + } + + @Override + public boolean pinned() { + return !closed; + } + + @Override + public void title(String message) { + if (closed) throw new IllegalStateException("Already closed!"); + title = message; + } + + @Override + public String title() { + return title; + } + + @Override + public void close() { + if (closed) throw new IllegalStateException("Already closed!"); + closed = true; + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/info/ModMessageSenderImpl.java b/src/main/java/net/fabricmc/loader/impl/info/ModMessageSenderImpl.java new file mode 100644 index 000000000..9d0906060 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/info/ModMessageSenderImpl.java @@ -0,0 +1,24 @@ +package net.fabricmc.loader.impl.info; + +import net.fabricmc.loader.api.info.Message; +import net.fabricmc.loader.api.info.ModMessageReceiver; +import net.fabricmc.loader.api.info.ModMessageSender; +import net.fabricmc.loader.api.info.ProgressBar; +import net.fabricmc.loader.impl.FabricLoaderImpl; + +public class ModMessageSenderImpl implements ModMessageSender { + + @Override + public Message message(String title) { + MessageImpl message = new MessageImpl(title); + FabricLoaderImpl.INSTANCE.getEntrypoints("modMessageReceiver", ModMessageReceiver.class).forEach(entrypoint -> entrypoint.message(message)); + return message; + } + + @Override + public ProgressBar progressBar(String name, int steps) { + ProgressBarImpl progressBar = new ProgressBarImpl(name, steps); + FabricLoaderImpl.INSTANCE.getEntrypoints("modMessageReceiver", ModMessageReceiver.class).forEach(entrypoint -> entrypoint.progressBar(progressBar)); + return progressBar; + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/info/ProgressBarImpl.java b/src/main/java/net/fabricmc/loader/impl/info/ProgressBarImpl.java new file mode 100644 index 000000000..85dc2bbb7 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/info/ProgressBarImpl.java @@ -0,0 +1,91 @@ +package net.fabricmc.loader.impl.info; + +import net.fabricmc.loader.api.info.ModMessageReceiver; +import net.fabricmc.loader.api.info.ProgressBar; + +import net.fabricmc.loader.impl.FabricLoaderImpl; + +import org.jetbrains.annotations.Nullable; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class ProgressBarImpl implements ProgressBar, Serializable { + private String title; + private int progress; + private int steps; + private boolean completed; + private ProgressBarImpl parent; + // TODO: Are there any concurrency issues? + private List children = new ArrayList<>(); + ProgressBarImpl(String title, int steps) { + this.title = title; + this.steps = steps; + } + @Override + public void increment() { + if (this.completed) throw new IllegalStateException("Already closed!"); + progress++; + } + + @Override + public float percentage() { + return (float) progress / steps; + } + + @Override + public int steps() { + return steps; + } + + @Override + public int progress() { + return progress; + } + + @Override + public void set(int steps) { + if (this.completed) throw new IllegalStateException("Already closed!"); + this.progress = steps; + } + + @Override + public String title() { + return title; + } + + @Override + public void title(String title) { + if (this.completed) throw new IllegalStateException("Already closed!"); + this.title = title; + } + + @Override + public void close() { + if (this.completed) throw new IllegalStateException("Already closed!"); + this.completed = true; + this.children.forEach(progressBar -> { + if (!progressBar.isCompleted()) progressBar.close(); + }); + } + + @Override + public boolean isCompleted() { + return completed; + } + + @Override + public ProgressBar progressBar(String name, int steps) { + ProgressBarImpl progressBar = new ProgressBarImpl(name, steps); + progressBar.parent = this; + this.children.add(progressBar); + FabricLoaderImpl.INSTANCE.getEntrypoints("modMessageReceiver", ModMessageReceiver.class).forEach(entrypoint -> entrypoint.progressBar(progressBar)); + return progressBar; + } + + @Override + public @Nullable ProgressBar getParent() { + return parent; + } +}