diff --git a/LICENSE.txt b/LICENSE.txt index b0cbe2b391..249debe26b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -57,6 +57,9 @@ This software contains a partial repackaging of javaxdelta, a BSD licensed progr binary differences and applying them, sourced from the subversion at http://sourceforge.net/projects/javaxdelta/ authored by genman, heikok, pivot. The only changes are to replace some Trove collection types with standard Java collections, and repackaged. + +This software includes the Monocraft font from https://github.com/IdreesInc/Monocraft/ for use in the early loading +display. ========================================================================= diff --git a/build.gradle b/build.gradle index 356f1ab070..e57b9f099b 100644 --- a/build.gradle +++ b/build.gradle @@ -568,7 +568,7 @@ project(':fmlonly') { ext { MCP_ARTIFACT = project(':mcp').mcp.config.get() - PACKED_DEPS = [':fmlcore:jar', ':fmlloader:jar', ':javafmllanguage:jar', ':lowcodelanguage:jar', ':mclanguage:jar'] + PACKED_DEPS = [':fmlcore:jar', ':fmlloader:jar', ':fmlearlydisplay:jar', ':javafmllanguage:jar', ':lowcodelanguage:jar', ':mclanguage:jar'] MAVEN_PATH = "${group.replace('.', '/')}/${project.name}/${VERSION}" } @@ -596,6 +596,7 @@ project(':fmlonly') { dependencies { implementation project(':fmlcore') implementation project(':fmlloader') + implementation project(':fmlearlydisplay') implementation project(':javafmllanguage') implementation project(':lowcodelanguage') implementation project(':mclanguage') @@ -632,7 +633,7 @@ project(':fmlonly') { sharedFmlonlyForge.call(project) task launcherJson(type: LauncherJson) { - packedDependencies = [':fmlloader:jar'] + packedDependencies = [':fmlloader:jar',':fmlearlydisplay:jar'] doFirst { def fmlonly_client = project(':fmlonly').patcher.runs.fmlonly_client json = [ @@ -931,7 +932,7 @@ project(':forge') { // Essentially, the same as the old, except dropping the first number, and the builds are no longer unique. MCP_ARTIFACT = project(':mcp').mcp.config.get() VERSION_JSON = project(':mcp').file('build/mcp/downloadJson/version.json') - PACKED_DEPS = [':fmlcore:jar', ':fmlloader:jar', ':javafmllanguage:jar', ':lowcodelanguage:jar', ':mclanguage:jar'] + PACKED_DEPS = [':fmlcore:jar', ':fmlloader:jar', ':fmlearlydisplay:jar', ':javafmllanguage:jar', ':lowcodelanguage:jar', ':mclanguage:jar'] MAVEN_PATH = "${group.replace('.', '/')}/${project.name}/${VERSION}" } @@ -951,6 +952,7 @@ project(':forge') { testImplementation 'org.hamcrest:hamcrest-all:1.3' // needs advanced matching for list order implementation project(':fmlcore') implementation project(':fmlloader') + implementation project(':fmlearlydisplay') implementation project(':javafmllanguage') implementation project(':lowcodelanguage') implementation project(':mclanguage') @@ -1328,7 +1330,7 @@ project(':forge') { } task launcherJson(type: LauncherJson) { - packedDependencies = [':fmlloader:jar'] + packedDependencies = [':fmlloader:jar',':fmlearlydisplay:jar'] doFirst { def forge_client = project(':forge').patcher.runs.forge_client json = [ diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java b/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java index 864c512026..1b259ec743 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java @@ -7,12 +7,14 @@ package net.minecraftforge.fml; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.loading.progress.ProgressMeter; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.ToIntFunction; /** * A mod loading state. During mod loading, the mod loader transitions between states in a defined sorted list of states, @@ -44,25 +46,33 @@ public interface IModLoadingState { */ Function message(); + /** + * @return a function that computes the size of this transition based on the size of the modlist. + * Used to compute progress. + */ + ToIntFunction size(); + /** * {@return an optional runnable, which runs before starting the transition from this state to the next} - * @see #buildTransition(Executor, Executor, Function, Function) + * @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function) */ Optional> inlineRunnable(); /** * Builds the transition task for this state with a blank pre-sync and post-sync task. * - * @param syncExecutor a synchronous executor + * @param a type of event fired on the mod-specific event bus + * @param syncExecutor a synchronous executor * @param parallelExecutor a parallel executor - * @param a type of event fired on the mod-specific event bus + * @param progressBar a progress meter for tracking progress * @return a transition task for this state - * @see #buildTransition(Executor, Executor, Function, Function) + * @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function) */ default Optional> buildTransition(final Executor syncExecutor, - final Executor parallelExecutor) { - return buildTransition(syncExecutor, parallelExecutor, + final Executor parallelExecutor, + final ProgressMeter progressBar) { + return buildTransition(syncExecutor, parallelExecutor, progressBar, e -> CompletableFuture.runAsync(() -> {}, e), e -> CompletableFuture.runAsync(() -> {}, e)); } @@ -71,16 +81,18 @@ public interface IModLoadingState { * Builds the transition task for this state. The pre-sync and post-sync task functions allow the transition builder * to run these tasks on the same executor as the actual event dispatch and pre/post hooks. * - * @param syncExecutor a synchronous executor + * @param a type of event fired on the mod-specific event bus + * @param syncExecutor a synchronous executor * @param parallelExecutor a parallel executor - * @param preSyncTask a function which returns a task to run before event pre-dispatch hook - * @param postSyncTask a function which returns a task to run after event post-dispatch hook - * @param a type of event fired on the mod-specific event bus + * @param progressBar a progress meter for tracking progress + * @param preSyncTask a function which returns a task to run before event pre-dispatch hook + * @param postSyncTask a function which returns a task to run after event post-dispatch hook * @return a transition task for this state */ Optional> buildTransition(final Executor syncExecutor, final Executor parallelExecutor, + final ProgressMeter progressBar, final Function> preSyncTask, final Function> postSyncTask); } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java b/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java index 941a701924..f64dc6950c 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java @@ -7,6 +7,7 @@ package net.minecraftforge.fml; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.loading.progress.ProgressMeter; import java.util.ArrayList; import java.util.List; @@ -24,20 +25,26 @@ public interface IModStateTransition { } default - CompletableFuture build(final Executor syncExecutor, + CompletableFuture build(final String name, + final Executor syncExecutor, final Executor parallelExecutor, + final ProgressMeter progressBar, final Function> preSyncTask, final Function> postSyncTask) { List> futures = new ArrayList<>(); this.eventFunctionStream().get() .map(f -> (EventGenerator) f) - .reduce((head, tail) -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, head, ModLoadingStage::currentState, tail)) - .ifPresent(last -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, last, nextModLoadingStage(), null)); + .reduce((head, tail) -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, progressBar, head, ModLoadingStage::currentState, tail)) + .ifPresent(last -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, progressBar, last, nextModLoadingStage(), null)); final CompletableFuture preSyncTaskCF = preSyncTask.apply(syncExecutor); final CompletableFuture eventDispatchCF = ModList.gather(futures).thenCompose(ModList::completableFutureFromExceptionList); final CompletableFuture postEventDispatchCF = preSyncTaskCF + .thenApplyAsync(n -> { + progressBar.label(progressBar.name() + ": dispatching "+name); + return null; + }, parallelExecutor) .thenComposeAsync(n -> eventDispatchCF, parallelExecutor) .thenApply(r -> { postSyncTask.apply(syncExecutor); @@ -54,6 +61,7 @@ public interface IModStateTransition { EventGenerator addCompletableFutureTaskForModDispatch(final Executor syncExecutor, final Executor parallelExecutor, final List> completableFutures, + final ProgressMeter progressBar, final EventGenerator eventGenerator, final BiFunction nextState, final EventGenerator nextGenerator) { @@ -62,7 +70,7 @@ public interface IModStateTransition { var preDispatchHook = (BiFunction, CompletableFuture>) preDispatchHook(); completableFutures.add(preDispatchHook.apply(selectedExecutor, eventGenerator)); - completableFutures.add(ModList.get().futureVisitor(eventGenerator, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor))); + completableFutures.add(ModList.get().futureVisitor(eventGenerator, progressBar, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor))); var postDispatchHook = (BiFunction, CompletableFuture>) postDispatchHook(); completableFutures.add(postDispatchHook.apply(selectedExecutor, eventGenerator)); diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java index 6eb85d8e9a..cbe678c6f8 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java @@ -9,6 +9,7 @@ import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.config.IConfigEvent; import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.loading.progress.ProgressMeter; import net.minecraftforge.forgespi.language.IModInfo; import java.util.EnumMap; @@ -113,6 +114,7 @@ public abstract class ModContainer public static CompletableFuture buildTransitionHandler( final ModContainer target, final IModStateTransition.EventGenerator eventGenerator, + final ProgressMeter progressBar, final BiFunction stateChangeHandler, final Executor executor) { return CompletableFuture @@ -123,6 +125,7 @@ public abstract class ModContainer }, executor) .whenComplete((mc, exception) -> { target.modLoadingStage = stateChangeHandler.apply(target.modLoadingStage, exception); + progressBar.increment(); ModLoadingContext.get().setActiveContainer(null); }); } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java index c06a64a623..1512297fde 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java @@ -7,6 +7,7 @@ package net.minecraftforge.fml; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.loading.progress.ProgressMeter; import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.ModFileScanData; @@ -114,10 +115,11 @@ public class ModList Function> futureVisitor( final IModStateTransition.EventGenerator eventGenerator, + final ProgressMeter progressBar, final BiFunction stateChange) { return executor -> gather( this.mods.stream() - .map(mod -> ModContainer.buildTransitionHandler(mod, eventGenerator, stateChange, executor)) + .map(mod -> ModContainer.buildTransitionHandler(mod, eventGenerator, progressBar, stateChange, executor)) .collect(Collectors.toList())) .thenComposeAsync(ModList::completableFutureFromExceptionList, executor); } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java index 1660d593e5..1debe3d308 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java @@ -10,11 +10,13 @@ import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.fml.loading.FMLLoader; +import net.minecraftforge.fml.loading.ImmediateWindowHandler; import net.minecraftforge.fml.loading.LoadingModList; import net.minecraftforge.fml.loading.moddiscovery.InvalidModIdentifier; import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.fml.loading.moddiscovery.ModInfo; -import net.minecraftforge.fml.loading.progress.StartupMessageManager; +import net.minecraftforge.fml.loading.progress.ProgressMeter; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.IModLanguageProvider; import net.minecraftforge.forgespi.locating.ForgeFeature; @@ -81,7 +83,7 @@ public class ModLoader private final ModStateManager stateManager; private boolean loadingStateValid; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private final Optional> statusConsumer = StartupMessageManager.modLoaderConsumer(); + private final Optional> statusConsumer = StartupNotificationManager.modLoaderConsumer(); private final Set completedStates = new HashSet<>(); private ModList modList; @@ -133,20 +135,19 @@ public class ModLoader * @param periodicTask Optional periodic task to perform on the main thread while other activities run */ public void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) { - ForgeFeature.registerFeature("java_version", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.SERVER, System.getProperty("java.version"))); + ForgeFeature.registerFeature("java_version", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.BOTH, System.getProperty("java.version"))); + ForgeFeature.registerFeature("openGLVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.CLIENT, ImmediateWindowHandler.getGLVersion())); loadingStateValid = true; - statusConsumer.ifPresent(c->c.accept("Waiting for scan to complete")); FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask); - statusConsumer.ifPresent(c->c.accept("Loading mods")); final ModList modList = ModList.of(loadingModList.getModFiles().stream().map(ModFileInfo::getFile).toList(), loadingModList.getMods()); if (!this.loadingExceptions.isEmpty()) { LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.get(0)); + statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); modList.setLoadedMods(Collections.emptyList()); loadingStateValid = false; throw new LoadingFailedException(loadingExceptions); } - statusConsumer.ifPresent(c->c.accept("Validating features")); List failedBounds = loadingModList.getMods().stream() .map(ModInfo::getForgeFeatures) .flatMap(Collection::stream) @@ -155,6 +156,7 @@ public class ModLoader if (!failedBounds.isEmpty()) { LOGGER.fatal(CORE, "Failed to validate feature bounds for mods"); + statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); modList.setLoadedMods(Collections.emptyList()); loadingStateValid = false; throw new LoadingFailedException(failedBounds.stream() @@ -162,7 +164,6 @@ public class ModLoader .toList()); } - statusConsumer.ifPresent(c->c.accept("Building Mod List")); final List modContainers = loadingModList.getModFiles().stream() .map(ModFileInfo::getFile) .map(this::buildMods) @@ -170,48 +171,42 @@ public class ModLoader .toList(); if (!loadingExceptions.isEmpty()) { LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.get(0)); + statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); modList.setLoadedMods(Collections.emptyList()); loadingStateValid = false; throw new LoadingFailedException(loadingExceptions); } modList.setLoadedMods(modContainers); this.modList = modList; - statusConsumer.ifPresent(c->c.accept("Dispatching gathering events")); - stateManager.getStates(ModLoadingPhase.GATHER).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask)); - statusConsumer.ifPresent(c->c.accept("Gathering phase complete")); + var stateList = stateManager.getStates(ModLoadingPhase.GATHER); + var progress = StartupMessageManager.addProgressBar("Mod Gather", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum()); + stateList.forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress)); + progress.complete(); } public void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) { - statusConsumer.ifPresent(c->c.accept("Performing load phase")); - stateManager.getStates(ModLoadingPhase.LOAD).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask)); - statusConsumer.ifPresent(c->c.accept("Mod setup complete")); + var stateList = stateManager.getStates(ModLoadingPhase.LOAD); + var progress = StartupMessageManager.addProgressBar("Mod Loading", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum()); + stateList.forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress)); + progress.complete(); } public void finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) { - statusConsumer.ifPresent(c->c.accept("Performing completion phase")); - stateManager.getStates(ModLoadingPhase.COMPLETE).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask)); + var stateList = stateManager.getStates(ModLoadingPhase.COMPLETE); + var progress = StartupMessageManager.addProgressBar("Mod Complete", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum()); + stateList.forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress)); statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size()))); + progress.complete(); } - private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker) { + private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, final ProgressMeter progressBar) { if (!isLoadingStateValid()) { LOGGER.error("Cowardly refusing to process mod state change request from {}", state); return; } - statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList))); + progressBar.label(progressBar.name()+ " working"); state.inlineRunnable().ifPresent(a->a.accept(this.modList)); - state.buildTransition(syncExecutor, parallelExecutor).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t)); - completedStates.add(state); - } - - private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, Function> preSyncTask, Function> postSyncTask) { - if (!isLoadingStateValid()) { - LOGGER.error("Cowardly refusing to process mod state change request from {}", state); - return; - } - statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList))); - state.inlineRunnable().ifPresent(a->a.accept(this.modList)); - state.buildTransition(syncExecutor, parallelExecutor, preSyncTask, postSyncTask).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t)); + state.buildTransition(syncExecutor, parallelExecutor, progressBar).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t)); completedStates.add(state); } @@ -229,6 +224,7 @@ public class ModLoader .collect(Collectors.toList()); if (!notModLoading.isEmpty()) { LOGGER.fatal("Encountered non-modloading exceptions!", e); + statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); throw e; } @@ -237,6 +233,7 @@ public class ModLoader .map(ModLoadingException.class::cast) .collect(Collectors.toList()); LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state.name(), modLoadingExceptions.size()); + statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); throw new LoadingFailedException(modLoadingExceptions); } } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java index b68f182b5d..d34421d39a 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java @@ -7,12 +7,14 @@ package net.minecraftforge.fml; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.loading.progress.ProgressMeter; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.ToIntFunction; /** * Implementation of the {@link IModLoadingState} interface. @@ -26,6 +28,7 @@ import java.util.function.Function; */ public record ModLoadingState(String name, String previous, Function message, + ToIntFunction size, ModLoadingPhase phase, Optional> inlineRunnable, Optional transition) implements IModLoadingState { @@ -33,9 +36,10 @@ public record ModLoadingState(String name, String previous, public Optional> buildTransition(final Executor syncExecutor, final Executor parallelExecutor, + final ProgressMeter progressBar, final Function> preSyncTask, final Function> postSyncTask) { - return transition.map(t -> t.build(syncExecutor, parallelExecutor, preSyncTask, postSyncTask)); + return transition.map(t -> t.build(name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask)); } /** @@ -47,7 +51,7 @@ public record ModLoadingState(String name, String previous, * @param phase the mod loading phase the state belongs to */ public static ModLoadingState empty(final String name, final String previous, final ModLoadingPhase phase) { - return new ModLoadingState(name, previous, ml -> "", phase, Optional.empty(), Optional.empty()); + return new ModLoadingState(name, previous, ml -> "", f->0, phase, Optional.empty(), Optional.empty()); } /** @@ -62,7 +66,7 @@ public record ModLoadingState(String name, String previous, */ public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase, final IModStateTransition transition) { - return new ModLoadingState(name, previous, ml -> "Processing transition " + name, phase, Optional.empty(), Optional.of(transition)); + return new ModLoadingState(name, previous, ml -> "Processing transition " + name, ModList::size, phase, Optional.empty(), Optional.of(transition)); } /** @@ -78,7 +82,7 @@ public record ModLoadingState(String name, String previous, public static ModLoadingState withTransition(final String name, final String previous, final Function message, final ModLoadingPhase phase, final IModStateTransition transition) { - return new ModLoadingState(name, previous, message, phase, Optional.empty(), Optional.of(transition)); + return new ModLoadingState(name, previous, message, ModList::size, phase, Optional.empty(), Optional.of(transition)); } /** @@ -93,6 +97,6 @@ public record ModLoadingState(String name, String previous, */ public static ModLoadingState withInline(final String name, final String previous, final ModLoadingPhase phase, final Consumer inline) { - return new ModLoadingState(name, previous, ml -> "Processing work " + name, phase, Optional.of(inline), Optional.empty()); + return new ModLoadingState(name, previous, ml -> "Processing work " + name, ml->0, phase, Optional.of(inline), Optional.empty()); } } \ No newline at end of file diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModWorkManager.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModWorkManager.java index 24eb6e9b4b..f34a749f19 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModWorkManager.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModWorkManager.java @@ -93,7 +93,7 @@ public class ModWorkManager { private static ForkJoinPool parallelThreadPool; public static Executor parallelExecutor() { if (parallelThreadPool == null) { - final int loadingThreadCount = FMLConfig.loadingThreadCount(); + final int loadingThreadCount = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.MAX_THREADS); LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount); parallelThreadPool = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false); } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/StartupMessageManager.java b/fmlcore/src/main/java/net/minecraftforge/fml/StartupMessageManager.java index a8a8de4deb..df69a26104 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/StartupMessageManager.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/StartupMessageManager.java @@ -5,19 +5,26 @@ package net.minecraftforge.fml; +import net.minecraftforge.fml.loading.progress.ProgressMeter; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; + import java.util.Optional; import java.util.function.Consumer; public class StartupMessageManager { public static void addModMessage(final String message) { - net.minecraftforge.fml.loading.progress.StartupMessageManager.addModMessage(message); + StartupNotificationManager.addModMessage(message); } public static Optional> modLoaderConsumer() { - return net.minecraftforge.fml.loading.progress.StartupMessageManager.modLoaderConsumer(); + return StartupNotificationManager.modLoaderConsumer(); } public static Optional> mcLoaderConsumer() { - return net.minecraftforge.fml.loading.progress.StartupMessageManager.mcLoaderConsumer(); + return StartupNotificationManager.mcLoaderConsumer(); + } + + public static ProgressMeter addProgressBar(final String barName, final int count) { + return StartupNotificationManager.addProgressBar(barName, count); } } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/VersionChecker.java b/fmlcore/src/main/java/net/minecraftforge/fml/VersionChecker.java index a0032688d1..0458d7ff52 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/VersionChecker.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/VersionChecker.java @@ -104,7 +104,7 @@ public class VersionChecker @Override public void run() { - if (!FMLConfig.runVersionCheck()) + if (!FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.VERSION_CHECK)) { LOGGER.info("Global Forge version check system disabled, no further processing."); return; diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/config/ConfigFileTypeHandler.java b/fmlcore/src/main/java/net/minecraftforge/fml/config/ConfigFileTypeHandler.java index 8633d78049..fb8b8a6051 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/config/ConfigFileTypeHandler.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/config/ConfigFileTypeHandler.java @@ -26,7 +26,7 @@ import static net.minecraftforge.fml.config.ConfigTracker.CONFIG; public class ConfigFileTypeHandler { private static final Logger LOGGER = LogUtils.getLogger(); static ConfigFileTypeHandler TOML = new ConfigFileTypeHandler(); - private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.defaultConfigPath()); + private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.getConfigValue(FMLConfig.ConfigValue.DEFAULT_CONFIG_PATH)); public Function reader(Path configBasePath) { return (c) -> { diff --git a/fmlearlydisplay/build.gradle b/fmlearlydisplay/build.gradle new file mode 100644 index 0000000000..2201dcffad --- /dev/null +++ b/fmlearlydisplay/build.gradle @@ -0,0 +1,93 @@ +plugins { + id 'com.github.ben-manes.versions' + id 'org.javamodularity.moduleplugin' version '1.8.7' apply false + id 'org.cadixdev.licenser' +} +apply plugin: 'java-library' +apply plugin: 'jacoco' +apply plugin: 'org.javamodularity.moduleplugin' + +import org.gradle.internal.os.OperatingSystem + +switch (OperatingSystem.current()) { + case OperatingSystem.LINUX: + project.ext.lwjglNatives = "natives-linux" + break + case OperatingSystem.MAC_OS: + project.ext.lwjglNatives = "natives-macos" + break + case OperatingSystem.WINDOWS: + project.ext.lwjglNatives = "natives-windows" + break +} + + +dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) } + +java.withSourcesJar() + +dependencies { + compileOnly('org.jetbrains:annotations:23.0.0') + implementation(project(':fmlloader')) + implementation(project(':fmlcore')) + implementation('org.lwjgl:lwjgl:3.3.1') + implementation('org.lwjgl:lwjgl-glfw:3.3.1') + implementation('org.lwjgl:lwjgl-opengl:3.3.1') + implementation('org.lwjgl:lwjgl-stb:3.3.1') + implementation('org.lwjgl:lwjgl-tinyfd:3.3.1') + implementation('org.slf4j:slf4j-api:1.8.0-beta4') + implementation("net.sf.jopt-simple:jopt-simple:${JOPT_SIMPLE_VERSION}") + testImplementation('org.junit.jupiter:junit-jupiter-api:5.8.2') + testImplementation('org.powermock:powermock-core:2.0.9') + testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.2') + testRuntimeOnly('org.slf4j:slf4j-jdk14:1.8.0-beta4') + testRuntimeOnly("org.lwjgl:lwjgl::$lwjglNatives") + testRuntimeOnly("org.lwjgl:lwjgl-glfw::$lwjglNatives") + testRuntimeOnly("org.lwjgl:lwjgl-opengl::$lwjglNatives") + testRuntimeOnly("org.lwjgl:lwjgl-stb::$lwjglNatives") +} + +test { + useJUnitPlatform() +} + +ext { + MANIFESTS = [ + '': [ + 'Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + 'Git-Commit': GIT_INFO.abbreviatedId, + 'Git-Branch': GIT_INFO.branch, + 'Build-Number': "${System.getenv('BUILD_NUMBER')?:0}", + ] as LinkedHashMap, + 'net/minecraftforge/fml/earlydisplay/': [ + 'Specification-Title': 'FMLEarlyDisplay', + 'Specification-Vendor': 'Forge Development LLC', + 'Specification-Version': '1', + 'Implementation-Title': 'FML Early Display', + 'Implementation-Version': '1.0', + 'Implementation-Vendor': 'Forge' + ] as LinkedHashMap + ] +} + +jar.doFirst { + MANIFESTS.each { pkg, values -> + if (pkg == '') + manifest.attributes(values) + else + manifest.attributes(values, pkg) + } +} + +tasks.withType(JavaCompile) { + options.compilerArgs << '-Xlint:unchecked' +} + +license { + header = rootProject.file('LICENSE-header.txt') + include 'net/minecraftforge/' +} + +publishing.publications.mavenJava(MavenPublication) { + from components.java +} \ No newline at end of file diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/ColourScheme.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/ColourScheme.java new file mode 100644 index 0000000000..560f34ff6c --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/ColourScheme.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +public enum ColourScheme { + RED(new Colour(239, 50, 61), new Colour(255, 255, 255)), + BLACK(new Colour(0, 0, 0), new Colour(255, 255, 255)); + + private final Colour background; + private final Colour foreground; + + ColourScheme(final Colour background, final Colour foreground) { + this.background = background; + this.foreground = foreground; + } + + public Colour background() { + return background; + } + + public Colour foreground() { + return foreground; + } + + public record Colour(int red, int green, int blue) { + public float redf() { + return ((float)red)/255f; + } + public float greenf() { + return ((float)green)/255f; + } + public float bluef() { + return ((float)blue)/255f; + } + + public int packedint(int a) { + return ((a & 0xff) << 24) | ((blue & 0xff) << 16) | ((green & 0xff) << 8) | (red & 0xff); + } + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/DisplayWindow.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/DisplayWindow.java new file mode 100644 index 0000000000..d7906734ec --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/DisplayWindow.java @@ -0,0 +1,561 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +import joptsimple.OptionParser; +import net.minecraftforge.fml.loading.FMLConfig; +import net.minecraftforge.fml.loading.FMLLoader; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.fml.loading.ImmediateWindowProvider; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.PointerBuffer; +import org.lwjgl.glfw.GLFWImage; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.stb.STBImage; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.util.tinyfd.TinyFileDialogs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Desktop; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL32C.*; + +/** + * The Loading Window that is opened Immediately after Forge starts. + * It is called from the ModDirTransformerDiscoverer, the soonest method that ModLauncher calls into Forge code. + * In this way, we can be sure that this will not run before any transformer or injection. + * + * The window itself is spun off into a secondary thread, and is handed off to the main game by Forge. + * + * Because it is created so early, this thread will "absorb" the context from OpenGL. + * Therefore, it is of utmost importance that the Context is made Current for the main thread before handoff, + * otherwise OS X will crash out. + * + * Based on the prior ClientVisualization, with some personal touches. + */ +public class DisplayWindow implements ImmediateWindowProvider { + private static final int[][] GL_VERSIONS = new int[][] {{4,6}, {4,5}, {4,4}, {4,3}, {4,2}, {4,1}, {4,0}, {3,3}, {3,2}}; + private static final Logger LOGGER = LoggerFactory.getLogger("EARLYDISPLAY"); + private final AtomicBoolean tickSemaphore = new AtomicBoolean(true); + + private ColourScheme colourScheme; + private ElementShader elementShader; + + private RenderElement.DisplayContext context; + private List elements; + private int framecount; + private EarlyFramebuffer framebuffer; + private ScheduledFuture windowTick; + + private PerformanceInfo performanceInfo; + private ScheduledFuture performanceTick; + // The GL ID of the window. Used for all operations + private long window; + // The thread that contains and ticks the window while Forge is loading mods + private ScheduledExecutorService renderScheduler; + private int fbWidth; + private int fbHeight; + private int fbScale; + private int winWidth; + private int winHeight; + private int winX; + private int winY; + + private final Semaphore renderLock = new Semaphore(1); + private boolean maximized; + private String glVersion; + private SimpleFont font; + + @Override + public String name() { + return "fmlearlywindow"; + } + @Override + public Runnable initialize(String[] arguments) { + final OptionParser parser = new OptionParser(); + var mcversionopt = parser.accepts("fml.mcVersion").withRequiredArg().ofType(String.class); + var forgeversionopt = parser.accepts("fml.forgeVersion").withRequiredArg().ofType(String.class); + var widthopt = parser.accepts("width") + .withRequiredArg().ofType(Integer.class) + .defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH)); + var heightopt = parser.accepts("height") + .withRequiredArg().ofType(Integer.class) + .defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT)); + var maximizedopt = parser.accepts("earlywindow.maximized"); + parser.allowsUnrecognizedOptions(); + var parsed = parser.parse(arguments); + winWidth = parsed.valueOf(widthopt); + winHeight = parsed.valueOf(heightopt); + FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH, winWidth); + FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT, winHeight); + fbScale = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_FBSCALE); + try { + var optionLines = Files.readAllLines(FMLPaths.GAMEDIR.get().resolve(Paths.get("options.txt"))); + var options = optionLines.stream().map(l->l.split(":")).filter(a->a.length == 2).collect(Collectors.toMap(a->a[0], a->a[1])); + var colourScheme = Boolean.parseBoolean(options.getOrDefault("darkMojangStudiosBackground", "false")); + this.colourScheme = colourScheme ? ColourScheme.BLACK : ColourScheme.RED; + } catch (IOException ioe) { + // No options + this.colourScheme = ColourScheme.RED; // default to red colourscheme + } + + this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED); + + var forgeVersion = parsed.valueOf(forgeversionopt); + StartupNotificationManager.modLoaderConsumer().ifPresent(c->c.accept("Forge loading "+ forgeVersion)); + performanceInfo = new PerformanceInfo(); + return start(parsed.valueOf(mcversionopt), forgeVersion); + } + + // The width and height of the framebuffer that we're rendering to. + + /** + * The main render loop. + * renderThread executes this. + * + * Performs initialization and then ticks the screen at 20 fps. + * When the thread is killed, context is destroyed. + */ + private void renderThreadFunc() { + if (!renderLock.tryAcquire()) return; + try { + tickSemaphore.set(true); + glfwMakeContextCurrent(window); + framebuffer.activate(); + glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight()); + this.context.elementShader().activate(); + this.context.elementShader().updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight()); + glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f); + paintFramebuffer(); + this.context.elementShader().clear(); + framebuffer.deactivate(); + glViewport(0, 0, fbWidth, fbHeight); + framebuffer.draw(this.fbWidth, this.fbHeight); + // Swap buffers; we're done + glfwSwapBuffers(window); + } finally { + glfwMakeContextCurrent(0); + renderLock.release(); + } + } + + /** + * Render initialization methods called by the Render Thread. + * It compiles the fragment and vertex shaders for rendering text with STB, and sets up basic render framework. + * + * Nothing fancy, we just want to draw and render text. + */ + private void initRender(final @Nullable String mcVersion, final String forgeVersion) { + // This thread owns the GL render context now. We should make a note of that. + glfwMakeContextCurrent(window); + // Wait for one frame to be complete before swapping; enable vsync in other words. + glfwSwapInterval(1); + createCapabilities(); + + elementShader = new ElementShader(); + try { + elementShader.init(); + } catch (Throwable t) { + LOGGER.error("Crash during shader initialization", t); + crashElegantly("An error occurred initializing shaders."); + } + + // Set the clear color based on the colour scheme + glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f); + + // we always render to an 854x480 texture and then fit that to the screen - with a scale factor + this.context = new RenderElement.DisplayContext(854, 480, fbScale, elementShader, colourScheme, performanceInfo); + framebuffer = new EarlyFramebuffer(this.context); + try { + this.font = new SimpleFont("Monocraft.ttf", fbScale, 200000, 1 + RenderElement.INDEX_TEXTURE_OFFSET); + } catch (Throwable t) { + LOGGER.error("Crash during font initialization", t); + crashElegantly("An error occurred initializing a font for rendering. "+t.getMessage()); + } + this.elements = new ArrayList<>(Arrays.asList( + RenderElement.squir(), + RenderElement.anvil(font), + RenderElement.logMessageOverlay(font), + RenderElement.forgeVersionOverlay(font, mcVersion+"-"+forgeVersion.split("-")[0]), + RenderElement.performanceBar(font), + RenderElement.progressBars(font) + )); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glfwMakeContextCurrent(0); + this.windowTick = renderScheduler.scheduleAtFixedRate(this::renderThreadFunc, 50, 50, TimeUnit.MILLISECONDS); + this.performanceTick = renderScheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS); + } + + /** + * Called every frame by the Render Thread to draw to the screen. + */ + void paintFramebuffer() { + // Clear the screen to our color + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + this.elements.removeIf(element -> !element.render(context, framecount)); + if (tickSemaphore.compareAndSet(true, false)) // we only increment the framecount on a periodic basis + framecount++; + } + + + public void render(int alpha) { + var currentVAO = glGetInteger(GL_VERTEX_ARRAY_BINDING); + var currentFB = glGetInteger(GL_READ_FRAMEBUFFER_BINDING); + glfwSwapInterval(0); + glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight()); + RenderElement.globalAlpha = alpha; + framebuffer.activate(); + glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), alpha / 255f); + elementShader.activate(); + elementShader.updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight()); + paintFramebuffer(); + elementShader.clear(); + framebuffer.deactivate(); + glBindVertexArray(currentVAO); + glBindFramebuffer(GL_FRAMEBUFFER, currentFB); + } + /** + * Start the window and Render Thread; we're ready to go. + */ + public Runnable start(@Nullable String mcVersion, final String forgeVersion) { + initWindow(mcVersion); + renderScheduler = Executors.newSingleThreadScheduledExecutor(); + renderScheduler.schedule(() -> initRender(mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS); + return this::periodicTick; + } + + private static final String ERROR_URL = "https://links.minecraftforge.net/early-display-errors"; + @Override + public String getGLVersion() { + return this.glVersion; + } + + private void crashElegantly(String errorDetails) { + String qrText; + try (var is = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/glfailure.txt")))) { + qrText = is.lines().collect(Collectors.joining("\n")); + } catch (IOException ioe) { + qrText = ""; + } + + StringBuilder msgBuilder = new StringBuilder(2000); + msgBuilder.append("Failed to initialize graphics window with current settings.\n"); + msgBuilder.append("\n\n"); + msgBuilder.append("Failure details:\n"); + msgBuilder.append(errorDetails); + msgBuilder.append("\n\n"); + msgBuilder.append("If you click yes, we will try and open " + ERROR_URL + " in your default browser"); + LOGGER.error("ERROR DISPLAY\n"+msgBuilder.toString()); + var res = TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge",msgBuilder.toString(), "yesno", "error", false); + if (res) { + try { + Desktop.getDesktop().browse(URI.create(ERROR_URL)); + } catch (IOException ioe) { + TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge", "Sadly, we couldn't open your browser.\nVisit " + ERROR_URL, "ok", "error", false); + } + } + System.exit(1); + } + /** + * Called to initialize the window when preparing for the Render Thread. + * + * The act of calling glfwInit here creates a concurrency issue; GL doesn't know whether we're gonna call any + * GL functions from the secondary thread and the main thread at the same time. + * + * It's then our job to make sure this doesn't happen, only calling GL functions where the Context is Current. + * As long as we can verify that, then GL (and things like OS X) have no complaints with doing this. + * + * @param mcVersion Minecraft Version + * @return The selected GL profile as an integer pair + */ + public void initWindow(@Nullable String mcVersion) { + // Initialize GLFW with a time guard, in case something goes wrong + long glfwInitBegin = System.nanoTime(); + if (!glfwInit()) { + crashElegantly("We are unable to initialize the graphics system.\nglfwInit failed.\n"); + throw new IllegalStateException("Unable to initialize GLFW"); + } + long glfwInitEnd = System.nanoTime(); + + if (glfwInitEnd - glfwInitBegin > 1e9) { + LOGGER.error("WARNING : glfwInit took {} seconds to start.", (glfwInitEnd - glfwInitBegin) / 1.0e9); + } + + // Clear the Last Exception (#7285 - Prevent Vanilla throwing an IllegalStateException due to invalid controller mappings) + handleLastGLFWError((error, description) -> LOGGER.error(String.format("Suppressing Last GLFW error: [0x%X]%s", error, description))); + + // Set window hints for the new window we're gonna create. + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); + if (mcVersion != null) { + // this emulates what we would get without early progress window + // as vanilla never sets these, so GLFW uses the first window title + // set them explicitly to avoid it using "FML early loading progress" as the class + String vanillaWindowTitle = "Minecraft* " + mcVersion; + glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle); + glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle); + } + + long primaryMonitor = glfwGetPrimaryMonitor(); + if (primaryMonitor == 0) { + LOGGER.error("Failed to find a primary monitor - this means LWJGL isn't working properly"); + crashElegantly("Failed to locate a primary monitor.\nglfwGetPrimaryMonitor failed.\n"); + throw new IllegalStateException("Can't find a primary monitor"); + } + GLFWVidMode vidmode = glfwGetVideoMode(primaryMonitor); + + if (vidmode == null) { + LOGGER.error("Failed to get the current display video mode."); + crashElegantly("Failed to get current display resolution.\nglfwGetVideoMode failed.\n"); + throw new IllegalStateException("Can't get a resolution"); + } + long window; + int versidx= 0; + final String[] lastGLError=new String[GL_VERSIONS.length]; + do { + LOGGER.info("Trying GL version "+GL_VERSIONS[versidx][0]+"."+GL_VERSIONS[versidx][1]); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, GL_VERSIONS[versidx][0]); // we try our versions one at a time + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, GL_VERSIONS[versidx][1]); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + window = glfwCreateWindow(winWidth, winHeight, "Minecraft: Forge Loading...", 0L, 0L); + var erridx = versidx; + handleLastGLFWError((error, description) -> lastGLError[erridx] = String.format("Trying %d.%d: GLFW error: [0x%X]%s", GL_VERSIONS[erridx][0], GL_VERSIONS[erridx][1], error, description)); + if (lastGLError[versidx] != null) { + LOGGER.trace(lastGLError[versidx]); + } + versidx++; + if (versidx== GL_VERSIONS.length) { + LOGGER.error("Failed to find any valid GLFW profile. "+lastGLError[0]); + crashElegantly("Failed to find a valid GLFW profile.\nWe tried "+ + Arrays.stream(GL_VERSIONS).map(p->p[0]+"."+p[1]).collect(Collectors.joining(", "))+ + " but none of them worked.\n"+String.join("\n", Arrays.asList(lastGLError))); + throw new IllegalStateException("Failed to create a GLFW window with any profile"); + } + } while (window == 0); + var requestedVersion = GL_VERSIONS[versidx-1][0]+"."+GL_VERSIONS[versidx-1][1]; + var maj = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR); + var min = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR); + var gotVersion = maj+"."+min; + LOGGER.info("Requested GL version "+requestedVersion+" got version "+gotVersion); + this.glVersion = gotVersion; + this.window = window; + + int[] x = new int[1]; + int[] y = new int[1]; + glfwGetMonitorPos(primaryMonitor, x, y); + int monitorX = x[0]; + int monitorY = y[0]; +// glfwSetWindowSizeLimits(window, 854, 480, GLFW_DONT_CARE, GLFW_DONT_CARE); + if (this.maximized) { + glfwMaximizeWindow(window); + } + + glfwGetWindowSize(window, x, y); + this.winWidth = x[0]; + this.winHeight = y[0]; + + glfwSetWindowPos(window, (vidmode.width() - this.winWidth) / 2 + monitorX, (vidmode.height() - this.winHeight) / 2 + monitorY); + + // Attempt setting the icon + int[] channels = new int[1]; + try (var glfwImgBuffer = GLFWImage.create(MemoryUtil.getAllocator().malloc(GLFWImage.SIZEOF), 1)) { + final ByteBuffer imgBuffer; + try (GLFWImage glfwImages = GLFWImage.malloc()) { + imgBuffer = STBHelper.loadImageFromClasspath("forge_logo.png", 20000, x, y, channels); + glfwImgBuffer.put(glfwImages.set(x[0], y[0], imgBuffer)); + glfwSetWindowIcon(window, glfwImgBuffer); + STBImage.stbi_image_free(imgBuffer); + } + } catch (NullPointerException e) { + System.err.println("Failed to load forge logo"); + } + handleLastGLFWError((error, description) -> LOGGER.debug(String.format("Suppressing GLFW icon error: [0x%X]%s", error, description))); + + glfwSetFramebufferSizeCallback(window, this::fbResize); + glfwSetWindowPosCallback(window, this::winMove); + glfwSetWindowSizeCallback(window, this::winResize); + + // Show the window + glfwShowWindow(window); + + glfwGetWindowPos(window, x, y); + this.winX = x[0]; + this.winY = y[0]; + glfwGetFramebufferSize(window, x, y); + this.fbWidth = x[0]; + this.fbHeight = y[0]; + glfwPollEvents(); + } + + private void winResize(long window, int width, int height) { + if (window == this.window && width != 0 && height != 0) { + this.winWidth = width; + this.winHeight = height; + } + } + private void fbResize(long window, int width, int height) { + if (window == this.window && width != 0 && height != 0) { + this.fbWidth = width; + this.fbHeight = height; + } + } + + private void winMove(long window, int x, int y) { + if (window == this.window) { + this.winX = x; + this.winY = y; + } + } + private void handleLastGLFWError(BiConsumer handler) { + try (MemoryStack memorystack = MemoryStack.stackPush()) { + PointerBuffer pointerbuffer = memorystack.mallocPointer(1); + int error = org.lwjgl.glfw.GLFW.glfwGetError(pointerbuffer); + if (error != GLFW_NO_ERROR) { + long pDescription = pointerbuffer.get(); + String description = pDescription == 0L ? "" : MemoryUtil.memUTF8(pDescription); + handler.accept(error, description); + } + } + } + + /** + * Hand-off the window to the vanilla game. + * Called on the main thread instead of the game's initialization. + * + * @return the Window we own. + */ + public long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier title, final LongSupplier monitorSupplier) { + // we have to spin wait for the window ticker + while (!this.windowTick.isDone()) { + this.windowTick.cancel(false); + } + var tries = 0; + var renderlockticket = false; + do { + try { + renderlockticket = renderLock.tryAcquire(100, TimeUnit.MILLISECONDS); + if (++tries > 9) { + Thread.dumpStack(); + crashElegantly("We seem to be having trouble handing off the window, tried for 1 second"); + } + } catch (InterruptedException e) { + Thread.interrupted(); + } + } while (!renderlockticket); + + // schedule a 50 ms ticker to try and smooth out the rendering + renderScheduler.scheduleAtFixedRate(()->tickSemaphore.set(true), 50, 50, TimeUnit.MILLISECONDS); + + glfwMakeContextCurrent(window); + // Set the title to what the game wants + glfwSetWindowTitle(window, title.get()); + glfwSwapInterval(0); + // Clean up our hooks + glfwSetFramebufferSizeCallback(window, null).free(); + glfwSetWindowPosCallback(window, null).free(); + glfwSetWindowSizeCallback(window, null).free(); + return window; + } + + @Override + public boolean positionWindow(final Optional monitor, final IntConsumer widthSetter, final IntConsumer heightSetter, final IntConsumer xSetter, final IntConsumer ySetter) { + widthSetter.accept(this.winWidth); + heightSetter.accept(this.winHeight); + xSetter.accept(this.winX); + ySetter.accept(this.winY); + return true; + } + + @Override + public void updateFramebufferSize(final IntConsumer width, final IntConsumer height) { + width.accept(this.fbWidth); + height.accept(this.fbHeight); + } + + private Method loadingOverlay; + + @SuppressWarnings("unchecked") + @Override + public Supplier loadingOverlay(final Supplier mc, final Supplier ri, final Consumer> ex, final boolean fade) { + try { + return (Supplier)loadingOverlay.invoke(null, mc, ri, ex, this); + } catch (Throwable e) { + throw new IllegalStateException("How did you get here?", e); + } + } + + @Override + public void updateModuleReads(final ModuleLayer layer) { + var fm = layer.findModule("forge").orElseThrow(); + getClass().getModule().addReads(fm); + var clz = FMLLoader.getGameLayer().findModule("forge").map(l->Class.forName(l, "net.minecraftforge.client.loading.ForgeLoadingOverlay")).orElseThrow(); + var methods = Arrays.stream(clz.getMethods()).filter(m-> Modifier.isStatic(m.getModifiers())).collect(Collectors.toMap(Method::getName, Function.identity())); + loadingOverlay = methods.get("newInstance"); + } + + public int getFramebufferTextureId() { + return framebuffer.getTexture(); + } + + public RenderElement.DisplayContext context() { + return this.context; + } + + @Override + public void periodicTick() { + glfwPollEvents(); + } + + public void addMojangTexture(final int textureId) { + this.elements.add(0, RenderElement.mojang(textureId, framecount)); +// this.elements.get(0).retire(framecount + 1); + } + + public void close() { + // Close the Render Scheduler thread + renderScheduler.shutdown(); + this.framebuffer.close(); + this.context.elementShader().close(); + SimpleBufferBuilder.destroy(); + } +} \ No newline at end of file diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/EarlyFramebuffer.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/EarlyFramebuffer.java new file mode 100644 index 0000000000..c6af00941a --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/EarlyFramebuffer.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +import java.nio.IntBuffer; + +import static net.minecraftforge.fml.earlydisplay.RenderElement.clamp; +import static org.lwjgl.opengl.GL32C.*; + +public class EarlyFramebuffer { + private final int framebuffer; + private final int texture; + + private final RenderElement.DisplayContext context; + + EarlyFramebuffer(final RenderElement.DisplayContext context) { + this.context = context; + this.framebuffer = glGenFramebuffers(); + this.texture = glGenTextures(); + glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, this.texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, context.width() * context.scale(), context.height() * context.scale(), 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer)null); + glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.texture, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + void activate() { + glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer); + } + + void deactivate() { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + void draw(int windowFBWidth, int windowFBHeight) { + var wscale = ((float)windowFBWidth / this.context.width()); + var hscale = ((float)windowFBHeight / this.context.height()); + var scale = this.context.scale() * Math.min(wscale, hscale) / 2f; + var wleft = (int)(windowFBWidth * 0.5f - scale * this.context.width()); + var wtop = (int)(windowFBHeight * 0.5f - scale * this.context.height()); + var wright = (int)(windowFBWidth * 0.5f + scale * this.context.width()); + var wbottom = (int)(windowFBHeight * 0.5f + scale * this.context.height()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, this.framebuffer); + final var colour = this.context.colourScheme().background(); + glClearColor(colour.redf(), colour.greenf(), colour.bluef(), 1f); + glClear(GL_COLOR_BUFFER_BIT); + // src Y are flipped, since our FB is flipped + glBlitFramebuffer(0, this.context.height() * this.context.scale(), this.context.width() * this.context.scale(), 0, clamp(wleft, 0, windowFBWidth), clamp(wtop, 0, windowFBHeight), clamp(wright, 0, windowFBWidth), clamp(wbottom, 0, windowFBHeight), GL_COLOR_BUFFER_BIT, GL_NEAREST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + int getTexture() { + return this.texture; + } + + public void close() { + glDeleteTextures(this.texture); + glDeleteFramebuffers(this.framebuffer); + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/ElementShader.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/ElementShader.java new file mode 100644 index 0000000000..5a39aba150 --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/ElementShader.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + + +import static org.lwjgl.opengl.GL32C.*; + +public class ElementShader { + private int program; + private int textureUniform; + private int screenSizeUniform; + private int renderTypeUniform; + + public void init() { + int vertexShader = glCreateShader(GL_VERTEX_SHADER); + int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + + // Bind the source of our shaders to the ones created above + glShaderSource(fragmentShader, """ + #version 150 core + uniform sampler2D tex; + uniform int rendertype; + in vec2 fTex; + in vec4 fColour; + out vec4 fragColor; + + void main() { + if (rendertype == 0) + fragColor = vec4(1,1,1,texture(tex, fTex).r) * fColour; + if (rendertype == 1) + fragColor = texture(tex, fTex) * fColour; + if (rendertype == 2) + fragColor = fColour; + } + """); + glShaderSource(vertexShader, """ + #version 150 core + in vec2 position; + in vec2 tex; + in vec4 colour; + uniform vec2 screenSize; + out vec2 fTex; + out vec4 fColour; + void main() { + fTex = tex; + fColour = colour; + gl_Position = vec4((position/screenSize) * 2 - 1, 0.0, 1.0); + } + """); + + // Compile the vertex and fragment elementShader so that we can use them + glCompileShader(vertexShader); + if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) { + throw new IllegalStateException("VertexShader linkage failure. \n" + glGetShaderInfoLog(vertexShader)); + } + glCompileShader(fragmentShader); + if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) { + throw new IllegalStateException("FragmentShader linkage failure. \n" + glGetShaderInfoLog(fragmentShader)); + } + + var program = glCreateProgram(); + glBindAttribLocation(program, 0, "position"); + glBindAttribLocation(program, 1, "tex"); + glBindAttribLocation(program, 2, "colour"); + glAttachShader(program, vertexShader); + glAttachShader(program, fragmentShader); + glLinkProgram(program); + if (glGetProgrami(program, GL_LINK_STATUS) == GL_FALSE) { + throw new RuntimeException("ShaderProgram linkage failure. \n" + glGetProgramInfoLog(program)); + } + this.program = program; + + glDetachShader(program, vertexShader); + glDetachShader(program, fragmentShader); + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + textureUniform = glGetUniformLocation(program, "tex"); + screenSizeUniform = glGetUniformLocation(program, "screenSize"); + renderTypeUniform = glGetUniformLocation(program, "rendertype"); + + activate(); + } + + public void activate() { + glUseProgram(program); + } + public void updateTextureUniform(int textureNumber) { + glUniform1i(textureUniform, textureNumber); + } + + public void updateScreenSizeUniform(int width, int height) { + glUniform2f(screenSizeUniform, width, height); + } + + public void updateRenderTypeUniform(RenderType type) { + glUniform1i(renderTypeUniform, type.ordinal()); + } + + public void clear() { + glUseProgram(0); + } + + public void close() { + glDeleteProgram(program); + } + + public enum RenderType { + FONT, TEXTURE, BAR; + } + + public int program() { + return program; + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/PerformanceInfo.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/PerformanceInfo.java new file mode 100644 index 0000000000..a1251fa9c8 --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/PerformanceInfo.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +import com.sun.management.OperatingSystemMXBean; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; + +public class PerformanceInfo { + + private final OperatingSystemMXBean osBean; + private final MemoryMXBean memoryBean; + float memory; + private String text; + + PerformanceInfo() { + osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); + memoryBean = ManagementFactory.getMemoryMXBean(); + } + + void update() { + final MemoryUsage heapusage = memoryBean.getHeapMemoryUsage(); + memory = (float) heapusage.getUsed() / heapusage.getMax(); + var cpuLoad = osBean.getProcessCpuLoad(); + String cpuText; + if (cpuLoad == -1) { + cpuText = String.format("*CPU: %.1f%%", osBean.getCpuLoad() * 100f); + } else { + cpuText = String.format("CPU: %.1f%%", cpuLoad * 100f); + } + + text = String.format("Heap: %d/%d MB (%.1f%%) OffHeap: %d MB %s", heapusage.getUsed() >> 20, heapusage.getMax() >> 20, memory * 100.0, memoryBean.getNonHeapMemoryUsage().getUsed() >> 20, cpuText); + } + + String text() { + return text; + } + + float memory() { + return memory; + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/QuadHelper.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/QuadHelper.java new file mode 100644 index 0000000000..8a7c858156 --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/QuadHelper.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +public class QuadHelper { + public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1, int colour) { + bb.pos(x0, y0).tex(u0, v0).colour(colour).endVertex(); + bb.pos(x1, y0).tex(u1, v0).colour(colour).endVertex(); + bb.pos(x0, y1).tex(u0, v1).colour(colour).endVertex(); + bb.pos(x1, y1).tex(u1, v1).colour(colour).endVertex(); + } + + public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1) { + bb.pos(x0, y0).tex(u0, v0).endVertex(); + bb.pos(x1, y0).tex(u1, v0).endVertex(); + bb.pos(x0, y1).tex(u0, v1).endVertex(); + bb.pos(x1, y1).tex(u1, v1).endVertex(); + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/RenderElement.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/RenderElement.java new file mode 100644 index 0000000000..d3a4b1fe14 --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/RenderElement.java @@ -0,0 +1,343 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +import net.minecraftforge.fml.loading.progress.Message; +import net.minecraftforge.fml.loading.progress.ProgressMeter; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import static org.lwjgl.opengl.GL32C.*; + +public class RenderElement { + static final int INDEX_TEXTURE_OFFSET = 5; + private final SimpleBufferBuilder bb; + private final Renderer renderer; + static int globalAlpha = 255; + private int retireCount; + + interface Renderer { + + void accept(SimpleBufferBuilder bb, DisplayContext context, int frame); + + default Renderer then(Renderer r) { + if (r == null) return this; + return (bb, ctx, frame) -> { + r.accept(bb, ctx, frame); + this.accept(bb, ctx, frame); + }; + } + } + interface TextureRenderer { + void accept(SimpleBufferBuilder bb, DisplayContext context, int[] size, int frame); + } + interface Initializer extends Supplier {} + + interface TextGenerator { + void accept(SimpleBufferBuilder bb, SimpleFont fh, DisplayContext ctx); + } + + public record DisplayContext(int width, int height, int scale, ElementShader elementShader, ColourScheme colourScheme, PerformanceInfo performance) { + public int scaledWidth() { + return scale() * width(); + } + + public int scaledHeight() { + return scale() * height(); + } + } + + public RenderElement(final Initializer rendererInitializer) { + this.bb = new SimpleBufferBuilder(1); + this.renderer = rendererInitializer.get(); + } + + public boolean render(DisplayContext ctx, int count) { + this.renderer.accept(bb, ctx, count); + return this.retireCount == 0 || this.retireCount < count; + } + + public void retire(final int frame) { + this.retireCount = frame; + } + + private static void startupLogMessages(SimpleBufferBuilder bb, SimpleFont font, DisplayContext context) { + List messages = StartupNotificationManager.getMessages(); + List texts = new ArrayList<>(); + for (int i = messages.size() - 1; i >= 0; i--) { + final StartupNotificationManager.AgeMessage pair = messages.get(i); + final float fade = clamp((4000.0f - (float) pair.age() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f); + if (fade <0.01f) continue; + Message msg = pair.message(); + int colour = Math.min((int)(fade * 255f), globalAlpha) << 24 | 0xFFFFFF; + texts.add(new SimpleFont.DisplayText(msg.getText()+"\n", colour)); + } + + font.generateVerticesForTexts(10, context.scaledHeight() - texts.size() * font.lineSpacing() + font.descent() - 10, bb, texts.toArray(SimpleFont.DisplayText[]::new)); + } + public static RenderElement monag() { + return new RenderElement(RenderElement.initializeTexture("monagstudios.png", 45000, 4, (bb, ctx, sz, frame) -> { + var size = 256; + var x0 = (ctx.width() - 2 * size) / 2; + var y0 = 64; + QuadHelper.loadQuad(bb, x0, x0+size, y0, y0+size/2f, 0f, 1f, 0f, 0.5f, 0xFFFFFFFF); + QuadHelper.loadQuad(bb, x0+size, x0+2*size, y0, y0+size/2f, 0f, 1f, 0.5f, 1f, 0xFFFFFFFF); + })); + + } + + public static RenderElement mojang(final int textureId, final int frameStart) { + return new RenderElement(()->(bb, ctx, frame) -> { + var size = 256 * ctx.scale(); + var x0 = (ctx.scaledWidth() - 2 * size) / 2; + var y0 = 64 * ctx.scale() + 32; + ctx.elementShader().updateTextureUniform(0); + ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE); + var fade = Math.min((frame - frameStart) * 10, 255); + glBindTexture(GL_TEXTURE_2D, textureId); + bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + QuadHelper.loadQuad(bb, x0, x0+size, y0, y0+size/2f, 0f, 1f, 0f, 0.5f, (fade << 24) | 0xFFFFFF); + QuadHelper.loadQuad(bb, x0+size, x0+2*size, y0, y0+size/2f, 0f, 1f, 0.5f, 1f, (fade << 24) | 0xFFFFFF); + bb.draw(); + glBindTexture(GL_TEXTURE_2D, 0); + }); + } + public static RenderElement logMessageOverlay(SimpleFont font) { + return new RenderElement(RenderElement.initializeText(font, RenderElement::startupLogMessages)); + } + + public static RenderElement forgeVersionOverlay(SimpleFont font, String version) { + return new RenderElement(RenderElement.initializeText(font, (bb, fnt, ctx)-> + font.generateVerticesForTexts(ctx.scaledWidth() - font.stringWidth(version) - 10, + ctx.scaledHeight() - font.lineSpacing() + font.descent() - 10, bb, + new SimpleFont.DisplayText(version, ctx.colourScheme.foreground().packedint(RenderElement.globalAlpha))))); + } + public static RenderElement squir() { + return new RenderElement(RenderElement.initializeTexture("squirrel.png", 45000, 3, (bb, context, size, frame) -> { + var inset = 5f; + var x0 = inset; + var x1 = inset + size[0] * context.scale(); + var y0 = inset; + var y1 = inset + size[1] * context.scale(); + int fade = (int) (Math.cos(frame * Math.PI / 16) * 16) + 16; +// int fade = 0xff; + var colour = (Math.min(fade, globalAlpha) & 0xff) << 24 | 0xffffff; + QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, 0f, 1f, colour); + })); + } + + public static RenderElement anvil(SimpleFont font) { + return new RenderElement(RenderElement.initializeTexture("forge_anvil.png", 20000, 2, (bb, context, size, frame) -> { + var x0 = context.scaledWidth() - size[0] * context.scale(); + var x1 = context.scaledWidth(); + var y0 = context.scaledHeight() - size[0] * context.scale() - font.descent() - font.lineSpacing(); + var y1 = context.scaledHeight() - font.descent() - font.lineSpacing(); + int frameidx = frame % 32; + float framepos = (frameidx * (float)size[0]) / size[1]; + float framesize = size[0] / (float)size[1]; + QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, framepos, framepos+framesize, globalAlpha << 24 | 0xFFFFFF); + })); + } + public static RenderElement progressBars(SimpleFont font) { + return new RenderElement(() -> (bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame)); + } + + public static RenderElement performanceBar(SimpleFont font) { + return new RenderElement(() -> (bb, ctx, frame) -> RenderElement.memoryInfo(font, bb, ctx, frame)); + } + + public static void startupProgressBars(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) { + Renderer acc = null; + var barCount = 2; + List currentProgress = StartupNotificationManager.getCurrentProgress(); + var size = currentProgress.size(); + var alpha = 0xFF; + for (int i = 0; i < barCount && i < size; i++) { + final ProgressMeter pm = currentProgress.get(i); + Renderer barRenderer = barRenderer(i, alpha, font, pm, context); + acc = barRenderer.then(acc); + alpha >>= 1; + } + if (acc != null) + acc.accept(buffer, context, frameNumber); + } + private static final int BAR_HEIGHT = 20; + private static Renderer barRenderer(int cnt, int alpha, SimpleFont font, ProgressMeter pm, DisplayContext context) { + var barSpacing = font.lineSpacing() - font.descent() + BAR_HEIGHT; + var y = 250 * context.scale() + cnt * barSpacing; + var colour = (alpha << 24) | 0xFFFFFF; + Renderer bar; + if (pm.steps() == 0) { + bar = progressBar(ctx->new int[] {50, y + font.lineSpacing() - font.descent(), context.scaledWidth() - 100}, f->colour, RenderElement::indeterminateBar); + } else { + bar = progressBar(ctx -> new int[]{50, y + font.lineSpacing() - font.descent(), ctx.scaledWidth() - 100}, f -> colour, f -> new float[]{0f, pm.progress()}); + } + Renderer label = (bb, ctx, frame) -> renderText(font, text(50, y, pm.label().getText(), colour), bb, ctx); + return bar.then(label); + } + private static float[] indeterminateBar(int frame) { + if (RenderElement.globalAlpha != 0xFF) { + return new float[] {0f,1f}; + } else { + var progress = frame % 100; + return new float[]{clamp((progress - 2) / 100f, 0f, 1f), clamp((progress + 2) / 100f, 0f, 1f)}; + } + } + + private static void memoryInfo(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) { + var y = 10 * context.scale(); + PerformanceInfo pi = context.performance(); + final int colour = hsvToRGB((1.0f - (float)Math.pow(pi.memory(), 1.5f)) / 3f, 1.0f, 0.5f); + var bar = progressBar(ctx -> new int[]{50, y, ctx.scaledWidth() - 100}, f -> colour, f -> new float[]{0f, pi.memory()}); + var width = font.stringWidth(pi.text()); + Renderer label = (bb, ctx, frame) -> renderText(font, text(ctx.scaledWidth() / 2 - width / 2, y + 18, pi.text(), context.colourScheme.foreground().packedint(globalAlpha)), bb, ctx); + bar.then(label).accept(buffer, context, frameNumber); + } + + interface ColourFunction { + int colour(int frame); + } + + interface ProgressDisplay { + float[] progress(int frame); + } + + interface BarPosition { + int[] location(DisplayContext context); + } + public static Renderer progressBar(BarPosition position, ColourFunction colourFunction, ProgressDisplay progressDisplay) { + return (bb, context, frame) -> { + var colour = colourFunction.colour(frame); + var alpha = (colour & 0xFF000000) >> 24; + context.elementShader().updateTextureUniform(0); + context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.BAR); + var progress = progressDisplay.progress(frame); + bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + var inset = 2; + var pos = position.location(context); + var x0 = pos[0]; + var x1 = pos[0] + pos[2] + 4 * inset; + var y0 = pos[1]; + var y1 = y0 + BAR_HEIGHT; + QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().foreground().packedint(alpha)); + + x0 += inset; + x1 -= inset; + y0 += inset; + y1 -= inset; + QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().background().packedint(RenderElement.globalAlpha)); + + x1 = x0 + inset + (int)(progress[1] * pos[2]); + x0 += inset + progress[0] * pos[2]; + y0 += inset; + y1 -= inset; + QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, colour); + bb.draw(); + }; + } + + private static Initializer initializeText(SimpleFont font, TextGenerator textGenerator) { + return () -> (bb, context, frame) -> renderText(font, textGenerator, bb, context); + } + + private static void renderText(final SimpleFont font, final TextGenerator textGenerator, final SimpleBufferBuilder bb, final DisplayContext context) { + context.elementShader().updateTextureUniform(font.textureNumber()); + context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.FONT); + bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + textGenerator.accept(bb, font, context); + bb.draw(); + } + + private static TextGenerator text(int x, int y, String text, int colour) { + return (bb, font, context) -> font.generateVerticesForTexts(x, y, bb, new SimpleFont.DisplayText(text, colour)); + } + private static Initializer initializeTexture(final String textureFileName, int size, int textureNumber, TextureRenderer positionAndColour) { + return ()->{ + int[] imgSize = STBHelper.loadTextureFromClasspath(textureFileName, size, GL_TEXTURE0 + textureNumber + INDEX_TEXTURE_OFFSET); + return (bb, ctx, frame) -> { + ctx.elementShader().updateTextureUniform(textureNumber + INDEX_TEXTURE_OFFSET); + ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE); + renderTexture(bb, ctx, frame, imgSize, positionAndColour); + }; + }; + } + + private static void renderTexture(SimpleBufferBuilder bb, DisplayContext context, int frame, int[] size, TextureRenderer positionAndColour) { + bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + positionAndColour.accept(bb, context, size, frame); + bb.draw(); + } + + + public static float clamp(float num, float min, float max) { + if (num < min) { + return min; + } else { + return num > max ? max : num; + } + } + + public static int clamp(int num, int min, int max) { + if (num < min) { + return min; + } else { + return num > max ? max : num; + } + } + + public static int hsvToRGB(float hue, float saturation, float value) { + int i = (int)(hue * 6.0F) % 6; + float f = hue * 6.0F - (float)i; + float f1 = value * (1.0F - saturation); + float f2 = value * (1.0F - f * saturation); + float f3 = value * (1.0F - (1.0F - f) * saturation); + float f4; + float f5; + float f6; + switch(i) { + case 0: + f4 = value; + f5 = f3; + f6 = f1; + break; + case 1: + f4 = f2; + f5 = value; + f6 = f1; + break; + case 2: + f4 = f1; + f5 = value; + f6 = f3; + break; + case 3: + f4 = f1; + f5 = f2; + f6 = value; + break; + case 4: + f4 = f3; + f5 = f1; + f6 = value; + break; + case 5: + f4 = value; + f5 = f1; + f6 = f2; + break; + default: + throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + hue + ", " + saturation + ", " + value); + } + + int j = clamp((int)(f4 * 255.0F), 0, 255); + int k = clamp((int)(f5 * 255.0F), 0, 255); + int l = clamp((int)(f6 * 255.0F), 0, 255); + return 0xFF << 24 | j << 16 | k << 8 | l; + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/STBHelper.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/STBHelper.java new file mode 100644 index 0000000000..512e0bb381 --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/STBHelper.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +import org.lwjgl.BufferUtils; +import org.lwjgl.stb.STBImage; +import org.lwjgl.system.MemoryUtil; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.util.Objects; + +import static org.lwjgl.opengl.GL32C.*; + +public class STBHelper { + public static ByteBuffer readFromClasspath(final String name, int initialCapacity) { + ByteBuffer buf; + try (var channel = Channels.newChannel( + Objects.requireNonNull(STBHelper.class.getClassLoader().getResourceAsStream(name), "The resource "+name+" cannot be found"))) { + buf = BufferUtils.createByteBuffer(initialCapacity); + while (true) { + var readbytes = channel.read(buf); + if (readbytes == -1) break; + if (buf.remaining() == 0) { // extend the buffer by 50% + var newBuf = BufferUtils.createByteBuffer(buf.capacity() * 3 / 2); + buf.flip(); + newBuf.put(buf); + buf = newBuf; + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + buf.flip(); + return MemoryUtil.memSlice(buf); // we trim the final buffer to the size of the content + } + + public static int[] loadTextureFromClasspath(String file, int size, int textureNumber) { + int[] lw = new int[1]; + int[] lh = new int[1]; + int[] lc = new int[1]; + var img = loadImageFromClasspath(file, size, lw, lh, lc); + var texid = glGenTextures(); + glActiveTexture(textureNumber); + glBindTexture(GL_TEXTURE_2D, texid); +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lw[0], lh[0], 0, GL_RGBA, GL_UNSIGNED_BYTE, img); + glActiveTexture(GL_TEXTURE0); + MemoryUtil.memFree(img); + return new int[] {lw[0], lh[0]}; + } + + public static ByteBuffer loadImageFromClasspath(String file, int size, int[] width, int[] height, int[] channels) { + ByteBuffer buf = STBHelper.readFromClasspath(file, size); + return STBImage.stbi_load_from_memory(buf, width, height, channels, 4); + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/SimpleBufferBuilder.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/SimpleBufferBuilder.java new file mode 100644 index 0000000000..136f291745 --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/SimpleBufferBuilder.java @@ -0,0 +1,517 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +import org.lwjgl.system.MemoryUtil; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static org.lwjgl.opengl.GL32C.*; + +/** + * A very simple, Mojang inspired BufferBuilder. + * This has been customized for 2d rendering such as text and simple planar textures + *

+ * Not bound to any specific format, ideally should be held onto for re-use. + *

+ * Can be used for 'immediate mode' style rendering using {@link #draw()}, or + * upload to external vertex arrays for proper instancing using {@link #finishAndUpload()}. + *

+ * This is a Triangles only buffer, all data uploaded is in Triangles. + * Quads are converted to triangles using {@code 0, 1, 2, 0, 2, 3}. + *

+ * Any given {@link Format} should have its individual {@link Element} components + * buffered in the order specified by the {@link Format}, + * followed by an {@link #endVertex()} call to prepare for the next vertex. + *

+ * It is illegal to buffer primitives in any format other than the one specified to + * {@link #begin(Format, Mode)}. + * + * @author covers1624 + */ +public class SimpleBufferBuilder implements Closeable { + + private static final MemoryUtil.MemoryAllocator ALLOCATOR = MemoryUtil.getAllocator(false); + + private static final int[] VERTEX_ARRAYS = new int[Format.values().length]; + private static final int[] VERTEX_BUFFERS = new int[Format.values().length]; + private static final int[] VERTEX_BUFFER_LENGTHS = new int[Format.values().length]; + private static int elementBuffer = 0; + private static int elementBufferVertexLength = 0; + + static { + Arrays.fill(VERTEX_ARRAYS, 0); + Arrays.fill(VERTEX_BUFFERS, 0); + Arrays.fill(VERTEX_BUFFER_LENGTHS, 0); + } + + private long bufferAddr; // Pointer to the backing buffer. + private ByteBuffer buffer; // ByteBuffer view of the backing buffer. + + private Format format; // The current format we are buffering. + private Mode mode; // The current mode we are buffering. + private boolean building; // If we are building the buffer. + private int elementIndex; // The current element index we are buffering. if elementIndex == format.types.length, we expect 'endVertex' + private int index; // The current index into the buffer we are writing to. + private int vertices; // The number of complete vertices we have buffered. + + /** + * Create a new SimpleBufferBuilder with an initial capacity. + *

+ * The buffer will be doubled as required. + *

+ * Generally picking a small number, around 128/256 should be a + * safe bet. Provided you cache your buffers, it should not mean much overall. + * + * @param capacity The initial capacity in bytes. + */ + public SimpleBufferBuilder(int capacity) { + bufferAddr = ALLOCATOR.malloc(capacity); + buffer = MemoryUtil.memByteBuffer(bufferAddr, capacity); + } + + public static void destroy() { + glDeleteBuffers(VERTEX_BUFFERS); + glDeleteBuffers(elementBuffer); + glDeleteVertexArrays(VERTEX_ARRAYS); + } + + private static void ensureElementBufferLength(int vertices) { + if (elementBufferVertexLength >= vertices) { + return; + } + + // treating it as immutable storage, even though it's not + final var newElementBuffer = glGenBuffers(); + var newElementBufferVertexLength = Math.max(1024, elementBufferVertexLength); + while (newElementBufferVertexLength < vertices) { + newElementBufferVertexLength *= 2; + } + + final var oldIndexCount = elementBufferVertexLength + elementBufferVertexLength / 2; + final var newIndexCount = newElementBufferVertexLength + newElementBufferVertexLength / 2; + + // allocate new buffer + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, newElementBuffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, newIndexCount * 4L, GL_STATIC_DRAW); + + // mapping avoids creating additional CPU copies of the data + // unsynchronized is fine because this is a brand-new buffer, and the old contents will be copied in afterward + // also can invalidate the whole buffer too, similarly because brand new, don't care what was there before + final var mappingOffset = oldIndexCount * 4; + final var mappingSize = (newIndexCount - oldIndexCount) * 4; + final var mappedBuffer = glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, mappingOffset, mappingSize, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_BUFFER_BIT); + + if(mappedBuffer == null){ + throw new NullPointerException("OpenGL buffer mapping failed"); + } + + final int quads = newElementBufferVertexLength / 4; + final int oldQuads = elementBufferVertexLength / 4; + // generate indices for the extension to the buffer + for (int i = oldQuads; i < quads; i++) { + // Quads are a bit different, we need to emit 2 triangles such that + // when combined they make up a single quad. + mappedBuffer.putInt(i * 4 + 0).putInt(i * 4 + 1).putInt(i * 4 + 2); + mappedBuffer.putInt(i * 4 + 1).putInt(i * 4 + 3).putInt(i * 4 + 2); + } + + glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER); + + if (elementBuffer != 0) { + // copy old data from previous element buffer + glBindBuffer(GL_COPY_READ_BUFFER, elementBuffer); + glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, mappingOffset); + glBindBuffer(GL_COPY_READ_BUFFER, 0); + } + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + glDeleteBuffers(elementBuffer); + elementBuffer = newElementBuffer; + elementBufferVertexLength = newElementBufferVertexLength; + } + + /** + * Start building a new set of vertex data in the + * given format and mode. + * + * @param format The format to start building in. + * @param mode The mode to start building in. + */ + public SimpleBufferBuilder begin(Format format, Mode mode) { + if (bufferAddr == MemoryUtil.NULL) { + throw new IllegalStateException("Buffer has been freed."); // You already free'd the buffer + } + if (building) { + throw new IllegalStateException("Already building."); // Your already building verticies. + } + this.format = format; + this.mode = mode; + building = true; + elementIndex = 0; + ensureSpace(format.stride); + // Rewind ready for new data. + buffer.rewind(); + buffer.limit(buffer.capacity()); + return this; + } + + /** + * Buffer a position element. + * + * @param x The x. + * @param y The y. + * @param z The z. + * @return The same builder. + */ + public SimpleBufferBuilder pos(float x, float y) { + if (!building) throw new IllegalStateException("Not building."); // You did not call begin. + + if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. + if (format.types[elementIndex] != Element.POS) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order. + + // Assumes that our POS element specifies the FLOAT data type. + buffer.putFloat(index + 0, x); + buffer.putFloat(index + 4, y); + + // Increment index for the number of bytes we wrote and increment the element index. + index += format.types[elementIndex].width; + elementIndex++; + return this; + } + + /** + * Buffer a texture element. + * + * @param u The u. + * @param v The v. + * @return The same builder. + */ + public SimpleBufferBuilder tex(float u, float v) { + if (!building) throw new IllegalStateException("Not building."); // You did not call begin. + + if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. + if (format.types[elementIndex] != Element.TEX) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order. + + // Assumes our TEX element specifies the FLOAT data type. + buffer.putFloat(index + 0, u); + buffer.putFloat(index + 4, v); + + // Increment index for the number of bytes we wrote and increment the element index. + index += format.types[elementIndex].width; + elementIndex++; + return this; + } + + /** + * Buffer a color element. + * + * @param r The red component. (0-1) + * @param g The green component. (0-1) + * @param b The blue component. (0-1) + * @param a The alpha component. (0-1) + * @return The same buffer. + */ + public SimpleBufferBuilder colour(float r, float g, float b, float a) { + // Expand floats to 0-255 and forward. + return colour((byte) (r * 255F), (byte) (g * 255F), (byte) (b * 255F), (byte) (a * 255F)); + } + + /** + * @see ColourScheme.Colour#packedint(int) + * @param packedColor an ABGR packed int + * @return the same buffer. + */ + public SimpleBufferBuilder colour(int packedColor) { + if (!building) throw new IllegalStateException("Not building."); // You did not call begin. + + if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. + if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order. + + // Assumes our COLOR element specifies the UNSIGNED_BYTE data type. + buffer.putInt(index + 0, packedColor); + + // Increment index for the number of bytes we wrote and increment the element index. + index += format.types[elementIndex].width; + elementIndex++; + return this; + } + /** + * Buffer a color element. + * + * @param r The red component. (0-255) + * @param g The green component. (0-255) + * @param b The blue component. (0-255) + * @param a The alpha component. (0-255) + * @return The same buffer. + */ + public SimpleBufferBuilder colour(byte r, byte g, byte b, byte a) { + if (!building) throw new IllegalStateException("Not building."); // You did not call begin. + + if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call. + if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order. + + // Assumes our COLOR element specifies the UNSIGNED_BYTE data type. + buffer.put(index + 0, r); + buffer.put(index + 1, g); + buffer.put(index + 2, b); + buffer.put(index + 3, a); + + // Increment index for the number of bytes we wrote and increment the element index. + index += format.types[elementIndex].width; + elementIndex++; + return this; + } + + /** + * End building the current vertex and prepare for the next. + * + * @return The same builder. + */ + public SimpleBufferBuilder endVertex() { + if (!building) throw new IllegalStateException("Not building."); // You did not call begin. + + if (elementIndex != format.types.length) throw new IllegalStateException("Expected " + format.types[elementIndex]); // You did not finish building the vertex. + + // Reset elementIndex + elementIndex = 0; + // Increment the number of vertices we have so far buffered. + vertices++; + // Make sure there is space for the next vertex. + ensureSpace(format.stride); + return this; + } + + // Checks there is enough space in the buffer for specified number of bytes. + // If there is not enough space, the buffer is increased by 50%. + private void ensureSpace(int newBytes) { + int cap = buffer.capacity(); + if (index + newBytes > cap) { + int newCap = Math.max(3 * cap / 2, 3 * newBytes / 2); + bufferAddr = ALLOCATOR.realloc(bufferAddr, newCap); + buffer = MemoryUtil.memByteBuffer(bufferAddr, newCap); + buffer.rewind(); + } + } + + /** + * Upload the current buffer. + *

+ * This will bind a {@link org.lwjgl.opengl.GL32C#GL_ARRAY_BUFFER} and {@link org.lwjgl.opengl.GL32C#GL_ELEMENT_ARRAY_BUFFER} + *

+ * The vertex data and index data is uploaded to their respective buffers. + *

+ * Uploading the buffers finishes drawing and resets for the next buffer operation. + *

+ * This should not be called in conjunction with {@link #draw()} + * + * @return The number of indexes that were uploaded. + */ + public int finishAndUpload() { + if (!building) throw new IllegalStateException("Not building."); + + int indices; + try { + if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // You didn't finish building your vertex. + if (elementIndex != 0) throw new IllegalStateException("Not finished building vertex, Expected: " + format.types[elementIndex]); // You didn't finish building your vertex data. + if (vertices == 0) return 0; // No vertices buffered, lets not do anything. + if (vertices % mode.vertices != 0) throw new IllegalStateException("Does not contain vertices aligned to " + mode); // You did not put in enough vertices to cleanly slice the data into TRIANGLES/QUADS + + // Reset position to 0, limit the buffer to our index. + buffer.position(0); + buffer.limit(index); + + // Upload the raw vertex data in dynamic mode. + final int vbo = VERTEX_BUFFERS[format.ordinal()]; + final int vboSize = VERTEX_BUFFER_LENGTHS[format.ordinal()]; + glBindBuffer(GL_ARRAY_BUFFER, vbo); + if (vboSize < index) { + // expand buffer, it's not big enough + var newVBOSize = Math.max(1024, vboSize); + while (newVBOSize < index){ + newVBOSize *= 2; + } + // because everything is overwritten anyway, we can do an in-place reallocation + glBufferData(GL_ARRAY_BUFFER, newVBOSize, GL_DYNAMIC_DRAW); + VERTEX_BUFFER_LENGTHS[format.ordinal()] = newVBOSize; + } + glBufferSubData(GL_ARRAY_BUFFER, 0, buffer); + + // The number of indices for triangles is equal to our vertex count, as that is + // what we operate in. However, for Quads, we have exactly vertices + vertices / 2 + // vertices once we convert the quads to triangles. + indices = mode == Mode.TRIANGLES ? vertices : vertices + vertices / 2; + + if (mode == Mode.QUADS) { + ensureElementBufferLength(vertices); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer); + } + + return indices; + } finally { + // Reset builder state for next begin call. + building = false; + vertices = 0; + index = 0; + } + } + + /** + * Upload and draw this buffer using one of a number of re-usable set of buffers. + *

+ * This will immediately upload the buffer, resetting this builder for the next + * buffer operation, and draw the uploaded data. + *

+ * You will need to bind shaders, textures, etc, before calling this function. + */ + public void draw() { + if (!building) throw new IllegalStateException("Not building."); + + int vao = VERTEX_ARRAYS[format.ordinal()]; + int vbo = VERTEX_BUFFERS[format.ordinal()]; + + if (vao == 0) { + // These 3 buffers are paired, you can't allocate one without the others. + assert vbo == 0; + + // Make new vertex array and buffers! + vao = glGenVertexArrays(); + vbo = glGenBuffers(); + + // Cache the vertex array and buffers for future re-use. + VERTEX_ARRAYS[format.ordinal()] = vao; + VERTEX_BUFFERS[format.ordinal()] = vbo; + + // Ask our Format to set up its data layout for the vertex array. + // but only once, the VAO saves this state + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + format.bind(); + format.enable(); + } + // Bind the vertex array and buffers! + glBindVertexArray(vao); + + // Upload the data. + int indices = finishAndUpload(); + + if (mode == Mode.QUADS) { + glDrawElements(GL_TRIANGLES, indices, GL_UNSIGNED_INT, 0); + } else { + glDrawArrays(GL_TRIANGLES, 0, indices); + } + + // Unbind the vertex array. + glBindVertexArray(0); + } + + /** + * Clear this builder's cached buffer. + *

+ * If you are completely done, call {@link #destroy()} + */ + @Override + public void close() { + ALLOCATOR.free(bufferAddr); + bufferAddr = MemoryUtil.NULL; + } + + /** + * Represents a primitive mode that this builder is capable of buffering in. + */ + public enum Mode { + TRIANGLES(3), + QUADS(4), + ; + + public final int vertices; + + Mode(int vertices) { + this.vertices = vertices; + } + } + + /** + * Specifies a vertex element with a specific data type, number of primitives and a size in bytes. + */ + public enum Element { + POS(GL_FLOAT, 2, 2 * 4), + TEX(GL_FLOAT, 2, 2 * 4), + COLOR(GL_UNSIGNED_BYTE, 4, 4); + + public final int glType; + public final int count; + public final int width; + + Element(int glType, int count, int width) { + this.glType = glType; + this.count = count; + this.width = width; + } + } + + /** + * Specifies a combination of vertex elements. + */ + public enum Format { + POS(Element.POS), + POS_TEX(Element.POS, Element.TEX), + POS_COLOR(Element.POS, Element.COLOR), + POS_TEX_COLOR(Element.POS, Element.TEX, Element.COLOR); + + private final Element[] types; + public final int stride; + + Format(Element... types) { + this.types = types; + + // Stride is the width of each vertex in bytes. + stride = Arrays.stream(types).mapToInt(e -> e.width).sum(); + } + + /** + * set up the attribute pointers for this format. + *

+ * Assumes that an array buffer is already bound and ready to go. + */ + public void bind() { + int offset = 0; + + // Set up the pointers that tell GL where our interleaved + // vertex data is in the buffers. + for (int i = 0; i < types.length; i++) { + Element type = types[i]; + switch (type.glType) { + case GL_FLOAT -> glVertexAttribPointer(i, type.count, GL_FLOAT, false, stride, offset); + case GL_UNSIGNED_BYTE -> glVertexAttribPointer(i, type.count, GL_UNSIGNED_BYTE, true, stride, offset); + default -> throw new IllegalStateException("Unknown glType, I don't know how to bind this vertex element: " + type); + } + // add to the offset for the next element. + offset += type.width; + } + } + + /** + * Enables the vertex attributes this format contains. + */ + public void enable() { + for (int i = 0; i < types.length; i++) { + glEnableVertexAttribArray(i); + } + } + + /** + * Disables the vertex attributes this format contains. + */ + public void disable() { + for (int i = 0; i < types.length; i++) { + glDisableVertexAttribArray(i); + } + } + } +} diff --git a/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/SimpleFont.java b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/SimpleFont.java new file mode 100644 index 0000000000..c79e9362e4 --- /dev/null +++ b/fmlearlydisplay/src/main/java/net/minecraftforge/fml/earlydisplay/SimpleFont.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.earlydisplay; + +import org.lwjgl.BufferUtils; +import org.lwjgl.stb.STBTTAlignedQuad; +import org.lwjgl.stb.STBTTFontinfo; +import org.lwjgl.stb.STBTTPackContext; +import org.lwjgl.stb.STBTTPackRange; +import org.lwjgl.stb.STBTTPackedchar; +import org.lwjgl.system.MemoryStack; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.lwjgl.opengl.GL32C.*; +import static org.lwjgl.stb.STBTruetype.*; +import static org.lwjgl.system.MemoryUtil.NULL; + +public class SimpleFont { + private final int textureNumber; + private final int lineSpacing; + private final int descent; + private final int GLYPH_COUNT = 127-32; + private Glyph[] glyphs; + + private record Glyph(char c, int charwidth, int[] pos, float[] uv) { + Pos loadQuad(Pos pos, int colour, SimpleBufferBuilder bb) { + final var x0 = pos.x() + pos()[0]; + final var y0 = pos.y() + pos()[1]; + final var x1 = pos.x() + pos()[2]; + final var y1 = pos.y() + pos()[3]; + bb.pos(x0, y0).tex(uv()[0], uv()[1]).colour(colour).endVertex(); + bb.pos(x1, y0).tex(uv()[2], uv()[1]).colour(colour).endVertex(); + bb.pos(x0, y1).tex(uv()[0], uv()[3]).colour(colour).endVertex(); + bb.pos(x1, y1).tex(uv()[2], uv()[3]).colour(colour).endVertex(); + return new Pos(pos.x()+charwidth(), pos.y(), pos.minx()); + } + } + + /** + * Build the font and store it in the textureNumber location + */ + public SimpleFont(String fontName, int scale, int bufferSize, int textureNumber) { + ByteBuffer buf = STBHelper.readFromClasspath(fontName, bufferSize); + var info = STBTTFontinfo.create(); + if (!stbtt_InitFont(info, buf)) { + throw new IllegalStateException("Bad font"); + } + + var ascent = new float[1]; + var descent = new float[1]; + var lineGap = new float[1]; + int fontSize = 24; + stbtt_GetScaledFontVMetrics(buf, 0, fontSize, ascent, descent, lineGap); + this.lineSpacing = (int)(ascent[0] - descent[0] + lineGap[0]); + this.descent = (int)Math.floor(descent[0]); + int fontTextureId = glGenTextures(); + glActiveTexture(GL_TEXTURE0+textureNumber); + this.textureNumber = textureNumber; + glBindTexture(GL_TEXTURE_2D, fontTextureId); + try (var packedchars = STBTTPackedchar.malloc(GLYPH_COUNT)) { + int texwidth = 256; + int texheight = 128; + try (STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1)) { + var bitmap = BufferUtils.createByteBuffer(texwidth * texheight); + try (STBTTPackRange packRange = STBTTPackRange.malloc()) { + packRanges.put(packRange.set(fontSize, 32, null, GLYPH_COUNT, packedchars, (byte) 1, (byte) 1)); + packRanges.flip(); + } + + try (STBTTPackContext pc = STBTTPackContext.malloc()) { + stbtt_PackBegin(pc, bitmap, texwidth, texheight, 0, 1, NULL); + stbtt_PackSetOversampling(pc, 1, 1); + stbtt_PackSetSkipMissingCodepoints(pc, true); + stbtt_PackFontRanges(pc, buf, 0, packRanges); + stbtt_PackEnd(pc); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, texwidth, texheight, 0, GL_RED, GL_UNSIGNED_BYTE, bitmap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + } + } + glActiveTexture(GL_TEXTURE0); + try (var q = STBTTAlignedQuad.malloc()) { + float[] x = new float[1]; + float[] y = new float[1]; + glyphs = new Glyph[GLYPH_COUNT]; + + for (int i = 0; i < GLYPH_COUNT; i++) { + x[0] = 0f; + y[0] = fontSize; + stbtt_GetPackedQuad(packedchars, texwidth, texheight, i, x, y, q, true); + glyphs[i] = new Glyph((char) (i + 32), (int) (x[0] - 0f), new int[]{(int) q.x0(), (int) q.y0(), (int) q.x1(), (int) q.y1()}, new float[]{q.s0(), q.t0(), q.s1(), q.t1()}); + } + } + } + } + + int lineSpacing() { + return lineSpacing; + } + + int textureNumber() { + return textureNumber; + } + + int descent() { + return descent; + } + + public int stringWidth(String text) { + var bytes = text.getBytes(StandardCharsets.US_ASCII); + int len = 0; + for (int i = 0; i < bytes.length; i++) { + final byte c = bytes[i]; + len += switch (c) { + case '\n', '\t' -> 0; + case ' ' -> glyphs[0].charwidth(); + default -> { + if (c - 32 < this.GLYPH_COUNT && c > 32) { + yield this.glyphs[c - 32].charwidth(); + } else { + yield 0; + } + } + }; + } + return len; + } + private record Pos(int x, int y, int minx) {} + + /** + * A piece of text to display + * + * @param string The text + * @param colour The colour of the text as an RGBA packed int + */ + public record DisplayText(String string, int colour) { + private byte[] asBytes() { + return string.getBytes(StandardCharsets.US_ASCII); + } + + Pos generateStringArray(SimpleFont font, Pos pos, SimpleBufferBuilder bb) { + for (int i = 0; i < asBytes().length; i++) { + byte c = asBytes()[i]; + pos = switch (c) { + case '\n' -> new Pos(pos.minx(), pos.y()+font.lineSpacing(), pos.minx()); + case '\t' -> new Pos(pos.x()+font.glyphs[0].charwidth() * 4, pos.y(), pos.minx()); + case ' ' -> new Pos(pos.x()+font.glyphs[0].charwidth(), pos.y(), pos.minx()); + default -> { + if (c-32 < font.GLYPH_COUNT && c > 32) { + pos = font.glyphs[c - 32].loadQuad(pos, colour(), bb); + } + yield pos; + } + }; + } + return pos; + } + } + + /** + * Generate vertices for a set of display texts + * @param x The starting screen x coordinate + * @param y The starting screen y coordinate + * @param texts Some {@link DisplayText} to display + * @return a {@link SimpleBufferBuilder} that can draw the texts + */ + public SimpleBufferBuilder generateVerticesForTexts(int x, int y, SimpleBufferBuilder textBB, DisplayText... texts) { + var pos = new Pos(x, y, x); + for (DisplayText text : texts) { + pos = text.generateStringArray(this, pos, textBB); + } + return textBB; + } + +} diff --git a/fmlearlydisplay/src/main/resources/META-INF/services/net.minecraftforge.fml.loading.ImmediateWindowProvider b/fmlearlydisplay/src/main/resources/META-INF/services/net.minecraftforge.fml.loading.ImmediateWindowProvider new file mode 100644 index 0000000000..b3c1beea76 --- /dev/null +++ b/fmlearlydisplay/src/main/resources/META-INF/services/net.minecraftforge.fml.loading.ImmediateWindowProvider @@ -0,0 +1 @@ +net.minecraftforge.fml.earlydisplay.DisplayWindow \ No newline at end of file diff --git a/fmlearlydisplay/src/main/resources/Monocraft.ttf b/fmlearlydisplay/src/main/resources/Monocraft.ttf new file mode 100644 index 0000000000..4066b0a988 Binary files /dev/null and b/fmlearlydisplay/src/main/resources/Monocraft.ttf differ diff --git a/fmlearlydisplay/src/main/resources/forge_anvil.png b/fmlearlydisplay/src/main/resources/forge_anvil.png new file mode 100644 index 0000000000..13483bc12f Binary files /dev/null and b/fmlearlydisplay/src/main/resources/forge_anvil.png differ diff --git a/fmlearlydisplay/src/main/resources/forge_logo.png b/fmlearlydisplay/src/main/resources/forge_logo.png new file mode 100644 index 0000000000..49db5c8a77 Binary files /dev/null and b/fmlearlydisplay/src/main/resources/forge_logo.png differ diff --git a/fmlearlydisplay/src/main/resources/forgejwst.png b/fmlearlydisplay/src/main/resources/forgejwst.png new file mode 100644 index 0000000000..010942e591 Binary files /dev/null and b/fmlearlydisplay/src/main/resources/forgejwst.png differ diff --git a/fmlearlydisplay/src/main/resources/glfailure.txt b/fmlearlydisplay/src/main/resources/glfailure.txt new file mode 100644 index 0000000000..4f5ee4aa80 --- /dev/null +++ b/fmlearlydisplay/src/main/resources/glfailure.txt @@ -0,0 +1,13 @@ +█▀▀▀▀▀█ ▀▄▄▄▀█ █ █▀▀▀▀▀█ +█ ███ █ ███ ▀███▀ █ ███ █ +█ ▀▀▀ █ ▀ ▄ ███▀█ █ ▀▀▀ █ +▀▀▀▀▀▀▀ ▀ █ ▀ ▀▄█ ▀▀▀▀▀▀▀ +█▀█▀▄▄▀▄▀ █▄▀▄ ▀▀▀ ▄▀▀▀▄▀ +▄█▄▄ ▀▀███▀██▄ █▀ ▀▄▄ +▀█▄▀ ▀▀▄▄▀▀ █▀█▄▄████ ▀▀█ +▄▀▀▄▀ ▀▄▀▄█ ▀ ▀▀▀▄█ ▀▀▄ + ▀ ▀▀ ▄ ▄██▀ ▄█▀▀▀█▀█▀█ +█▀▀▀▀▀█ ▄█▄▄▀▀▄█ ▀ █ ▀▀▀ +█ ███ █ ▄▄ ▄ █ ▀██▀██▄██ +█ ▀▀▀ █ ██▄███▀█ █ █ █▀ +▀▀▀▀▀▀▀ ▀▀ ▀▀▀ ▀▀▀▀▀▀▀▀ \ No newline at end of file diff --git a/fmlearlydisplay/src/main/resources/squirrel.png b/fmlearlydisplay/src/main/resources/squirrel.png new file mode 100644 index 0000000000..3d1b2d6469 Binary files /dev/null and b/fmlearlydisplay/src/main/resources/squirrel.png differ diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/BackgroundWaiter.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/BackgroundWaiter.java index f5f52129e6..cac3c4f5c2 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/BackgroundWaiter.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/BackgroundWaiter.java @@ -5,6 +5,8 @@ package net.minecraftforge.fml.loading; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; + import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -14,6 +16,7 @@ public class BackgroundWaiter { private static ExecutorService runner = Executors.newSingleThreadExecutor(); public static void runAndTick(Runnable r, Runnable tick) { + ImmediateWindowHandler.updateProgress("Loading bootstrap resources"); final Future work = runner.submit(r); do { tick.run(); diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java index 6f14cc2531..e6ed31e6ab 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLConfig.java @@ -5,7 +5,9 @@ package net.minecraftforge.fml.loading; +import com.electronwill.nightconfig.core.CommentedConfig; import com.electronwill.nightconfig.core.ConfigSpec; +import com.electronwill.nightconfig.core.UnmodifiableCommentedConfig; import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.core.file.FileNotFoundAction; import com.electronwill.nightconfig.core.io.ParsingException; @@ -15,21 +17,78 @@ import org.slf4j.Logger; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; import static net.minecraftforge.fml.loading.LogMarkers.CORE; public class FMLConfig { + public enum ConfigValue { + SPLASHSCREEN("splashscreen", Boolean.TRUE, "should we show the early splashscreen"), + MAX_THREADS("maxThreads", -1, "Max threads for early initialization parallelism, -1 is based on processor count", FMLConfig::maxThreads), + VERSION_CHECK("versionCheck", Boolean.TRUE, "Enable forge global version checking"), + DEFAULT_CONFIG_PATH("defaultConfigPath", "defaultconfigs", "Default config path for servers"), + DISABLE_OPTIMIZED_DFU("disableOptimizedDFU", Boolean.TRUE, "Disables Optimized DFU client-side - already disabled on servers"), + EARLY_WINDOW_PROVIDER("earlyWindowProvider", "fmlearlywindow", "Early window provider"), + EARLY_WINDOW_WIDTH("earlyWindowWidth", 854, "Early window width"), + EARLY_WINDOW_HEIGHT("earlyWindowHeight", 480, "Early window height"), + EARLY_WINDOW_FBSCALE("earlyWindowFBScale", 2, "Early window framebuffer scale"), + EARLY_WINDOW_MAXIMIZED("earlyWindowMaximized", true, "Early window starts maximized") + ; + + private final String entry; + private final Object defaultValue; + private final String comment; + private final Class valueType; + private final Function entryFunction; + + ConfigValue(final String entry, final Object defaultValue, final String comment) { + this(entry, defaultValue, comment, Function.identity()); + } + ConfigValue(final String entry, final Object defaultValue, final String comment, Function entryFunction) { + this.entry = entry; + this.defaultValue = defaultValue; + this.comment = comment; + this.valueType = defaultValue.getClass(); + this.entryFunction = entryFunction; + } + + void buildConfigEntry(ConfigSpec spec, CommentedConfig commentedConfig) { + if (this.defaultValue instanceof List list) { + spec.defineList(this.entry, list, e -> e instanceof String); + } else { + spec.define(this.entry, this.defaultValue); + } + commentedConfig.setComment(this.entry, this.comment); + } + @SuppressWarnings("unchecked") + private T getConfigValue(CommentedFileConfig config) { + return (T) this.entryFunction.apply(config.get(this.entry)); + } + + public void updateValue(final CommentedFileConfig configData, final T value) { + configData.set(this.entry, value); + } + } + + private static Object maxThreads(final Object value) { + int val = (Integer)value; + if (val <= 0) return Runtime.getRuntime().availableProcessors(); + else return val; + } + private static final Logger LOGGER = LogUtils.getLogger(); - private static FMLConfig INSTANCE = new FMLConfig(); - private static ConfigSpec configSpec = new ConfigSpec(); + private static final FMLConfig INSTANCE = new FMLConfig(); + private static final ConfigSpec configSpec = new ConfigSpec(); + private static final CommentedConfig configComments = CommentedConfig.inMemory(); static { - configSpec.define("splashscreen", Boolean.TRUE); - configSpec.define("maxThreads", -1); - configSpec.define("versionCheck", Boolean.TRUE); - configSpec.define("defaultConfigPath", "defaultconfigs"); - configSpec.define("disableOptimizedDFU", Boolean.TRUE); + for (ConfigValue cv: ConfigValue.values()) { + cv.buildConfigEntry(configSpec, configComments); + } } private CommentedFileConfig configData; @@ -38,7 +97,6 @@ public class FMLConfig { configData = CommentedFileConfig.builder(configFile).sync() .onFileNotFound(FileNotFoundAction.copyData(Objects.requireNonNull(getClass().getResourceAsStream("/META-INF/defaultfmlconfig.toml")))) - .autosave().autoreload() .writingMode(WritingMode.REPLACE) .build(); try @@ -47,13 +105,14 @@ public class FMLConfig } catch (ParsingException e) { - throw new RuntimeException("Failed to load FML config from " + configFile.toString(), e); + throw new RuntimeException("Failed to load FML config from " + configFile, e); } if (!configSpec.isCorrect(configData)) { LOGGER.warn(CORE, "Configuration file {} is not correct. Correcting", configFile); configSpec.correct(configData, (action, path, incorrectValue, correctedValue) -> - LOGGER.warn(CORE, "Incorrect key {} was corrected from {} to {}", path, incorrectValue, correctedValue)); + LOGGER.info(CORE, "Incorrect key {} was corrected from {} to {}", path, incorrectValue, correctedValue)); } + configData.putAllComments(configComments); configData.save(); } @@ -64,33 +123,34 @@ public class FMLConfig if (LOGGER.isTraceEnabled(CORE)) { LOGGER.trace(CORE, "Loaded FML config from {}", FMLPaths.FMLCONFIG.get()); - LOGGER.trace(CORE, "Splash screen is {}", FMLConfig.splashScreenEnabled()); - LOGGER.trace(CORE, "Max threads for mod loading computed at {}", FMLConfig.loadingThreadCount()); - LOGGER.trace(CORE, "Version check is {}", FMLConfig.runVersionCheck()); - LOGGER.trace(CORE, "Default config paths at {}", FMLConfig.defaultConfigPath()); + for (ConfigValue cv: ConfigValue.values()) { + LOGGER.trace(CORE, "FMLConfig {} is {}", cv.entry, cv.getConfigValue(INSTANCE.configData)); + } } - FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.defaultConfigPath())); + FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH))); } - public static boolean splashScreenEnabled() { - return INSTANCE.configData.getOptional("splashscreen").orElse(Boolean.FALSE); + public static String getConfigValue(ConfigValue v) { + return v.getConfigValue(INSTANCE.configData); } - public static int loadingThreadCount() { - int val = INSTANCE.configData.getOptional("maxThreads").orElse(-1); - if (val <= 0) return Runtime.getRuntime().availableProcessors(); - return val; + public static boolean getBoolConfigValue(ConfigValue v) { + return v.getConfigValue(INSTANCE.configData); } - public static boolean runVersionCheck() { - return INSTANCE.configData.getOptional("versionCheck").orElse(Boolean.TRUE); + public static int getIntConfigValue(ConfigValue v) { + return v.getConfigValue(INSTANCE.configData); + } + + public static List getListConfigValue(ConfigValue v) { + return v.getConfigValue(INSTANCE.configData); + } + public static void updateConfig(ConfigValue v, T value) { + v.updateValue(INSTANCE.configData, value); + INSTANCE.configData.save(); } public static String defaultConfigPath() { - return INSTANCE.configData.getOptional("defaultConfigPath").orElse("defaultconfigs"); - } - - public static boolean isOptimizedDFUDisabled() { - return INSTANCE.configData.getOptional("disableOptimizedDFU").orElse(Boolean.TRUE); + return getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLLoader.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLLoader.java index 7cf191366f..721b9a18f8 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLLoader.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/FMLLoader.java @@ -16,8 +16,7 @@ import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.fml.loading.moddiscovery.ModValidator; import net.minecraftforge.accesstransformer.service.AccessTransformerService; import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.fml.loading.progress.EarlyProgressVisualization; -import net.minecraftforge.fml.loading.progress.StartupMessageManager; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.fml.loading.targets.CommonLaunchHandler; import net.minecraftforge.forgespi.Environment; import net.minecraftforge.forgespi.coremod.ICoreModProvider; @@ -152,7 +151,6 @@ public class FMLLoader versionInfo = new VersionInfo(arguments); - StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Early Loading!")); accessTransformer.getExtension().accept(Pair.of(naming, "srg")); LOGGER.debug(CORE,"Received command line version data : {}", versionInfo); @@ -169,7 +167,6 @@ public class FMLLoader } public static List completeScan(IModuleLayerManager layerManager) { - progressWindowTick = EarlyProgressVisualization.INSTANCE.accept(dist, commonLaunchHandler.isData(), versionInfo.mcVersion()); moduleLayerManager = layerManager; languageLoadingProvider = new LanguageLoadingProvider(); backgroundScanHandler = modValidator.stage2Validation(); @@ -205,9 +202,10 @@ public class FMLLoader return dist; } - public static void beforeStart(ClassLoader launchClassLoader) + public static void beforeStart(ModuleLayer gameLayer) { - StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Launching minecraft")); + ImmediateWindowHandler.acceptGameLayer(gameLayer); + ImmediateWindowHandler.updateProgress("Launching minecraft"); progressWindowTick.run(); } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/ImmediateWindowHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ImmediateWindowHandler.java new file mode 100644 index 0000000000..41a7ddb679 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ImmediateWindowHandler.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading; + +import net.minecraftforge.fml.loading.progress.ProgressMeter; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.*; +import java.util.stream.Collectors; + +public class ImmediateWindowHandler { + private static final Logger LOGGER = LogManager.getLogger(); + + private static ImmediateWindowProvider provider; + + private static ProgressMeter earlyProgress; + public static void load(final String launchTarget, final String[] arguments) { + if (!List.of("forgeclient", "forgeclientuserdev", "forgeclientdev").contains(launchTarget)) { + provider = new DummyProvider(); + LOGGER.info("ImmediateWindowProvider not loading because launch target is {}", launchTarget); + } else if (!FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.SPLASHSCREEN)) { + provider = new DummyProvider(); + LOGGER.info("ImmediateWindowProvider not loading because splash screen is disabled"); + } else { + final var providername = FMLConfig.getConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_PROVIDER); + LOGGER.info("Loading ImmediateWindowProvider {}", providername); + final var maybeProvider = ServiceLoader.load(ImmediateWindowProvider.class) + .stream() + .map(ServiceLoader.Provider::get) + .filter(p -> Objects.equals(p.name(), providername)) + .findFirst(); + provider = maybeProvider.or(() -> { + LOGGER.info("Failed to find ImmediateWindowProvider {}, disabling", providername); + return Optional.of(new DummyProvider()); + }).orElseThrow(); + } + // Only update config if the provider isn't the dummy provider + if (!Objects.equals(provider.name(), "dummyprovider")) + FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_PROVIDER, provider.name()); + FMLLoader.progressWindowTick = provider.initialize(arguments); + earlyProgress = StartupNotificationManager.addProgressBar("EARLY", 0); + earlyProgress.label("Bootstrapping Minecraft"); + } + + public static long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier title, final LongSupplier monitor) { + return provider.setupMinecraftWindow(width, height, title, monitor); + } + + public static boolean positionWindow(Optional monitor,IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter) { + return provider.positionWindow(monitor, widthSetter, heightSetter, xSetter, ySetter); + } + + public static void updateFBSize(IntConsumer width, IntConsumer height) { + provider.updateFramebufferSize(width, height); + } + + public static Supplier loadingOverlay(Supplier mc, Supplier ri, Consumer> ex, boolean fade) { + earlyProgress.complete(); + return provider.loadingOverlay(mc, ri, ex, fade); + } + + public static void acceptGameLayer(final ModuleLayer layer) { + provider.updateModuleReads(layer); + } + + public static void renderTick() { + provider.periodicTick(); + } + + public static String getGLVersion() { + return provider.getGLVersion(); + } + public static void updateProgress(final String message) { + earlyProgress.label(message); + } + + private record DummyProvider() implements ImmediateWindowProvider { + private static Method NV_HANDOFF; + private static Method NV_POSITION; + private static Method NV_OVERLAY; + + @Override + public String name() { + return "dummyprovider"; + } + + @Override + public Runnable initialize(String[] args) { + return () -> {}; + } + + @Override + public void updateFramebufferSize(final IntConsumer width, final IntConsumer height) { + } + + @Override + public long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier title, final LongSupplier monitor) { + try { + var longsupplier = (LongSupplier)NV_HANDOFF.invoke(null, width, height, title, monitor); + return longsupplier.getAsLong(); + } catch (Throwable e) { + throw new IllegalStateException("How did you get here?", e); + } + } + + public boolean positionWindow(Optional monitor, IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter) { + try { + return (boolean)NV_POSITION.invoke(null, monitor, widthSetter, heightSetter, xSetter, ySetter); + } catch (Throwable e) { + throw new IllegalStateException("How did you get here?", e); + } + } + @SuppressWarnings("unchecked") + public Supplier loadingOverlay(Supplier mc, Supplier ri, Consumer> ex, boolean fade) { + try { + return (Supplier) NV_OVERLAY.invoke(null, mc, ri, ex, fade); + } catch (Throwable e) { + throw new IllegalStateException("How did you get here?", e); + } + } + + @Override + public String getGLVersion() { + return "3.2"; // The default minecraft GL version + } + + @Override + public void updateModuleReads(final ModuleLayer layer) { + var fm = layer.findModule("forge"); + if (fm.isPresent()) { + getClass().getModule().addReads(fm.get()); + var clz = fm.map(l -> Class.forName(l, "net.minecraftforge.client.loading.NoVizFallback")).orElseThrow(); + var methods = Arrays.stream(clz.getMethods()).filter(m -> Modifier.isStatic(m.getModifiers())).collect(Collectors.toMap(Method::getName, Function.identity())); + NV_HANDOFF = methods.get("windowHandoff"); + NV_OVERLAY = methods.get("loadingOverlay"); + NV_POSITION = methods.get("windowPositioning"); + } + } + + @Override + public void periodicTick() { + // NOOP + } + } +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/ImmediateWindowProvider.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ImmediateWindowProvider.java new file mode 100644 index 0000000000..d1b8c74058 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ImmediateWindowProvider.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; +import java.util.function.Supplier; + +/** + * This is for allowing the plugging in of alternative early display implementations. + * + * They can be selected through the config value "earlyWindowProvider" which defaults to "fmlearlywindow" implemented by {@link net.minecraftforge.fml.earlydisplay.DisplayWindow} + * + * There are a few key things to keep in mind if following through on implementation. You cannot access the game state as it + * literally DOES NOT EXIST at the time this object is constructed. You have to be very careful about managing the handoff + * to mojang, be sure that if you're trying to tick your window in a background thread (a nice idea!) that you properly + * transition to the main thread before handoff is complete. Do note that in general, you should construct your GL objects + * on the MAIN thread before starting your ticker, to ensure MacOS compatibility. + * + * No doubt many more things can be said here. + */ +public interface ImmediateWindowProvider { + /** + * @return The name of this window provider. Do NOT use fmlearlywindow. + */ + String name(); + + /** + * This is called very early on to initialize ourselves. Use this to initialize the window and other GL core resources. + * + * One thing we want to ensure is that we try and create the highest GL_PROFILE we can accomplish. + * GLFW_CONTEXT_VERSION_MAJOR,GLFW_CONTEXT_VERSION_MINOR should be as high as possible on the created window, + * and it should have all the typical profile settings. + * + * @param arguments The arguments provided to the Java process. This is the entire command line, so you can process + * stuff from it. + * @return A runnable that will be periodically ticked by FML during startup ON THE MAIN THREAD. This is usually + * a good place to put glfwPollEvents() tests. + */ + Runnable initialize(String[] arguments); + + /** + * This will be called during the handoff to minecraft to update minecraft with the size of the framebuffer we have. + * Generally won't be called because Minecraft figures it out for itself. + * @param width Consumer of the framebuffer width + * @param height Consumer of the framebuffer height + */ + void updateFramebufferSize(IntConsumer width, IntConsumer height); + + /** + * This is called to setup the minecraft window, as if Mojang had done it themselves in their Window class. This + * handoff is difficult to get right - you have to make sure that any activities you're doing to the window are finished + * prior to returning. You should try and setup the width and height as Mojang expects - the suppliers give you all that + * information. Alternatively, you can force Mojang to update from the current position of the window in {@link #positionWindow(Optional, IntConsumer, IntConsumer, IntConsumer, IntConsumer)} + * instead. This might give a more seamless experience. + * + * @param width This is the width of the window Mojang expects + * @param height This is the height of the Window Mojang expects. + * @param title This is the title for the window. + * @param monitor This is the monitor it should appear on. + * @return The window id + */ + long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier title, final LongSupplier monitor); + + /** + * This is called after window handoff to allow us to tell Mojang about our window's position. This might give a + * preferrable user experience to users, because we just tell Mojang our truth, rather than accept theirs. + * @param monitor This is the monitor we're rendering on. Note that this is the Mojang monitor object. You might have trouble unwrapping it. + * @param widthSetter This sets the width on the Mojang side + * @param heightSetter This sets the height on the Mojang side + * @param xSetter This sets the x coordinate on the Mojang side + * @param ySetter This sets the y coordinate on the Mojang side + * @return true if you've handled the window positioning - this skips the "forced fullscreen" code until a later stage + */ + boolean positionWindow(Optional monitor, IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter); + + /** + * Return a Supplier of an object extending the LoadingOverlay class from Mojang. This is what will be used once + * the Mojang window code has taken over rendering of the window, to render the later stages of the loading process. + * + * @param mc This supplies the Minecraft object + * @param ri This supplies the ReloadInstance object that tells us when the loading is finished + * @param ex This Consumes the final state of the loading - if it's an error you pass it the Throwable, otherwise you + * pass Optional.empty() + * @param fade This is the fade flag passed to LoadingOverlay. You probably want to ignore it. + * @param This is the type LoadingOverlay to allow type binding on the Mojang side + * @return A supplier of your later LoadingOverlay screen. + */ + Supplier loadingOverlay(Supplier mc, Supplier ri, Consumer> ex, boolean fade); + + /** + * This is called during the module loading process to allow us to find objects inside the GAME layer, such as a + * later loading screen. + * @param layer This is the GAME layer from ModLauncher + */ + void updateModuleReads(ModuleLayer layer); + + /** + * This is called periodically during the loading process to "tick" the window. It is typically the same as the Runnable + * from {@link #initialize(String[])} + */ + void periodicTick(); + + /** + * This is called to construct a {@link net.minecraftforge.forgespi.locating.ForgeFeature} for the GL_VERSION we + * managed to create for the window. Should be a string of the format {MAJOR}.{MINOR}, such as 4.6, 4.5 or such. + * + * @return the GL profile we created + */ + String getGLVersion(); +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/LanguageLoadingProvider.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/LanguageLoadingProvider.java index 1c225599da..e1663a1ea0 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/LanguageLoadingProvider.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/LanguageLoadingProvider.java @@ -9,7 +9,7 @@ import com.mojang.logging.LogUtils; import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.util.ServiceLoaderUtils; -import net.minecraftforge.fml.loading.progress.StartupMessageManager; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.forgespi.language.IModLanguageProvider; import net.minecraftforge.fml.loading.moddiscovery.ExplodedDirectoryLocator; import net.minecraftforge.fml.loading.moddiscovery.ModFile; @@ -83,7 +83,7 @@ public class LanguageLoadingProvider private void loadLanguageProviders() { LOGGER.debug(CORE, "Found {} language providers", ServiceLoaderUtils.streamServiceLoader(()->serviceLoader, sce->LOGGER.error("Problem with language loaders")).count()); serviceLoader.forEach(languageProviders::add); - + ImmediateWindowHandler.updateProgress("Loading language providers"); languageProviders.forEach(lp -> { final Path lpPath; try { @@ -98,7 +98,7 @@ public class LanguageLoadingProvider throw new RuntimeException("Failed to find implementation version for language provider "+ lp.name()); } LOGGER.debug(CORE, "Found language provider {}, version {}", lp.name(), impl); - StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Loaded language provider "+lp.name()+ " " + impl)); + ImmediateWindowHandler.updateProgress("Loaded language provider "+lp.name()+ " " + impl); languageProviderMap.put(lp.name(), new ModLanguageWrapper(lp, new DefaultArtifactVersion(impl))); }); } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java index eb810bae15..9609f85638 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java @@ -7,6 +7,7 @@ package net.minecraftforge.fml.loading; import com.mojang.logging.LogUtils; import cpw.mods.jarhandling.SecureJar; +import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.LamdbaExceptionUtils; import cpw.mods.modlauncher.api.NamedPath; import cpw.mods.modlauncher.serviceapi.ITransformerDiscoveryService; @@ -18,6 +19,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.ServiceLoader; +import java.util.zip.ZipFile; import java.util.Set; public class ModDirTransformerDiscoverer implements ITransformerDiscoveryService { @@ -28,6 +31,18 @@ public class ModDirTransformerDiscoverer implements ITransformerDiscoveryService "net.minecraftforge.forgespi.locating.IDependencyLocator" ); + @Override + public List candidates(final Path gameDirectory, final String launchTarget) { + FMLPaths.loadAbsolutePaths(gameDirectory); + FMLConfig.load(); + return candidates(gameDirectory); + } + + @Override + public void earlyInitialization(final String launchTarget, final String[] arguments) { + ImmediateWindowHandler.load(launchTarget, arguments); + } + @Override public List candidates(final Path gameDirectory) { ModDirTransformerDiscoverer.scan(gameDirectory); diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/BackgroundScanHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/BackgroundScanHandler.java index c4550454b7..494c56bb5e 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/BackgroundScanHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/BackgroundScanHandler.java @@ -6,8 +6,10 @@ package net.minecraftforge.fml.loading.moddiscovery; import com.mojang.logging.LogUtils; +import net.minecraftforge.fml.loading.ImmediateWindowHandler; import net.minecraftforge.fml.loading.LoadingModList; import net.minecraftforge.fml.loading.LogMarkers; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.forgespi.language.ModFileScanData; import org.slf4j.Logger; @@ -60,6 +62,7 @@ public class BackgroundScanHandler throw new IllegalStateException("Scanner has shutdown"); } status = ScanStatus.RUNNING; + ImmediateWindowHandler.updateProgress("Scanning mod candidates"); allFiles.add(file); pendingFiles.add(file); final CompletableFuture future = CompletableFuture.supplyAsync(file::compileContent, modContentScanner) diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java index 7e0b36abec..27d4d9dddb 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java @@ -12,9 +12,10 @@ import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.util.ServiceLoaderUtils; import net.minecraftforge.fml.loading.EarlyLoadingException; +import net.minecraftforge.fml.loading.ImmediateWindowHandler; import net.minecraftforge.fml.loading.LogMarkers; import net.minecraftforge.fml.loading.UniqueModListBuilder; -import net.minecraftforge.fml.loading.progress.StartupMessageManager; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.forgespi.Environment; import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.locating.IDependencyLocator; @@ -38,7 +39,7 @@ public class ModDiscoverer { public ModDiscoverer(Map arguments) { Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.MODDIRECTORYFACTORY.get(), v->ModsFolderLocator::new); - Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), v-> StartupMessageManager.locatorConsumer().orElseGet(()-> s->{})); + Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), v-> StartupNotificationManager.locatorConsumer().orElseGet(()-> s->{})); final var moduleLayerManager = Launcher.INSTANCE.environment().findModuleLayerManager().orElseThrow(); modLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IModLocator.class); dependencyLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IDependencyLocator.class); @@ -66,7 +67,7 @@ public class ModDiscoverer { List discoveryErrorData = new ArrayList<>(); boolean successfullyLoadedMods = true; List brokenFiles = new ArrayList<>(); - + ImmediateWindowHandler.updateProgress("Discovering mod files"); //Loop all mod locators to get the prime mods to load from. for (IModLocator locator : modLocatorList) { try { @@ -168,7 +169,6 @@ public class ModDiscoverer { var locatedModFiles = locatedFiles.stream().filter(ModFile.class::isInstance).map(ModFile.class::cast).toList(); for (IModFile mf : locatedModFiles) { LOGGER.info(LogMarkers.SCAN, "Found mod file {} of type {} with provider {}", mf.getFileName(), mf.getType(), mf.getProvider()); - StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Found mod file "+mf.getFileName()+" of type "+mf.getType())); } loadedFiles.addAll(locatedModFiles); } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java index 17feeb36cb..f95c4dedb1 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java @@ -10,7 +10,7 @@ import com.mojang.logging.LogUtils; import cpw.mods.jarhandling.SecureJar; import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.LogMarkers; -import net.minecraftforge.fml.loading.progress.StartupMessageManager; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.IModLanguageProvider; @@ -157,7 +157,6 @@ public class ModFile implements IModFile { if (throwable != null) { this.scanError = throwable; } - StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Completed deep scan of "+this.getFileName())); } public void setFileProperties(Map fileProperties) { diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java index 4db790f857..08e3275b58 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java @@ -8,11 +8,8 @@ package net.minecraftforge.fml.loading.moddiscovery; import com.mojang.logging.LogUtils; import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.api.ITransformationService; -import net.minecraftforge.fml.loading.EarlyLoadingException; -import net.minecraftforge.fml.loading.LoadingModList; -import net.minecraftforge.fml.loading.LogMarkers; -import net.minecraftforge.fml.loading.ModSorter; -import net.minecraftforge.fml.loading.progress.StartupMessageManager; +import net.minecraftforge.fml.loading.*; +import net.minecraftforge.fml.loading.progress.StartupNotificationManager; import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.locating.IModFile; import org.jetbrains.annotations.NotNull; @@ -52,7 +49,7 @@ public class ModValidator { if (LOGGER.isDebugEnabled(LogMarkers.SCAN)) { LOGGER.debug(LogMarkers.SCAN, "Found {} mod files with {} mods", candidateMods.size(), candidateMods.stream().mapToInt(mf -> mf.getModInfos().size()).sum()); } - StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Found "+ candidateMods.size()+" modfiles to load")); + ImmediateWindowHandler.updateProgress("Found "+candidateMods.size()+" mod candidates"); } @NotNull diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/ClientVisualization.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/ClientVisualization.java deleted file mode 100644 index 7fa1082321..0000000000 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/ClientVisualization.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -/* - - -package net.minecraftforge.fml.loading.progress; - -import com.google.common.io.ByteStreams; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.lwjgl.PointerBuffer; -import org.lwjgl.glfw.*; -import org.lwjgl.opengl.GL; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL14; -import org.lwjgl.stb.STBEasyFont; -import org.lwjgl.stb.STBImage; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.MemoryUtil; - -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryUsage; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.IntBuffer; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.IntConsumer; -import java.util.function.IntSupplier; -import java.util.function.LongSupplier; -import java.util.function.Supplier; - -import static org.lwjgl.glfw.GLFW.*; -import static org.lwjgl.glfw.GLFW.glfwCreateWindow; -import static org.lwjgl.opengl.GL11.*; -import static org.lwjgl.system.MemoryStack.stackPush; -import static org.lwjgl.system.MemoryUtil.NULL; - - -class ClientVisualization implements EarlyProgressVisualization.Visualization { - private final int screenWidth = 854; - private final int screenHeight = 480; - private long window; - private Thread renderThread = new Thread(this::renderThreadFunc); - - private boolean running = true; - private GLFWFramebufferSizeCallback framebufferSizeCallback; - private int[] fbSize; - - private void initWindow(@Nullable String mcVersion) { - GLFWErrorCallback.createPrint(System.err).set(); - - long glfwInitBegin = System.nanoTime(); - if (!glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - long glfwInitEnd = System.nanoTime(); - - if (glfwInitEnd - glfwInitBegin > 1e9) { - LogManager.getLogger().fatal("WARNING : glfwInit took {} seconds to start.", (glfwInitEnd-glfwInitBegin) / 1.0e9); - } - - // Clear the Last Exception (#7285 - Prevent Vanilla throwing an IllegalStateException due to invalid controller mappings) - handleLastGLFWError((error, description) -> LogManager.getLogger().error(String.format("Suppressing Last GLFW error: [0x%X]%s", error, description))); - - glfwDefaultWindowHints(); - glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); - glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); - - if (mcVersion != null) - { - // this emulates what we would get without early progress window - // as vanilla never sets these, so GLFW uses the first window title - // set them explicitly to avoid it using "FML early loading progress" as the class - String vanillaWindowTitle = "Minecraft* " + mcVersion; - glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle); - glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle); - } - - window = glfwCreateWindow(screenWidth, screenHeight, "FML early loading progress", NULL, NULL); - if (window == NULL) { - throw new RuntimeException("Failed to create the GLFW window"); // ignore it and make the GUI optional? - } - framebufferSizeCallback = GLFWFramebufferSizeCallback.create(this::fbResize); - - try (MemoryStack stack = stackPush()) { - IntBuffer pWidth = stack.mallocInt(1); - IntBuffer pHeight = stack.mallocInt(1); - IntBuffer monPosLeft = stack.mallocInt(1); - IntBuffer monPosTop = stack.mallocInt(1); - glfwGetWindowSize(window, pWidth, pHeight); - - // try to center the window, this is a best-effort as there may not be - // a primary monitor and we might not even be on the primary monitor... - long primaryMonitor = glfwGetPrimaryMonitor(); - if (primaryMonitor != NULL) - { - GLFWVidMode vidmode = glfwGetVideoMode(primaryMonitor); - glfwGetMonitorPos(primaryMonitor, monPosLeft, monPosTop); - glfwSetWindowPos( - window, - (vidmode.width() - pWidth.get(0)) / 2 + monPosLeft.get(0), - (vidmode.height() - pHeight.get(0)) / 2 + monPosTop.get(0) - ); - } - IntBuffer iconWidth = stack.mallocInt(1); - IntBuffer iconHeight = stack.mallocInt(1); - IntBuffer iconChannels = stack.mallocInt(1); - final GLFWImage.Buffer glfwImages = GLFWImage.mallocStack(1, stack); - byte[] icon; - try { - icon = ByteStreams.toByteArray(getClass().getClassLoader().getResourceAsStream("forge_icon.png")); - final ByteBuffer iconBuf = stack.malloc(icon.length); - iconBuf.put(icon); - ((Buffer)iconBuf).position(0); - final ByteBuffer imgBuffer = STBImage.stbi_load_from_memory(iconBuf, iconWidth, iconHeight, iconChannels, 4); - if (imgBuffer == null) { - throw new NullPointerException("Failed to load window icon"); // fall down to catch block - } - glfwImages.position(0); - glfwImages.width(iconWidth.get(0)); - glfwImages.height(iconHeight.get(0)); - ((Buffer)imgBuffer).position(0); - glfwImages.pixels(imgBuffer); - glfwImages.position(0); - glfwSetWindowIcon(window, glfwImages); - STBImage.stbi_image_free(imgBuffer); - } catch (NullPointerException | IOException e) { - System.err.println("Failed to load forge logo"); - } - } - int[] w = new int[1]; - int[] h = new int[1]; - glfwGetFramebufferSize(window, w, h); - fbSize = new int[] {w[0], h[0]}; - glfwSetFramebufferSizeCallback(window, framebufferSizeCallback); - glfwShowWindow(window); - glfwPollEvents(); - } - - private void renderProgress() { - // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - // glMatrixMode(GL_PROJECTION); - // glLoadIdentity(); - // glOrtho(0.0D, screenWidth, screenHeight, 0.0D, -1000.0D, 1000.0D); - // glMatrixMode(GL_MODELVIEW); - // glLoadIdentity(); - // - // glEnableClientState(GL11.GL_VERTEX_ARRAY); - // glEnable(GL_BLEND); - // renderBackground(); - // renderMessages(); - // glfwSwapBuffers(window); - } - - private static float clamp(float num, float min, float max) { - if (num < min) { - return min; - } else { - return num > max ? max : num; - } - } - - private static int clamp(int num, int min, int max) { - if (num < min) { - return min; - } else { - return num > max ? max : num; - } - } - - private static int hsvToRGB(float hue, float saturation, float value) { - int i = (int)(hue * 6.0F) % 6; - float f = hue * 6.0F - (float)i; - float f1 = value * (1.0F - saturation); - float f2 = value * (1.0F - f * saturation); - float f3 = value * (1.0F - (1.0F - f) * saturation); - float f4; - float f5; - float f6; - switch(i) { - case 0: - f4 = value; - f5 = f3; - f6 = f1; - break; - case 1: - f4 = f2; - f5 = value; - f6 = f1; - break; - case 2: - f4 = f1; - f5 = value; - f6 = f3; - break; - case 3: - f4 = f1; - f5 = f2; - f6 = value; - break; - case 4: - f4 = f3; - f5 = f1; - f6 = value; - break; - case 5: - f4 = value; - f5 = f1; - f6 = f2; - break; - default: - throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + hue + ", " + saturation + ", " + value); - } - - int j = clamp((int)(f4 * 255.0F), 0, 255); - int k = clamp((int)(f5 * 255.0F), 0, 255); - int l = clamp((int)(f6 * 255.0F), 0, 255); - return j << 16 | k << 8 | l; - } - - private void renderBackground() { - glBegin(GL_QUADS); - glColor4f(239F / 255F, 50F / 255F, 61F / 255F, 255F / 255F); //Color from ResourceLoadProgressGui - glVertex2f(0, 0); - glVertex2f(0, screenHeight); - glVertex2f(screenWidth, screenHeight); - glVertex2f(screenWidth, 0); - glEnd(); - } - - private void fbResize(long window, int width, int height) { - if (window == this.window && width != 0 && height != 0) { - fbSize = new int[] {width, height}; - } - } - - private void handleLastGLFWError(BiConsumer handler) { - try (MemoryStack memorystack = MemoryStack.stackPush()) { - PointerBuffer pointerbuffer = memorystack.mallocPointer(1); - int error = GLFW.glfwGetError(pointerbuffer); - if (error != GLFW_NO_ERROR) { - long pDescription = pointerbuffer.get(); - String description = pDescription == 0L ? "" : MemoryUtil.memUTF8(pDescription); - handler.accept(error, description); - } - } - } - - private void renderMessages() { - List> messages = StartupMessageManager.getMessages(); - for (int i = 0; i < messages.size(); i++) { - final Pair pair = messages.get(i); - final float fade = clamp((4000.0f - (float) pair.getLeft() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f); - if (fade <0.01f) continue; - StartupMessageManager.Message msg = pair.getRight(); - renderMessage(msg.getText(), msg.getTypeColour(), ((screenHeight - 15) / 20) - i, fade); - } - renderMemoryInfo(); - } - - @Override - public void updateFBSize(final IntConsumer width, final IntConsumer height) { - width.accept(this.fbSize[0]); - height.accept(this.fbSize[1]); - } - - private static final float[] memorycolour = new float[] { 0.0f, 0.0f, 0.0f}; - - private void renderMemoryInfo() { - final MemoryUsage heapusage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); - final MemoryUsage offheapusage = ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage(); - final float pctmemory = (float) heapusage.getUsed() / heapusage.getMax(); - String memory = String.format("Memory Heap: %d / %d MB (%.1f%%) OffHeap: %d MB", heapusage.getUsed() >> 20, heapusage.getMax() >> 20, pctmemory * 100.0, offheapusage.getUsed() >> 20); - - final int i = hsvToRGB((1.0f - (float)Math.pow(pctmemory, 1.5f)) / 3f, 1.0f, 0.5f); - memorycolour[2] = ((i) & 0xFF) / 255.0f; - memorycolour[1] = ((i >> 8 ) & 0xFF) / 255.0f; - memorycolour[0] = ((i >> 16 ) & 0xFF) / 255.0f; - renderMessage(memory, memorycolour, 1, 1.0f); - } - - private void renderMessage(final String message, final float[] colour, int row, float alpha) { - ByteBuffer charBuffer = MemoryUtil.memAlloc(message.length() * 270); - int quads = STBEasyFont.stb_easy_font_print(0, 0, message, null, charBuffer); - - glVertexPointer(3, GL11.GL_FLOAT, 16, charBuffer); - glEnable(GL_BLEND); - GL14.glBlendColor(0,0,0, alpha); - glBlendFunc(GL14.GL_CONSTANT_ALPHA, GL14.GL_ONE_MINUS_CONSTANT_ALPHA); - glColor3f(colour[0], colour[1], colour[2]); - glPushMatrix(); - glTranslatef(10, row * 20, 0); - glScalef(2, 2, 1); - glDrawArrays(GL11.GL_QUADS, 0, quads * 4); - glPopMatrix(); - MemoryUtil.memFree(charBuffer); - } - - private void renderThreadFunc() { - glfwMakeContextCurrent(window); - glfwSwapInterval(1); - GL.createCapabilities(); - glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - while (running) { - renderProgress(); - try { - Thread.sleep(50); - } catch (InterruptedException ignored) { - break; - } - } - glfwMakeContextCurrent(0); - } - - @Override - public Runnable start(@Nullable String mcVersion) { - initWindow(mcVersion); - renderThread.setDaemon(true); // Don't hang the game if it terminates before handoff (i.e. datagen) - renderThread.start(); - return org.lwjgl.glfw.GLFW::glfwPollEvents; - } - - @Override - public long handOffWindow(final IntSupplier width, final IntSupplier height, final Supplier title, final LongSupplier monitorSupplier) { - running = false; - try { - renderThread.join(); - } catch (InterruptedException ignored) { - } - glfwSetWindowTitle(window, title.get()); - glfwSetWindowSize(window, width.getAsInt(), height.getAsInt()); - if (monitorSupplier.getAsLong() != 0L) - glfwSetWindowMonitor(window, monitorSupplier.getAsLong(), 0, 0, width.getAsInt(), height.getAsInt(), GLFW_DONT_CARE); - glfwMakeContextCurrent(window); - GL.createCapabilities(); - glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - renderProgress(); - glfwSwapInterval(0); - glfwSwapBuffers(window); - glfwSwapInterval(1); - final GLFWFramebufferSizeCallback previous = glfwSetFramebufferSizeCallback(window, null); - previous.free(); - return window; - } -} -*/ diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/EarlyProgressVisualization.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/EarlyProgressVisualization.java deleted file mode 100644 index 75e6a28a6c..0000000000 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/EarlyProgressVisualization.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.minecraftforge.fml.loading.progress; - -import cpw.mods.modlauncher.api.LamdbaExceptionUtils; -import net.minecraftforge.fml.loading.FMLLoader; -import net.minecraftforge.api.distmarker.Dist; -import org.jetbrains.annotations.Nullable; - -import java.util.function.IntConsumer; -import java.util.function.IntSupplier; -import java.util.function.LongSupplier; -import java.util.function.Supplier; - -public enum EarlyProgressVisualization { - INSTANCE; - - private Visualization visualization; - - public Runnable accept(Dist dist, boolean isData) - { - return accept(dist, isData, null); - } - - public Runnable accept(final Dist dist, final boolean isData, @Nullable String mcVersion) { -// visualization = !isData && dist.isClient() && Boolean.parseBoolean(System.getProperty("fml.earlyprogresswindow", "true")) ? new ClientVisualization() : new NoVisualization(); - visualization = new NoVisualization(); - return visualization.start(mcVersion); - } - - public long handOffWindow(final IntSupplier width, final IntSupplier height, final Supplier title, final LongSupplier monitor) { - return visualization.handOffWindow(width, height, title, monitor); - } - - public void updateFBSize(IntConsumer width, IntConsumer height) { - visualization.updateFBSize(width, height); - } - - interface Visualization { - Runnable start(@Nullable String mcVersion); - - default long handOffWindow(final IntSupplier width, final IntSupplier height, final Supplier title, LongSupplier monitorSupplier) { - return FMLLoader.getGameLayer().findModule("forge") - .map(l->Class.forName(l, "net.minecraftforge.client.loading.NoVizFallback")) - .map(LamdbaExceptionUtils.rethrowFunction(c->c.getMethod("fallback", IntSupplier.class, IntSupplier.class, Supplier.class, LongSupplier.class))) - .map(LamdbaExceptionUtils.rethrowFunction(m->(LongSupplier)m.invoke(null, width, height, title, monitorSupplier))) - .map(LongSupplier::getAsLong) - .orElseThrow(()->new IllegalStateException("Why are you here?")); - } - - default void updateFBSize(IntConsumer width, IntConsumer height) { - } - } - - private static class NoVisualization implements Visualization { - @Override - public Runnable start(@Nullable String mcVersion) { - return () -> {}; - } - } - -} - diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/Message.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/Message.java new file mode 100644 index 0000000000..138159da65 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/Message.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading.progress; + +public class Message { + private final String text; + private final MessageType type; + private final long timestamp; + + public Message(final String text, final MessageType type) { + this.text = text; + this.type = type; + this.timestamp = System.nanoTime(); + } + + public String getText() { + return text; + } + + MessageType getType() { + return type; + } + + long timestamp() { + return timestamp; + } + + public float[] getTypeColour() { + return type.colour(); + } + + enum MessageType { + MC(1.0f, 1.0f, 1.0f), + ML(0.0f, 0.0f, 0.5f), + LOC(0.0f, 0.5f, 0.0f), + MOD(0.5f, 0.0f, 0.0f); + + private final float[] colour; + + MessageType(final float r, final float g, final float b) { + colour = new float[] {r,g,b}; + } + + public float[] colour() { + return colour; + } + } +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/ProgressMeter.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/ProgressMeter.java new file mode 100644 index 0000000000..79c8130fd6 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/ProgressMeter.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading.progress; + +import java.util.concurrent.atomic.AtomicInteger; + +public final class ProgressMeter { + private final String name; + private final int steps; + private AtomicInteger current; + private Message label; + + public ProgressMeter(String name, int steps, int current, Message label) { + this.name = name; + this.steps = steps; + this.current = new AtomicInteger(current); + this.label = label; + } + + public String name() { + return name; + } + + public int steps() { + return steps; + } + + public int current() { + return current.get(); + } + + public Message label() { + return label; + } + + public void increment() { + this.current.incrementAndGet(); + } + + public void complete() { + StartupNotificationManager.popBar(this); + } + + public float progress() { + return current.get()/(float)steps; + } + + public void setAbsolute(final int absolute) { + this.current.set(absolute); + } + + public void label(final String message) { + this.label = new Message(message, Message.MessageType.ML); + } +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/StartupMessageManager.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/StartupMessageManager.java deleted file mode 100644 index 735d1281c9..0000000000 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/StartupMessageManager.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.minecraftforge.fml.loading.progress; - -import com.google.common.base.Ascii; -import com.google.common.base.CharMatcher; -import org.apache.commons.lang3.tuple.Pair; - -import java.util.*; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -public class StartupMessageManager { - - private static volatile EnumMap> messages = new EnumMap<>(MessageType.class); - - public static List> getMessages() { - final long ts = System.nanoTime(); - return messages.values().stream().flatMap(Collection::stream). - sorted(Comparator.comparingLong(Message::getTimestamp).thenComparing(Message::getText).reversed()). - map(m -> Pair.of((int) ((ts - m.timestamp) / 1e6), m)). - limit(5). - collect(Collectors.toList()); - } - - public static class Message { - private final String text; - private final MessageType type; - private final long timestamp; - - public Message(final String text, final MessageType type) { - this.text = text; - this.type = type; - this.timestamp = System.nanoTime(); - } - - public String getText() { - return text; - } - - MessageType getType() { - return type; - } - - long getTimestamp() { - return timestamp; - } - - public float[] getTypeColour() { - return type.colour(); - } - } - - enum MessageType { - MC(1.0f, 1.0f, 1.0f), - ML(0.0f, 0.0f, 0.5f), - LOC(0.0f, 0.5f, 0.0f), - MOD(0.5f, 0.0f, 0.0f); - - private final float[] colour; - - MessageType(final float r, final float g, final float b) { - colour = new float[] {r,g,b}; - } - - public float[] colour() { - return colour; - } - } - - private synchronized static void addMessage(MessageType type, String message, int maxSize) - { - EnumMap> newMessages = new EnumMap<>(messages); - newMessages.compute(type, (key, existingList) -> { - List newList = new ArrayList<>(); - if (existingList != null) - { - if (maxSize < 0) - { - newList.addAll(existingList); - } - else - { - newList.addAll(existingList.subList(0, Math.min(existingList.size(), maxSize))); - } - } - newList.add(new Message(message, type)); - return newList; - }); - messages = newMessages; - } - - public static void addModMessage(final String message) { - final String safeMessage = Ascii.truncate(CharMatcher.ascii().retainFrom(message),80,"~"); - addMessage(MessageType.MOD, safeMessage, 20); - } - - public static Optional> modLoaderConsumer() { - return Optional.of(s-> addMessage(MessageType.ML, s, -1)); - } - - public static Optional> locatorConsumer() { - return Optional.of(s -> addMessage(MessageType.LOC, s, -1)); - } - - public static Optional> mcLoaderConsumer() { - return Optional.of(s-> addMessage(MessageType.MC, s, -1)); - } -} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/StartupNotificationManager.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/StartupNotificationManager.java new file mode 100644 index 0000000000..4a5236a1a3 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/progress/StartupNotificationManager.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading.progress; + +import com.google.common.base.Ascii; +import com.google.common.base.CharMatcher; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class StartupNotificationManager { + private static volatile EnumMap> messages = new EnumMap<>(Message.MessageType.class); + + private static final Deque progressMeters = new ArrayDeque<>(); + + public static List getCurrentProgress() { + synchronized (progressMeters) { + return progressMeters.stream().toList(); + } + } + + public static ProgressMeter addProgressBar(final String barName, final int count) { + var pm = new ProgressMeter(barName, count, 0, new Message(barName, Message.MessageType.ML)); + synchronized (progressMeters) { + progressMeters.push(pm); + } + return pm; + } + + public static void popBar(final ProgressMeter progressMeter) { + synchronized (progressMeters) { + progressMeters.remove(progressMeter); + } + } + + public record AgeMessage(int age, Message message) {} + public static List getMessages() { + final long ts = System.nanoTime(); + return messages.values().stream().flatMap(Collection::stream) + .sorted(Comparator.comparingLong(Message::timestamp).thenComparing(Message::getText).reversed()) + .map(m -> new AgeMessage((int) ((ts - m.timestamp()) / 1000000), m)) + .limit(2) + .toList(); + } + + private synchronized static void addMessage(Message.MessageType type, String message, int maxSize) + { + EnumMap> newMessages = new EnumMap<>(messages); + newMessages.compute(type, (key, existingList) -> { + List newList = new ArrayList<>(); + if (existingList != null) + { + if (maxSize < 0) + { + newList.addAll(existingList); + } + else + { + newList.addAll(existingList.subList(0, Math.min(existingList.size(), maxSize))); + } + } + newList.add(new Message(message, type)); + return newList; + }); + messages = newMessages; + } + + public static void addModMessage(final String message) { + final String safeMessage = Ascii.truncate(CharMatcher.ascii().retainFrom(message),80,"~"); + addMessage(Message.MessageType.MOD, safeMessage, 20); + } + + public static Optional> modLoaderConsumer() { + return Optional.of(s-> addMessage(Message.MessageType.ML, s, -1)); + } + + public static Optional> locatorConsumer() { + return Optional.of(s -> addMessage(Message.MessageType.LOC, s, -1)); + } + + public static Optional> mcLoaderConsumer() { + return Optional.of(s-> addMessage(Message.MessageType.MC, s, -1)); + } +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonClientLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonClientLaunchHandler.java index 0d2b455354..918e2a26d1 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonClientLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonClientLaunchHandler.java @@ -21,12 +21,8 @@ public abstract class CommonClientLaunchHandler extends CommonLaunchHandler { @Override public boolean isProduction() { return true; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(),"net.minecraft.client.main.Main").getMethod("main", String[].class).invoke(null, (Object)args); - }; + protected ServiceRunner makeService(final String[] arguments, final ModuleLayer gameLayer) { + return ()->clientService(arguments, gameLayer); } @Override diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonDevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonDevLaunchHandler.java index 43f6c578c8..5a8ba6d897 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonDevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonDevLaunchHandler.java @@ -6,6 +6,7 @@ package net.minecraftforge.fml.loading.targets; import cpw.mods.jarhandling.SecureJar; +import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.fml.loading.FileUtils; import java.io.File; @@ -127,4 +128,12 @@ public abstract class CommonDevLaunchHandler extends CommonLaunchHandler { // Generate a time-based random number, to mimic how n.m.client.Main works return Long.toString(System.nanoTime() % (int) Math.pow(10, length)); } + + @Override + protected ServiceRunner makeService(final String[] arguments, final ModuleLayer gameLayer) { + var args = preLaunch(arguments, gameLayer); + return ()->devService(args, gameLayer); + } + + abstract void devService(final String[] arguments, final ModuleLayer gameLayer) throws Throwable; } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonLaunchHandler.java index 1d8b7fea7c..2a9aaf95d3 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonLaunchHandler.java @@ -8,6 +8,8 @@ package net.minecraftforge.fml.loading.targets; import com.mojang.logging.LogUtils; import cpw.mods.modlauncher.api.ILaunchHandlerService; import cpw.mods.modlauncher.api.ITransformingClassLoaderBuilder; +import cpw.mods.modlauncher.api.ServiceRunner; +import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.LogMarkers; import net.minecraftforge.api.distmarker.Dist; import org.apache.logging.log4j.core.LoggerContext; @@ -84,4 +86,28 @@ public abstract class CommonLaunchHandler implements ILaunchHandlerService { //modClassPaths.forEach((modlabel,paths) -> explodedTargets.add(new ExplodedDirectoryLocator.ExplodedMod(modlabel, paths))); return modClassPaths; } + + @Override + public ServiceRunner launchService(final String[] arguments, final ModuleLayer gameLayer) { + FMLLoader.beforeStart(gameLayer); + return makeService(arguments, gameLayer); + } + + protected abstract ServiceRunner makeService(final String[] arguments, final ModuleLayer gameLayer); + + protected void clientService(final String[] arguments, final ModuleLayer layer) throws Throwable { + runTarget("net.minecraft.client.main.Main", arguments, layer); + } + + protected void serverService(final String[] arguments, final ModuleLayer layer) throws Throwable { + runTarget("net.minecraft.server.Main", arguments, layer); + } + + protected void dataService(final String[] arguments, final ModuleLayer layer) throws Throwable { + runTarget("net.minecraft.data.Main", arguments, layer); + } + + protected void runTarget(final String target, final String[] arguments, final ModuleLayer layer) throws Throwable { + Class.forName(layer.findModule("minecraft").orElseThrow(),target).getMethod("main", String[].class).invoke(null, (Object)arguments); + } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonServerLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonServerLaunchHandler.java index c8f028709c..7201e71178 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonServerLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/CommonServerLaunchHandler.java @@ -23,12 +23,8 @@ public abstract class CommonServerLaunchHandler extends CommonLaunchHandler { @Override public boolean isProduction() { return true; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(),"net.minecraft.server.Main").getMethod("main", String[].class).invoke(null, (Object)args); - }; + protected ServiceRunner makeService(String[] arguments, ModuleLayer layer) { + return () -> serverService(arguments, layer); } @Override diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientDevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientDevLaunchHandler.java index a5c05b33ed..fa3c585e10 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientDevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientDevLaunchHandler.java @@ -5,20 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; public class FMLClientDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "fmlclientdev"; } @Override public Dist getDist() { return Dist.CLIENT; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.client.main.Main").getMethod("main", String[].class).invoke(null, (Object) args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + clientService(arguments, layer); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientUserdevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientUserdevLaunchHandler.java index abbd36facc..90c9a6da96 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientUserdevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLClientUserdevLaunchHandler.java @@ -5,11 +5,8 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class FMLClientUserdevLaunchHandler extends FMLUserdevLaunchHandler { @Override public String name() { return "fmlclientuserdev"; } @@ -18,11 +15,7 @@ public class FMLClientUserdevLaunchHandler extends FMLUserdevLaunchHandler { public Dist getDist() { return Dist.CLIENT; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.client.main.Main").getMethod("main", String[].class).invoke(null, (Object) args); - }; + protected void devService(String[] arguments, ModuleLayer layer) throws Throwable { + clientService(arguments, layer); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLDataUserdevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLDataUserdevLaunchHandler.java index 05dcc57329..9dc2d79aa6 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLDataUserdevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLDataUserdevLaunchHandler.java @@ -5,11 +5,8 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class FMLDataUserdevLaunchHandler extends FMLUserdevLaunchHandler { @Override public String name() { return "fmldatauserdev"; } @@ -21,11 +18,7 @@ public class FMLDataUserdevLaunchHandler extends FMLUserdevLaunchHandler { public boolean isData() { return true; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.data.Main").getMethod("main", String[].class).invoke(null, (Object) args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + dataService(arguments, layer); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerDevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerDevLaunchHandler.java index b431bd2c87..6f39657a2c 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerDevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerDevLaunchHandler.java @@ -5,20 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; public class FMLServerDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "fmlserverdev"; } @Override public Dist getDist() { return Dist.DEDICATED_SERVER; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(),"net.minecraft.server.Main").getMethod("main", String[].class).invoke(null, (Object)args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + serverService(arguments, layer); } } \ No newline at end of file diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerUserdevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerUserdevLaunchHandler.java index ca6fedaaea..b12acc19b0 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerUserdevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/FMLServerUserdevLaunchHandler.java @@ -5,21 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class FMLServerUserdevLaunchHandler extends FMLUserdevLaunchHandler { @Override public String name() { return "fmlserveruserdev"; } @Override public Dist getDist() { return Dist.DEDICATED_SERVER; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(),"net.minecraft.server.Main").getMethod("main", String[].class).invoke(null, (Object)args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + serverService(arguments, layer); } } \ No newline at end of file diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientDevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientDevLaunchHandler.java index 759f670ee2..7bae42b620 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientDevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientDevLaunchHandler.java @@ -5,20 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; public class ForgeClientDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "forgeclientdev"; } @Override public Dist getDist() { return Dist.CLIENT; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.client.main.Main").getMethod("main", String[].class).invoke(null, (Object) args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + clientService(arguments, layer); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientUserdevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientUserdevLaunchHandler.java index 7f31b8021c..08b8d90cc4 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientUserdevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeClientUserdevLaunchHandler.java @@ -5,11 +5,8 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class ForgeClientUserdevLaunchHandler extends ForgeUserdevLaunchHandler { @Override public String name() { return "forgeclientuserdev"; } @@ -18,11 +15,7 @@ public class ForgeClientUserdevLaunchHandler extends ForgeUserdevLaunchHandler { public Dist getDist() { return Dist.CLIENT; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.client.main.Main").getMethod("main", String[].class).invoke(null, (Object) args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + clientService(arguments, layer); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataDevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataDevLaunchHandler.java index ab187e1a9e..307e09f5a0 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataDevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataDevLaunchHandler.java @@ -5,22 +5,15 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class ForgeDataDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "forgedatadev"; } @Override public Dist getDist() { return Dist.CLIENT; } @Override public boolean isData() { return true; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.data.Main").getMethod("main", String[].class).invoke(null, (Object) args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.data.Main").getMethod("main", String[].class).invoke(null, (Object) arguments); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataUserdevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataUserdevLaunchHandler.java index f16b272029..a5995577f1 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataUserdevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeDataUserdevLaunchHandler.java @@ -5,11 +5,8 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class ForgeDataUserdevLaunchHandler extends ForgeUserdevLaunchHandler { @Override public String name() { return "forgedatauserdev"; } @@ -21,11 +18,7 @@ public class ForgeDataUserdevLaunchHandler extends ForgeUserdevLaunchHandler { public boolean isData() { return true; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(), "net.minecraft.data.Main").getMethod("main", String[].class).invoke(null, (Object) args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + dataService(arguments, layer); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestDevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestDevLaunchHandler.java index 8a60ecc364..2ce9e8ea93 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestDevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestDevLaunchHandler.java @@ -5,21 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class ForgeGametestDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "forgegametestserverdev"; } @Override public Dist getDist() { return Dist.DEDICATED_SERVER; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("forge").orElseThrow(), "net.minecraftforge.gametest.GameTestMain").getMethod("main", String[].class).invoke(null, (Object)args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + Class.forName(layer.findModule("forge").orElseThrow(), "net.minecraftforge.gametest.GameTestMain").getMethod("main", String[].class).invoke(null, (Object)arguments); } } \ No newline at end of file diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestUserdevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestUserdevLaunchHandler.java index 4b25709f40..3ada4f71da 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestUserdevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeGametestUserdevLaunchHandler.java @@ -5,21 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class ForgeGametestUserdevLaunchHandler extends ForgeUserdevLaunchHandler { @Override public String name() { return "forgegametestserveruserdev"; } @Override public Dist getDist() { return Dist.DEDICATED_SERVER; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("forge").orElseThrow(), "net.minecraftforge.gametest.GameTestMain").getMethod("main", String[].class).invoke(null, (Object)args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + Class.forName(layer.findModule("forge").orElseThrow(), "net.minecraftforge.gametest.GameTestMain").getMethod("main", String[].class).invoke(null, (Object)arguments); } } \ No newline at end of file diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerDevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerDevLaunchHandler.java index d74def558e..85f9fd9e84 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerDevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerDevLaunchHandler.java @@ -5,20 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; public class ForgeServerDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "forgeserverdev"; } @Override public Dist getDist() { return Dist.DEDICATED_SERVER; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(),"net.minecraft.server.Main").getMethod("main", String[].class).invoke(null, (Object)args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + serverService(arguments, layer); } } \ No newline at end of file diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerUserdevLaunchHandler.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerUserdevLaunchHandler.java index b412560a65..b33cc67f32 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerUserdevLaunchHandler.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/targets/ForgeServerUserdevLaunchHandler.java @@ -5,21 +5,14 @@ package net.minecraftforge.fml.loading.targets; -import cpw.mods.modlauncher.api.ServiceRunner; import net.minecraftforge.api.distmarker.Dist; -import java.util.concurrent.Callable; - public class ForgeServerUserdevLaunchHandler extends ForgeUserdevLaunchHandler { @Override public String name() { return "forgeserveruserdev"; } @Override public Dist getDist() { return Dist.DEDICATED_SERVER; } @Override - public ServiceRunner launchService(String[] arguments, ModuleLayer layer) { - return () -> { - var args = preLaunch(arguments, layer); - - Class.forName(layer.findModule("minecraft").orElseThrow(),"net.minecraft.server.Main").getMethod("main", String[].class).invoke(null, (Object)args); - }; + public void devService(String[] arguments, ModuleLayer layer) throws Throwable { + serverService(arguments, layer); } } \ No newline at end of file diff --git a/fmlloader/src/main/resources/META-INF/defaultfmlconfig.toml b/fmlloader/src/main/resources/META-INF/defaultfmlconfig.toml index 270cfd9909..3c54521b89 100644 --- a/fmlloader/src/main/resources/META-INF/defaultfmlconfig.toml +++ b/fmlloader/src/main/resources/META-INF/defaultfmlconfig.toml @@ -1,8 +1,21 @@ -# does the splashscreen run -splashscreen = true -# max threads for parallel loading : -1 uses Runtime#availableProcessors -maxThreads = -1 -# Enable forge global version checking +#Enable forge global version checking versionCheck = true -# Disables Optimized DFU client-side. +#Disables Optimized DFU client-side disableOptimizedDFU = true +#Default config path for servers +defaultConfigPath = "defaultconfigs" +#Max threads for early initialization parallelism, -1 is based on processor count +maxThreads = -1 +#should we show the early splashscreen +splashscreen = true +#Early window provider (default fmlearlywindow) +earlyWindowProvider = "fmlearlywindow" +#Initial window height +earlyWindowHeight = 480 +#Initial window width +earlyWindowWidth = 854 +# Start the early window as "maximized" +earlyWindowMaximized = false +# The rendering scale of the early window +earlyWindowFBScale = 1 + diff --git a/fmlonly/src/main/java/net/minecraftforge/fmlonlyclient/ClientModLoader.java b/fmlonly/src/main/java/net/minecraftforge/fmlonlyclient/ClientModLoader.java index 0cdc872f48..afbbe21032 100644 --- a/fmlonly/src/main/java/net/minecraftforge/fmlonlyclient/ClientModLoader.java +++ b/fmlonly/src/main/java/net/minecraftforge/fmlonlyclient/ClientModLoader.java @@ -27,48 +27,19 @@ import java.util.concurrent.TimeUnit; @OnlyIn(Dist.CLIENT) public class ClientModLoader { - private static final Logger LOGGER = LogManager.getLogger(); private static boolean loading; private static Minecraft mc; private static boolean loadingComplete; private static LoadingFailedException error; -// private static EarlyLoaderGUI earlyLoaderGUI; - private static class SpacedRunnable implements Runnable { - static final long NANO_SLEEP_TIME = TimeUnit.MILLISECONDS.toNanos(50); - private final Runnable wrapped; - private long lastRun; - - private SpacedRunnable(final Runnable wrapped) { - this.wrapped = wrapped; - this.lastRun = System.nanoTime() - NANO_SLEEP_TIME; - } - - @Override - public void run() { - if (System.nanoTime() - this.lastRun > NANO_SLEEP_TIME) { - wrapped.run(); - this.lastRun = System.nanoTime(); - } - } - } public static void begin(final Minecraft minecraft, final PackRepository defaultResourcePacks, final ReloadableResourceManager mcResourceManager) { // force log4j to shutdown logging in a shutdown hook. This is because we disable default shutdown hook so the server properly logs it's shutdown Runtime.getRuntime().addShutdownHook(new Thread(LogManager::shutdown)); loading = true; ClientModLoader.mc = minecraft; -// LogicalSidedProvider.setClient(()->minecraft); -// LanguageHook.loadForgeAndMCLangs(); -// earlyLoaderGUI = new EarlyLoaderGUI(minecraft.getWindow()); createRunnableWithCatch(()->ModLoader.get().gatherAndInitializeMods(ModWorkManager.syncExecutor(), ModWorkManager.parallelExecutor(), ()->{})).run(); mcResourceManager.registerReloadListener(ClientModLoader::onResourceReload); -// if (error == null) { -//// ResourcePackLoader.loadResourcePacks(defaultResourcePacks, ClientModLoader::buildPackFinder); -//// DatapackCodec.DEFAULT.addModPacks(ResourcePackLoader.getPackNames()); -//// mcResourceManager.registerReloadListener(BrandingControl.resourceManagerReloadListener()); -//// ModelLoaderRegistry.init(); -// } } private static CompletableFuture onResourceReload(final PreparableReloadListener.PreparationBarrier stage, final ResourceManager resourceManager, final ProfilerFiller prepareProfiler, final ProfilerFiller executeProfiler, final Executor asyncExecutor, final Executor syncExecutor) { @@ -90,15 +61,9 @@ public class ClientModLoader private static void startModLoading(ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor) { if (error!=null) throw error; -// earlyLoaderGUI.handleElsewhere(); createRunnableWithCatch(() -> ModLoader.get().loadMods(syncExecutor, parallelExecutor, ()->{})).run(); } - private static void postSidedRunnable() { -// LOGGER.debug(LOADING, "Running post client event work"); -// RenderingRegistry.loadEntityRenderers(mc.getEntityRenderDispatcher()); - } - private static void finishModLoading(ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor) { if (error!=null) throw error; @@ -109,80 +74,8 @@ public class ClientModLoader syncExecutor.execute(()->mc.options.load()); } - public static boolean completeModLoading() - { -// RenderSystem.disableTexture(); -// RenderSystem.enableTexture(); -// List warnings = ModLoader.get().getWarnings(); -// boolean showWarnings = true; -// try { -// showWarnings = ForgeConfig.CLIENT.showLoadWarnings.get(); -// } catch (NullPointerException e) { -// // We're in an early error state, config is not available. Assume true. -// } -// if (!showWarnings) { -// //User disabled warning screen, as least log them -// if (!warnings.isEmpty()) { -// LOGGER.warn(LOADING, "Mods loaded with {} warning(s)", warnings.size()); -// warnings.forEach(warning -> LOGGER.warn(LOADING, warning.formatToString())); -// } -// warnings = Collections.emptyList(); //Clear warnings, as the user does not want to see them -// } -// File dumpedLocation = null; -// if (error == null) { -// // We can finally start the forge eventbus up -// MinecraftForge.EVENT_BUS.start(); -// } else { -// // Double check we have the langs loaded for forge -// LanguageHook.loadForgeAndMCLangs(); -// dumpedLocation = CrashReportExtender.dumpModLoadingCrashReport(LOGGER, error, mc.gameDirectory); -// } -// if (error != null || !warnings.isEmpty()) { -// mc.setScreen(new LoadingErrorScreen(error, warnings, dumpedLocation)); -// return true; -// } else { -// ClientHooks.logMissingTextureErrors(); -// return false; -// } - return true; - } - - public static void renderProgressText() { -// earlyLoaderGUI.renderFromGUI(); - } public static boolean isLoading() { return loading; } - -// private static ResourcePackLoader.IPackInfoFinder buildPackFinder(Map modResourcePacks, BiConsumer packSetter) { -// return (packList, factory) -> clientPackFinder(modResourcePacks, packSetter, packList, factory); -// } -// -// private static void clientPackFinder(Map modResourcePacks, BiConsumer packSetter, Consumer consumer, ResourcePackInfo.IFactory factory) { -// List hiddenPacks = new ArrayList<>(); -// for (Entry e : modResourcePacks.entrySet()) -// { -// IModInfo mod = e.getKey().getModInfos().get(0); -// if (Objects.equals(mod.getModId(), "minecraft")) continue; // skip the minecraft "mod" -// final String name = "mod:" + mod.getModId(); -// final ResourcePackInfo packInfo = ResourcePackInfo.create(name, false, e::getValue, factory, ResourcePackInfo.Priority.BOTTOM, IPackNameDecorator.DEFAULT); -// if (packInfo == null) { -// // Vanilla only logs an error, instead of propagating, so handle null and warn that something went wrong -// ModLoader.get().addWarning(new ModLoadingWarning(mod, ModLoadingStage.ERROR, "fml.modloading.brokenresources", e.getKey())); -// continue; -// } -// packSetter.accept(e.getValue(), packInfo); -// LOGGER.debug(CORE, "Generating PackInfo named {} for mod file {}", name, e.getKey().getFilePath()); -// if (mod.getOwningFile().showAsResourcePack()) { -// consumer.accept(packInfo); -// } else { -// hiddenPacks.add(e.getValue()); -// } -// } -// final ResourcePackInfo packInfo = ResourcePackInfo.create("mod_resources", true, () -> new DelegatingResourcePack("mod_resources", "Mod Resources", -// new PackMetadataSection(new TranslationTextComponent("fml.resources.modresources", hiddenPacks.size()), 6), -// hiddenPacks), factory, ResourcePackInfo.Priority.BOTTOM, IPackNameDecorator.DEFAULT); -// consumer.accept(packInfo); -// } } diff --git a/patches/minecraft/com/mojang/blaze3d/platform/Window.java.patch b/patches/minecraft/com/mojang/blaze3d/platform/Window.java.patch index 16b6094da3..e269d84127 100644 --- a/patches/minecraft/com/mojang/blaze3d/platform/Window.java.patch +++ b/patches/minecraft/com/mojang/blaze3d/platform/Window.java.patch @@ -1,19 +1,28 @@ --- a/com/mojang/blaze3d/platform/Window.java +++ b/com/mojang/blaze3d/platform/Window.java -@@ -83,7 +_,7 @@ +@@ -83,7 +_,8 @@ GLFW.glfwWindowHint(139267, 2); GLFW.glfwWindowHint(139272, 204801); GLFW.glfwWindowHint(139270, 1); - this.f_85349_ = GLFW.glfwCreateWindow(this.f_85359_, this.f_85360_, p_85376_, this.f_85355_ && monitor != null ? monitor.m_84954_() : 0L, 0L); -+ this.f_85349_ = net.minecraftforge.fml.loading.progress.EarlyProgressVisualization.INSTANCE.handOffWindow(()->this.f_85359_, ()->this.f_85360_, ()->p_85376_, ()->this.f_85355_ && monitor != null ? monitor.m_84954_() : 0L); ++ this.f_85349_ = net.minecraftforge.fml.loading.ImmediateWindowHandler.setupMinecraftWindow(()->this.f_85359_, ()->this.f_85360_, ()->p_85376_, ()->this.f_85355_ && monitor != null ? monitor.m_84954_() : 0L); ++ if (!net.minecraftforge.fml.loading.ImmediateWindowHandler.positionWindow(Optional.ofNullable(monitor), w->this.f_85359_ = this.f_85352_ = w, h->this.f_85360_ = this.f_85353_ = h, x->this.f_85357_ = this.f_85350_ = x, y->this.f_85358_ = this.f_85351_ = y)) { if (monitor != null) { VideoMode videomode = monitor.m_84948_(this.f_85355_ ? this.f_85354_ : Optional.empty()); this.f_85350_ = this.f_85357_ = monitor.m_84951_() + videomode.m_85332_() / 2 - this.f_85359_ / 2; +@@ -95,6 +_,7 @@ + this.f_85350_ = this.f_85357_ = aint1[0]; + this.f_85351_ = this.f_85358_ = aint[0]; + } ++ } + + GLFW.glfwMakeContextCurrent(this.f_85349_); + Locale locale = Locale.getDefault(Category.FORMAT); @@ -238,6 +_,7 @@ GLFW.glfwGetFramebufferSize(this.f_85349_, aint, aint1); this.f_85361_ = aint[0] > 0 ? aint[0] : 1; this.f_85362_ = aint1[0] > 0 ? aint1[0] : 1; -+ if (this.f_85362_ == 0 || this.f_85361_==0) net.minecraftforge.fml.loading.progress.EarlyProgressVisualization.INSTANCE.updateFBSize(w->this.f_85361_=w, h->this.f_85362_=h); ++ if (this.f_85362_ == 0 || this.f_85361_==0) net.minecraftforge.fml.loading.ImmediateWindowHandler.updateFBSize(w->this.f_85361_=w, h->this.f_85362_=h); } private void m_85427_(long p_85428_, int p_85429_, int p_85430_) { diff --git a/patches/minecraft/net/minecraft/client/Minecraft.java.patch b/patches/minecraft/net/minecraft/client/Minecraft.java.patch index b0a136b1c3..4c1fddc5b7 100644 --- a/patches/minecraft/net/minecraft/client/Minecraft.java.patch +++ b/patches/minecraft/net/minecraft/client/Minecraft.java.patch @@ -17,16 +17,17 @@ this.f_91033_ = p_91084_.f_101908_.f_101926_; this.f_91034_ = !p_91084_.f_101908_.f_101929_; this.f_91035_ = !p_91084_.f_101908_.f_101930_; -@@ -450,7 +_,7 @@ +@@ -449,15 +_,15 @@ + } this.f_90990_.m_85380_(this.f_91066_.m_232035_().m_231551_()); ++ // FORGE: Move mouse and keyboard handler setup further below this.f_91067_ = new MouseHandler(this); - this.f_91067_.m_91524_(this.f_90990_.m_85439_()); -+ // FORGE: Move mouse handler setup further below this.f_91068_ = new KeyboardHandler(this); - this.f_91068_.m_90887_(this.f_90990_.m_85439_()); +- this.f_91068_.m_90887_(this.f_90990_.m_85439_()); RenderSystem.initRenderer(this.f_91066_.f_92035_, false); -@@ -458,6 +_,7 @@ + this.f_91042_ = new MainTarget(this.f_90990_.m_85441_(), this.f_90990_.m_85442_()); this.f_91042_.m_83931_(0.0F, 0.0F, 0.0F, 0.0F); this.f_91042_.m_83954_(f_91002_); this.f_91036_ = new ReloadableResourceManager(PackType.CLIENT_RESOURCES); @@ -49,14 +50,15 @@ this.f_91036_.m_7217_(this.f_91061_); this.f_91053_ = new PaintingTextureManager(this.f_90987_); this.f_91036_.m_7217_(this.f_91053_); -@@ -516,7 +_,9 @@ +@@ -516,7 +_,10 @@ this.f_91047_ = new GpuWarnlistManager(); this.f_91036_.m_7217_(this.f_91047_); this.f_91036_.m_7217_(this.f_205120_); - this.f_91065_ = new Gui(this, this.f_90995_); + this.f_91065_ = new net.minecraftforge.client.gui.overlay.ForgeGui(this); -+ // FORGE: Moved mouse handler setup below ingame gui creation to prevent NPEs in mouse handler. ++ // FORGE: Moved keyboard and mouse handler setup below ingame gui creation to prevent NPEs in them. + this.f_91067_.m_91524_(this.f_90990_.m_85439_()); ++ this.f_91068_.m_90887_(this.f_90990_.m_85439_()); this.f_91064_ = new DebugRenderer(this); RealmsClient realmsclient = RealmsClient.m_239151_(this); this.f_238717_ = new RealmsDataFetcher(realmsclient); @@ -68,6 +70,15 @@ this.f_90990_.m_85409_(this.f_91066_.m_231817_().m_231551_()); this.f_90990_.m_85424_(this.f_91066_.m_232123_().m_231551_()); this.f_90990_.m_85426_(); +@@ -554,7 +_,7 @@ + this.f_167847_.m_168557_(ResourceLoadStateTracker.ReloadReason.INITIAL, list); + ReloadInstance reloadinstance = this.f_91036_.m_142463_(Util.m_183991_(), this, f_90983_, list); + GameLoadTimesEvent.f_285635_.m_285833_(TelemetryProperty.f_285629_); +- this.m_91150_(new LoadingOverlay(this, reloadinstance, (p_210745_) -> { ++ this.m_91150_(net.minecraftforge.fml.loading.ImmediateWindowHandler.loadingOverlay(()->this, ()->reloadinstance, (p_210745_) -> { + Util.m_137521_(p_210745_, this::m_91239_, () -> { + if (SharedConstants.f_136183_) { + this.m_91273_(); @@ -562,20 +_,23 @@ this.f_167847_.m_168556_(); @@ -87,7 +98,8 @@ + this.m_278684_(realmsclient, reloadinstance, p_91084_.f_278410_); + } }); - }, false)); +- }, false)); ++ }, false).get()); this.f_278504_ = QuickPlayLog.m_278648_(p_91084_.f_278410_.f_278493_()); - if (this.m_239929_()) { - this.m_91152_(BanNoticeScreen.m_239967_((p_278873_) -> { diff --git a/patches/minecraft/net/minecraft/client/gui/screens/LoadingOverlay.java.patch b/patches/minecraft/net/minecraft/client/gui/screens/LoadingOverlay.java.patch index b147df86a1..e3c7a5c084 100644 --- a/patches/minecraft/net/minecraft/client/gui/screens/LoadingOverlay.java.patch +++ b/patches/minecraft/net/minecraft/client/gui/screens/LoadingOverlay.java.patch @@ -1,13 +1,5 @@ --- a/net/minecraft/client/gui/screens/LoadingOverlay.java +++ b/net/minecraft/client/gui/screens/LoadingOverlay.java -@@ -123,6 +_,7 @@ - int k1 = (int)((double)p_281839_.m_280206_() * 0.8325D); - float f6 = this.f_96164_.m_7750_(); - this.f_96167_ = Mth.m_14036_(this.f_96167_ * 0.95F + f6 * 0.050000012F, 0.0F, 1.0F); -+ net.minecraftforge.client.loading.ClientModLoader.renderProgressText(); - if (f < 1.0F) { - this.m_96182_(p_281839_, i / 2 - j1, k1 - 5, i / 2 + j1, k1 + 5, 1.0F - Mth.m_14036_(f, 0.0F, 1.0F)); - } @@ -132,6 +_,7 @@ } diff --git a/settings.gradle b/settings.gradle index 89ce6f59b3..9061595066 100644 --- a/settings.gradle +++ b/settings.gradle @@ -27,6 +27,7 @@ include 'fmlcore' include 'mclanguage' include 'javafmllanguage' include 'lowcodelanguage' +include 'fmlearlydisplay' include ':mcp' include ':clean' diff --git a/src/main/java/net/minecraftforge/client/gui/TitleScreenModUpdateIndicator.java b/src/main/java/net/minecraftforge/client/gui/TitleScreenModUpdateIndicator.java index d9678e24b9..079e33870c 100644 --- a/src/main/java/net/minecraftforge/client/gui/TitleScreenModUpdateIndicator.java +++ b/src/main/java/net/minecraftforge/client/gui/TitleScreenModUpdateIndicator.java @@ -50,7 +50,7 @@ public class TitleScreenModUpdateIndicator extends Screen @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - if (showNotification == null || !showNotification.shouldDraw() || !FMLConfig.runVersionCheck()) + if (showNotification == null || !showNotification.shouldDraw() || !FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.VERSION_CHECK)) { return; } diff --git a/src/main/java/net/minecraftforge/client/loading/ClientModLoader.java b/src/main/java/net/minecraftforge/client/loading/ClientModLoader.java index e92b9d0f66..cbc322cece 100644 --- a/src/main/java/net/minecraftforge/client/loading/ClientModLoader.java +++ b/src/main/java/net/minecraftforge/client/loading/ClientModLoader.java @@ -25,6 +25,7 @@ import net.minecraft.server.packs.repository.PackRepository; import net.minecraft.world.level.DataPackConfig; import net.minecraftforge.event.AddPackFindersEvent; import net.minecraftforge.fml.*; +import net.minecraftforge.fml.loading.ImmediateWindowHandler; import net.minecraftforge.internal.BrandingControl; import net.minecraftforge.logging.CrashReportExtender; import net.minecraftforge.common.util.LogicalSidedProvider; @@ -59,7 +60,6 @@ public class ClientModLoader private static Minecraft mc; private static boolean loadingComplete; private static LoadingFailedException error; - private static EarlyLoaderGUI earlyLoaderGUI; private static class SpacedRunnable implements Runnable { static final long NANO_SLEEP_TIME = TimeUnit.MILLISECONDS.toNanos(50); @@ -87,8 +87,7 @@ public class ClientModLoader ClientModLoader.mc = minecraft; LogicalSidedProvider.setClient(()->minecraft); LanguageHook.loadForgeAndMCLangs(); - earlyLoaderGUI = new EarlyLoaderGUI(minecraft); - createRunnableWithCatch(()->ModLoader.get().gatherAndInitializeMods(ModWorkManager.syncExecutor(), ModWorkManager.parallelExecutor(), new SpacedRunnable(earlyLoaderGUI::renderTick))).run(); + createRunnableWithCatch(()->ModLoader.get().gatherAndInitializeMods(ModWorkManager.syncExecutor(), ModWorkManager.parallelExecutor(), new SpacedRunnable(ImmediateWindowHandler::renderTick))).run(); if (error == null) { ResourcePackLoader.loadResourcePacks(defaultResourcePacks, ClientModLoader::buildPackFinder); ModLoader.get().postEvent(new AddPackFindersEvent(PackType.CLIENT_RESOURCES, defaultResourcePacks::addPackFinder)); @@ -116,13 +115,12 @@ public class ClientModLoader } private static void startModLoading(ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor) { - earlyLoaderGUI.handleElsewhere(); - createRunnableWithCatch(() -> ModLoader.get().loadMods(syncExecutor, parallelExecutor, new SpacedRunnable(earlyLoaderGUI::renderTick))).run(); + createRunnableWithCatch(() -> ModLoader.get().loadMods(syncExecutor, parallelExecutor, new SpacedRunnable(ImmediateWindowHandler::renderTick))).run(); } private static void finishModLoading(ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor) { - createRunnableWithCatch(() -> ModLoader.get().finishMods(syncExecutor, parallelExecutor, new SpacedRunnable(earlyLoaderGUI::renderTick))).run(); + createRunnableWithCatch(() -> ModLoader.get().finishMods(syncExecutor, parallelExecutor, new SpacedRunnable(ImmediateWindowHandler::renderTick))).run(); loading = false; loadingComplete = true; // reload game settings on main thread @@ -172,9 +170,6 @@ public class ClientModLoader } } - public static void renderProgressText() { - earlyLoaderGUI.renderFromGUI(); - } public static boolean isLoading() { return loading; diff --git a/src/main/java/net/minecraftforge/client/loading/EarlyLoaderGUI.java b/src/main/java/net/minecraftforge/client/loading/EarlyLoaderGUI.java deleted file mode 100644 index b63ca31f3d..0000000000 --- a/src/main/java/net/minecraftforge/client/loading/EarlyLoaderGUI.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.minecraftforge.client.loading; - -import com.mojang.blaze3d.platform.GlStateManager; -import com.mojang.blaze3d.systems.RenderSystem; - -import com.mojang.blaze3d.platform.Window; -import net.minecraft.client.Minecraft; -import net.minecraft.util.Mth; -import net.minecraftforge.fml.StartupMessageManager; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL14; -import org.lwjgl.stb.STBEasyFont; -import org.lwjgl.system.MemoryUtil; - -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryUsage; -import java.nio.ByteBuffer; -import java.util.Locale; - -public class EarlyLoaderGUI { - private final Minecraft minecraft; - private final Window window; - private boolean handledElsewhere; - - public EarlyLoaderGUI(final Minecraft minecraft) { - this.minecraft = minecraft; - this.window = minecraft.getWindow(); - } - - private void setupMatrix() { - RenderSystem.clear(256, Minecraft.ON_OSX); - GL11.glMatrixMode(5889); - GL11.glLoadIdentity(); - GL11.glOrtho(0.0D, window.getWidth() / window.getGuiScale(), window.getHeight() / window.getGuiScale(), 0.0D, 1000.0D, 3000.0D); - GL11.glMatrixMode(5888); - GL11.glLoadIdentity(); - GL11.glTranslatef(0.0F, 0.0F, -2000.0F); - } - - public void handleElsewhere() { - this.handledElsewhere = true; - } - - void renderFromGUI() { - renderMessages(); - } - - void renderTick() { - if (handledElsewhere) return; - // int guiScale = window.calculateScale(0, false); - // window.setGuiScale(guiScale); - // - // RenderSystem.clearColor(1.0f, 1.0f, 1.0f, 1.0f); - // RenderSystem.clear(GL11.GL_COLOR_BUFFER_BIT, Minecraft.ON_OSX); - // GL11.glPushMatrix(); - // setupMatrix(); - // renderBackground(); - // renderMessages(); - // window.updateDisplay(); - // GL11.glPopMatrix(); - } - - private void renderBackground() { - GL11.glBegin(GL11.GL_QUADS); - boolean isDarkBackground = minecraft.options.darkMojangStudiosBackground().get(); - GL11.glColor4f(isDarkBackground ? 0 : (239F / 255F), isDarkBackground ? 0 : (50F / 255F), isDarkBackground ? 0 : (61F / 255F), 1); //Color from LoadingOverlay - GL11.glVertex3f(0, 0, -10); - GL11.glVertex3f(0, window.getGuiScaledHeight(), -10); - GL11.glVertex3f(window.getGuiScaledWidth(), window.getGuiScaledHeight(), -10); - GL11.glVertex3f(window.getGuiScaledWidth(), 0, -10); - GL11.glEnd(); - } - - private void renderMessages() { -// List> messages = StartupMessageManager.getMessages(); -// for (int i = 0; i < messages.size(); i++) { -// boolean nofade = i == 0; -// final Pair pair = messages.get(i); -// final float fade = MathHelper.clamp((4000.0f - (float) pair.getLeft() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f); -// if (fade <0.01f && !nofade) continue; -// StartupMessageManager.Message msg = pair.getRight(); -// renderMessage(msg.getText(), msg.getTypeColour(), ((window.getGuiScaledHeight() - 15) / 10) - i + 1, nofade ? 1.0f : fade); -// } -// List> messages = StartupMessageManager.getMessages(); -// for (int i = 0; i < messages.size(); i++) { -// boolean nofade = i == 0; -// final Pair pair = messages.get(i); -// final float fade = Mth.clamp((4000.0f - (float) pair.getLeft() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f); -// if (fade <0.01f && !nofade) continue; -// StartupMessageManager.Message msg = pair.getRight(); -// renderMessage(msg.getText(), msg.getTypeColour(), ((window.getGuiScaledHeight() - 15) / 10) - i + 1, nofade ? 1.0f : fade); -// } -// renderMemoryInfo(); - } - - private static final float[] memorycolour = new float[] { 0.0f, 0.0f, 0.0f}; - - private void renderMemoryInfo() { - final MemoryUsage heapusage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); - final MemoryUsage offheapusage = ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage(); - final float pctmemory = (float) heapusage.getUsed() / heapusage.getMax(); - String memory = String.format(Locale.ENGLISH, "Memory Heap: %d / %d MB (%.1f%%) OffHeap: %d MB", heapusage.getUsed() >> 20, heapusage.getMax() >> 20, pctmemory * 100.0, offheapusage.getUsed() >> 20); - - final int i = Mth.hsvToRgb((1.0f - (float)Math.pow(pctmemory, 1.5f)) / 3f, 1.0f, 0.5f); - memorycolour[2] = ((i) & 0xFF) / 255.0f; - memorycolour[1] = ((i >> 8 ) & 0xFF) / 255.0f; - memorycolour[0] = ((i >> 16 ) & 0xFF) / 255.0f; - renderMessage(memory, memorycolour, 1, 1.0f); - } - - void renderMessage(final String message, final float[] colour, int line, float alpha) { - // GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY); - // ByteBuffer charBuffer = MemoryUtil.memAlloc(message.length() * 270); - // int quads = STBEasyFont.stb_easy_font_print(0, 0, message, null, charBuffer); - // GL14.glVertexPointer(2, GL11.GL_FLOAT, 16, charBuffer); - // - // RenderSystem.enableBlend(); - // RenderSystem.disableTexture(); - // // STBEasyFont's quads are in reverse order or what OGGL expects, so it gets culled for facing the wrong way. - // // So Disable culling https://github.com/MinecraftForge/MinecraftForge/pull/6824 - // RenderSystem.disableCull(); - // GL14.glBlendColor(0,0,0, alpha); - // RenderSystem.blendFunc(GlStateManager.SourceFactor.CONSTANT_ALPHA, GlStateManager.DestFactor.ONE_MINUS_CONSTANT_ALPHA); - // GL11.glColor3f(colour[0],colour[1],colour[2]); - // GL11.glPushMatrix(); - // GL11.glTranslatef(10, line * 10, 0); - // GL11.glScalef(1, 1, 0); - // GL11.glDrawArrays(GL11.GL_QUADS, 0, quads * 4); - // GL11.glPopMatrix(); - // - // RenderSystem.enableCull(); - // GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY); - // MemoryUtil.memFree(charBuffer); - } -} diff --git a/src/main/java/net/minecraftforge/client/loading/ForgeLoadingOverlay.java b/src/main/java/net/minecraftforge/client/loading/ForgeLoadingOverlay.java new file mode 100644 index 0000000000..797dabafe2 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/loading/ForgeLoadingOverlay.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.client.loading; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.BufferVertexConsumer; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.blaze3d.vertex.VertexSorting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.LoadingOverlay; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ReloadInstance; +import net.minecraftforge.fml.StartupMessageManager; +import net.minecraftforge.fml.earlydisplay.ColourScheme; +import net.minecraftforge.fml.earlydisplay.DisplayWindow; +import net.minecraftforge.fml.loading.progress.ProgressMeter; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static com.mojang.blaze3d.platform.GlConst.*; +import static net.minecraft.util.Mth.clamp; +import static org.lwjgl.opengl.GL30C.glViewport; +import static org.lwjgl.opengl.GL30C.glTexParameterIi; + +/** + * This is an implementation of the LoadingOverlay that calls back into the early window rendering, as part of the + * game loading cycle. We completely replace the {@link #render(GuiGraphics, int, int, float)} call from the parent + * with one of our own, that allows us to blend our early loading screen into the main window, in the same manner as + * the Mojang screen. It also allows us to see and tick appropriately as the later stages of the loading system run. + * + * It is somewhat a copy of the superclass render method. + */ +public class ForgeLoadingOverlay extends LoadingOverlay { + private final Minecraft minecraft; + private final ReloadInstance reload; + private final Consumer> onFinish; + private final DisplayWindow displayWindow; + private final ProgressMeter progress; + private long fadeOutStart = -1L; + + public ForgeLoadingOverlay(final Minecraft mc, final ReloadInstance reloader, final Consumer> errorConsumer, DisplayWindow displayWindow) { + super(mc, reloader, errorConsumer, false); + this.minecraft = mc; + this.reload = reloader; + this.onFinish = errorConsumer; + this.displayWindow = displayWindow; + displayWindow.addMojangTexture(mc.getTextureManager().getTexture(new ResourceLocation("textures/gui/title/mojangstudios.png")).getId()); + this.progress = StartupMessageManager.addProgressBar("Minecraft Progress", 100); + } + + public static Supplier newInstance(Supplier mc, Supplier ri, Consumer> handler, DisplayWindow window) { + return ()->new ForgeLoadingOverlay(mc.get(), ri.get(), handler, window); + } + + @Override + public void render(final @NotNull GuiGraphics graphics, final int mouseX, final int mouseY, final float partialTick) { + long millis = Util.getMillis(); + float fadeouttimer = this.fadeOutStart > -1L ? (float)(millis - this.fadeOutStart) / 1000.0F : -1.0F; + progress.setAbsolute(clamp((int)(this.reload.getActualProgress() * 100f), 0, 100)); + var fade = 1.0F - clamp(fadeouttimer - 1.0F, 0.0F, 1.0F); + var colour = this.displayWindow.context().colourScheme().background(); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, fade); + if (fadeouttimer >= 1.0F) { + if (this.minecraft.screen != null) { + this.minecraft.screen.render(graphics, 0, 0, partialTick); + } + displayWindow.render(0xff); + } else { + GlStateManager._clearColor(colour.redf(), colour.greenf(), colour.bluef(), 1f); + GlStateManager._clear(GL_COLOR_BUFFER_BIT, Minecraft.ON_OSX); + displayWindow.render(0xFF); + } + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + var fbWidth = this.minecraft.getWindow().getWidth(); + var fbHeight = this.minecraft.getWindow().getHeight(); + glViewport(0, 0, fbWidth, fbHeight); + final var twidth = this.displayWindow.context().width(); + final var theight = this.displayWindow.context().height(); + var wscale = (float)fbWidth / twidth; + var hscale = (float)fbHeight / theight; + var scale = this.displayWindow.context().scale() * Math.min(wscale, hscale) / 2f; + var wleft = clamp(fbWidth * 0.5f - scale * twidth, 0, fbWidth); + var wtop = clamp(fbHeight * 0.5f - scale * theight, 0, fbHeight); + var wright = clamp(fbWidth * 0.5f + scale * twidth, 0, fbWidth); + var wbottom = clamp(fbHeight * 0.5f + scale * theight, 0, fbHeight); + GlStateManager.glActiveTexture(GL_TEXTURE0); + RenderSystem.disableCull(); + BufferBuilder bufferbuilder = Tesselator.getInstance().getBuilder(); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, fade); + RenderSystem.getModelViewMatrix().identity(); + RenderSystem.setProjectionMatrix(new Matrix4f().setOrtho(0.0F, fbWidth, 0.0F, fbHeight, 0.1f, -0.1f), VertexSorting.ORTHOGRAPHIC_Z); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + // This is fill in around the edges - it's empty solid colour + bufferbuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + // top box from hpos + addQuad(bufferbuilder, 0, fbWidth, wtop, fbHeight, colour, fade); + // bottom box to hpos + addQuad(bufferbuilder, 0, fbWidth, 0, wtop, colour, fade); + // left box to wpos + addQuad(bufferbuilder, 0, wleft, wtop, wbottom, colour, fade); + // right box from wpos + addQuad(bufferbuilder, wright, fbWidth, wtop, wbottom, colour, fade); + BufferUploader.drawWithShader(bufferbuilder.end()); + + // This is the actual screen data from the loading screen + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + RenderSystem.setShader(GameRenderer::getPositionTexColorShader); + RenderSystem.setShaderTexture(0, displayWindow.getFramebufferTextureId()); + bufferbuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + bufferbuilder.vertex(wleft, wbottom, 0f).uv(0, 0).color(1f, 1f, 1f, fade).endVertex(); + bufferbuilder.vertex(wright, wbottom, 0f).uv(1, 0).color(1f, 1f, 1f, fade).endVertex(); + bufferbuilder.vertex(wright, wtop, 0f).uv(1, 1).color(1f, 1f, 1f, fade).endVertex(); + bufferbuilder.vertex(wleft, wtop, 0f).uv(0, 1).color(1f, 1f, 1f, fade).endVertex(); + glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + BufferUploader.drawWithShader(bufferbuilder.end()); + RenderSystem.defaultBlendFunc(); + RenderSystem.disableBlend(); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1f); + + if (fadeouttimer >= 2.0F) { + this.minecraft.setOverlay(null); + this.displayWindow.close(); + } + + if (this.fadeOutStart == -1L && this.reload.isDone()) { + progress.complete(); + this.fadeOutStart = Util.getMillis(); + try { + this.reload.checkExceptions(); + this.onFinish.accept(Optional.empty()); + } catch (Throwable throwable) { + this.onFinish.accept(Optional.of(throwable)); + } + + if (this.minecraft.screen != null) { + this.minecraft.screen.init(this.minecraft, this.minecraft.getWindow().getGuiScaledWidth(), this.minecraft.getWindow().getGuiScaledHeight()); + } + } + } + + private static void addQuad(BufferVertexConsumer bufferbuilder, float x0, float x1, float y0, float y1, ColourScheme.Colour colour, float fade) { + bufferbuilder.vertex(x0, y0, 0f).color(colour.redf(), colour.greenf(), colour.bluef(), fade).endVertex(); + bufferbuilder.vertex(x0, y1, 0f).color(colour.redf(), colour.greenf(), colour.bluef(), fade).endVertex(); + bufferbuilder.vertex(x1, y1, 0f).color(colour.redf(), colour.greenf(), colour.bluef(), fade).endVertex(); + bufferbuilder.vertex(x1, y0, 0f).color(colour.redf(), colour.greenf(), colour.bluef(), fade).endVertex(); + } +} diff --git a/src/main/java/net/minecraftforge/client/loading/NoVizFallback.java b/src/main/java/net/minecraftforge/client/loading/NoVizFallback.java index d795c3dcbb..51eca1f5f3 100644 --- a/src/main/java/net/minecraftforge/client/loading/NoVizFallback.java +++ b/src/main/java/net/minecraftforge/client/loading/NoVizFallback.java @@ -5,12 +5,25 @@ package net.minecraftforge.client.loading; -import java.util.function.IntSupplier; -import java.util.function.LongSupplier; -import java.util.function.Supplier; +import com.mojang.blaze3d.platform.Monitor; +import com.mojang.blaze3d.platform.VideoMode; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.LoadingOverlay; +import net.minecraft.server.packs.resources.ReloadInstance; + +import java.util.Optional; +import java.util.function.*; public final class NoVizFallback { - public static LongSupplier fallback(IntSupplier width, IntSupplier height, Supplier title, LongSupplier monitor) { + public static LongSupplier windowHandoff(IntSupplier width, IntSupplier height, Supplier title, LongSupplier monitor) { return ()->org.lwjgl.glfw.GLFW.glfwCreateWindow(width.getAsInt(), height.getAsInt(), title.get(), monitor.getAsLong(), 0L); } + + public static Supplier loadingOverlay(Supplier mc, Supplier ri, Consumer> ex, boolean fadein) { + return () -> new LoadingOverlay(mc.get(), ri.get(), ex, fadein); + } + + public static Boolean windowPositioning(Optional monitor, IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter) { + return Boolean.FALSE; + } } diff --git a/src/main/java/net/minecraftforge/registries/GameData.java b/src/main/java/net/minecraftforge/registries/GameData.java index 6a737ea7cd..263dbed618 100644 --- a/src/main/java/net/minecraftforge/registries/GameData.java +++ b/src/main/java/net/minecraftforge/registries/GameData.java @@ -360,7 +360,6 @@ public class GameData ordered.addAll(keySet.stream().sorted(ResourceLocation::compareNamespaced).toList()); RuntimeException aggregate = new RuntimeException(); - for (ResourceLocation rootRegistryName : ordered) { try @@ -386,7 +385,6 @@ public class GameData aggregate.addSuppressed(t); } } - if (aggregate.getSuppressed().length > 0) { LOGGER.fatal("Failed to register some entries, see suppressed exceptions for details", aggregate); @@ -771,9 +769,7 @@ public class GameData } private static void fireRemapEvent(final Map> remaps, final boolean isFreezing) { - StartupMessageManager.modLoaderConsumer().ifPresent(s->s.accept("Remapping mod data")); MinecraftForge.EVENT_BUS.post(new IdMappingEvent(remaps, isFreezing)); - StartupMessageManager.modLoaderConsumer().ifPresent(s->s.accept("Remap complete")); } //Has to be split because of generics, Yay!