From 19cbb8009d960c1bf8736f9978a307bacdb3aff5 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 23 Mar 2025 11:19:30 +0100 Subject: [PATCH 01/22] Early display theming --- .../fml/earlydisplay/ColourScheme.java | 45 --- .../fml/earlydisplay/DisplayWindow.java | 338 +++++++---------- .../fml/earlydisplay/EarlyFramebuffer.java | 69 ---- .../fml/earlydisplay/ElementShader.java | 120 ------ .../fml/earlydisplay/RenderContext.java | 3 + .../fml/earlydisplay/RenderElement.java | 344 ------------------ .../neoforged/fml/earlydisplay/STBHelper.java | 69 ---- .../fml/earlydisplay/SimpleFont.java | 180 --------- .../earlydisplay/render/EarlyFramebuffer.java | 89 +++++ .../earlydisplay/render/ElementRenderer.java | 25 ++ .../earlydisplay/render/ElementShader.java | 175 +++++++++ .../earlydisplay/{ => render}/GlDebug.java | 6 +- .../earlydisplay/{ => render}/GlState.java | 7 +- .../render/LoadingScreenRenderer.java | 291 +++++++++++++++ .../earlydisplay/{ => render}/QuadHelper.java | 2 +- .../earlydisplay/render/RenderContext.java | 121 ++++++ .../{ => render}/SimpleBufferBuilder.java | 2 +- .../fml/earlydisplay/render/SimpleFont.java | 237 ++++++++++++ .../fml/earlydisplay/render/Texture.java | 50 +++ .../render/elements/ImageElement.java | 31 ++ .../render/elements/LabelElement.java | 38 ++ .../render/elements/ProgressBarsElement.java | 49 +++ .../render/elements/RenderElement.java | 292 +++++++++++++++ .../render/elements/StartupLogElement.java | 52 +++ .../earlydisplay/theme/AnimationMetadata.java | 8 + .../earlydisplay/theme/ClasspathResource.java | 38 ++ .../fml/earlydisplay/theme/FileResource.java | 26 ++ .../fml/earlydisplay/theme/ImageLoader.java | 61 ++++ .../fml/earlydisplay/theme/NativeBuffer.java | 27 ++ .../fml/earlydisplay/theme/Theme.java | 91 +++++ .../fml/earlydisplay/theme/ThemeColor.java | 22 ++ .../earlydisplay/theme/ThemeColorScheme.java | 35 ++ .../fml/earlydisplay/theme/ThemeResource.java | 20 + .../fml/earlydisplay/theme/ThemeShader.java | 15 + .../fml/earlydisplay/theme/ThemeTexture.java | 14 + .../earlydisplay/theme/UncompressedImage.java | 23 ++ .../theme/UnresolvedThemeColor.java | 13 + .../theme/elements/ThemeElement.java | 57 +++ .../theme/elements/ThemeImageElement.java | 16 + .../theme/elements/ThemeLabelElement.java | 14 + .../elements/ThemeProgressBarsElement.java | 7 + .../elements/ThemeStartupLogElement.java | 7 + .../fml/earlydisplay/util/Bounds.java | 23 ++ .../neoforged/fml/earlydisplay/util/Size.java | 3 + .../fml/earlydisplay/util/StyleLength.java | 52 +++ .../net/neoforged/fml/earlydisplay/gui.frag | 9 + .../net/neoforged/fml/earlydisplay/gui.vert | 14 + .../neoforged/fml/earlydisplay/gui_color.frag | 8 + .../neoforged/fml/earlydisplay/gui_font.frag | 9 + .../net/neoforged/fml/loading/FMLConfig.java | 1 - 50 files changed, 2206 insertions(+), 1042 deletions(-) delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ColourScheme.java delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/EarlyFramebuffer.java delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ElementShader.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderElement.java delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/STBHelper.java delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/SimpleFont.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java rename earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/{ => render}/GlDebug.java (97%) rename earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/{ => render}/GlState.java (98%) create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java rename earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/{ => render}/QuadHelper.java (94%) create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java rename earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/{ => render}/SimpleBufferBuilder.java (99%) create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.frag create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.vert create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_color.frag create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_font.frag diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ColourScheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ColourScheme.java deleted file mode 100644 index cd7908a88..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ColourScheme.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.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/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 910691f5e..1e15ffc9b 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -5,33 +5,72 @@ package net.neoforged.fml.earlydisplay; -import static org.lwjgl.glfw.GLFW.*; -import static org.lwjgl.opengl.GL.createCapabilities; -import static org.lwjgl.opengl.GL32C.*; +import static org.lwjgl.glfw.GLFW.GLFW_CLIENT_API; +import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_CREATION_API; +import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MAJOR; +import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MINOR; +import static org.lwjgl.glfw.GLFW.GLFW_FALSE; +import static org.lwjgl.glfw.GLFW.GLFW_NATIVE_CONTEXT_API; +import static org.lwjgl.glfw.GLFW.GLFW_NO_ERROR; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_API; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_CORE_PROFILE; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_DEBUG_CONTEXT; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_FORWARD_COMPAT; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_PROFILE; +import static org.lwjgl.glfw.GLFW.GLFW_RESIZABLE; +import static org.lwjgl.glfw.GLFW.GLFW_TRUE; +import static org.lwjgl.glfw.GLFW.GLFW_VISIBLE; +import static org.lwjgl.glfw.GLFW.GLFW_X11_CLASS_NAME; +import static org.lwjgl.glfw.GLFW.GLFW_X11_INSTANCE_NAME; +import static org.lwjgl.glfw.GLFW.glfwCreateWindow; +import static org.lwjgl.glfw.GLFW.glfwDefaultWindowHints; +import static org.lwjgl.glfw.GLFW.glfwGetError; +import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; +import static org.lwjgl.glfw.GLFW.glfwGetMonitorPos; +import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor; +import static org.lwjgl.glfw.GLFW.glfwGetVideoMode; +import static org.lwjgl.glfw.GLFW.glfwGetWindowPos; +import static org.lwjgl.glfw.GLFW.glfwGetWindowSize; +import static org.lwjgl.glfw.GLFW.glfwInit; +import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; +import static org.lwjgl.glfw.GLFW.glfwMaximizeWindow; +import static org.lwjgl.glfw.GLFW.glfwPollEvents; +import static org.lwjgl.glfw.GLFW.glfwSetFramebufferSizeCallback; +import static org.lwjgl.glfw.GLFW.glfwSetWindowIcon; +import static org.lwjgl.glfw.GLFW.glfwSetWindowPos; +import static org.lwjgl.glfw.GLFW.glfwSetWindowPosCallback; +import static org.lwjgl.glfw.GLFW.glfwSetWindowSizeCallback; +import static org.lwjgl.glfw.GLFW.glfwShowWindow; +import static org.lwjgl.glfw.GLFW.glfwSwapInterval; +import static org.lwjgl.glfw.GLFW.glfwWindowHint; +import static org.lwjgl.glfw.GLFW.glfwWindowHintString; +import static org.lwjgl.opengl.GL32C.GL_TRUE; import java.awt.Desktop; +import java.io.File; import java.io.IOException; import java.net.URI; -import java.nio.ByteBuffer; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import joptsimple.OptionParser; +import net.neoforged.fml.earlydisplay.render.LoadingScreenRenderer; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.loading.FMLConfig; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.fml.loading.progress.ProgressMeter; @@ -41,7 +80,6 @@ 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; @@ -52,29 +90,25 @@ * 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 Logger LOGGER = LoggerFactory.getLogger("EARLYDISPLAY"); - private final AtomicBoolean animationTimerTrigger = new AtomicBoolean(true); + private static final ThreadGroup BACKGROUND_THREAD_GROUP = new ThreadGroup("fml-loadingscreen"); private final ProgressMeter mainProgress; - private ColourScheme colourScheme; - private ElementShader elementShader; + private boolean darkMode; + private Theme theme; - private RenderElement.DisplayContext context; - private List elements; private int framecount; - private EarlyFramebuffer framebuffer; - private ScheduledFuture windowTick; - private ScheduledFuture initializationFuture; + private ScheduledFuture rendererFuture; private PerformanceInfo performanceInfo; private ScheduledFuture performanceTick; @@ -84,19 +118,18 @@ public class DisplayWindow implements ImmediateWindowProvider { 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 SimpleFont font; + private Map fonts; private Runnable repaintTick = () -> {}; + private ThemeColor background; public DisplayWindow() { - mainProgress = StartupNotificationManager.addProgressBar("EARLY", 0); + mainProgress = StartupNotificationManager.addProgressBar("", 0); } @Override @@ -122,176 +155,58 @@ public Runnable initialize(String[] arguments) { 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); + if (System.getenv("FML_EARLY_WINDOW_DARK") != null) { - this.colourScheme = ColourScheme.BLACK; + this.darkMode = true; } else { 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) { + this.darkMode = Boolean.parseBoolean(options.getOrDefault("darkMojangStudiosBackground", "false")); + } catch (NoSuchFileException ignored) { // No options - this.colourScheme = ColourScheme.RED; // default to red colourscheme + } catch (IOException e) { + LOGGER.warn("Failed to read dark-mode settings from options.txt", e); } } + this.theme = Theme.load(new File(""), darkMode); this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED); var forgeVersion = parsed.valueOf(forgeversionopt); StartupNotificationManager.modLoaderConsumer().ifPresent(c -> c.accept("NeoForge loading " + forgeVersion)); performanceInfo = new PerformanceInfo(); - return start(parsed.valueOf(mcversionopt), forgeVersion); - } - private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window - note animation is capped at 20FPS via the tickTimer - private long nextFrameTime = 0; + this.renderScheduler = Executors.newSingleThreadScheduledExecutor( + Thread.ofPlatform().group(BACKGROUND_THREAD_GROUP) + .name("fml-loadingscreen") + .daemon() + .uncaughtExceptionHandler((t, e) -> { + System.err.println("Uncaught error on background rendering thread: " + e); + e.printStackTrace(); + }) + .factory()); + + var mcVersion = parsed.valueOf(mcversionopt); + initWindow(mcVersion); - /** - * 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 { - long nt; - if ((nt = System.nanoTime()) < nextFrameTime) { - return; + this.rendererFuture = renderScheduler.schedule(() -> new LoadingScreenRenderer(renderScheduler, window, theme, mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS); + StartupNotificationManager.addModMessage("BLAHFASEL"); + while (true) { + try { + periodicTick(); + Thread.sleep(100L); + } catch (InterruptedException e) { + throw new RuntimeException(e); } - nextFrameTime = nt + MINFRAMETIME; - glfwMakeContextCurrent(window); - - GlState.readFromOpenGL(); - var backup = GlState.createSnapshot(); - - framebuffer.activate(); - GlState.viewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight()); - this.context.elementShader().activate(); - this.context.elementShader().updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight()); - GlState.clearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f); - paintFramebuffer(); - this.context.elementShader().clear(); - framebuffer.deactivate(); - GlState.viewport(0, 0, fbWidth, fbHeight); - framebuffer.draw(this.fbWidth, this.fbHeight); - // Swap buffers; we're done - glfwSwapBuffers(window); - - GlState.applySnapshot(backup); - } catch (Throwable t) { - LOGGER.error("BARF", t); - } finally { - if (this.windowTick != null) glfwMakeContextCurrent(0); // we release the gl context IF we're running off the main thread - 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); - var capabilities = createCapabilities(); - GlState.readFromOpenGL(); - GlDebug.setCapabilities(capabilities); - LOGGER.info("GL info: {} GL version {}, {}", glGetString(GL_RENDERER), glGetString(GL_VERSION), glGetString(GL_VENDOR)); - - 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 - GlState.clearColor(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); - } 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.fox(font), - RenderElement.logMessageOverlay(font), - RenderElement.forgeVersionOverlay(font, mcVersion + "-" + forgeVersion.split("-")[0]), - RenderElement.performanceBar(font), - RenderElement.progressBars(font))); - - var date = Calendar.getInstance(); - if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SQUIR) || (date.get(Calendar.MONTH) == Calendar.APRIL && date.get(Calendar.DAY_OF_MONTH) == 1)) - this.elements.add(0, RenderElement.squir()); - - GlState.enableBlend(true); - GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, 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); - // schedule a 50 ms ticker to try and smooth out the rendering - renderScheduler.scheduleAtFixedRate(() -> animationTimerTrigger.set(true), 1, 50, 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); - GlState.enableBlend(true); - GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - this.elements.removeIf(element -> !element.render(context, framecount)); - if (animationTimerTrigger.compareAndSet(true, false)) // we only increment the framecount on a periodic basis - framecount++; + //return this::periodicTick; } // Called from NeoForge - public void renderToFramebuffer() { - GlDebug.pushGroup("update EarlyDisplay framebuffer"); - GlState.readFromOpenGL(); - var backup = GlState.createSnapshot(); - - GlState.viewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight()); - framebuffer.activate(); - GlState.clearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f); - elementShader.activate(); - elementShader.updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight()); - paintFramebuffer(); - elementShader.clear(); - framebuffer.deactivate(); - - GlState.applySnapshot(backup); - GlDebug.popGroup(); - } - - /** - * Start the window and Render Thread; we're ready to go. - */ - public Runnable start(@Nullable String mcVersion, final String forgeVersion) { - renderScheduler = Executors.newSingleThreadScheduledExecutor(r -> { - final var thread = Executors.defaultThreadFactory().newThread(r); - thread.setDaemon(true); - return thread; - }); - initWindow(mcVersion); - this.initializationFuture = renderScheduler.schedule(() -> initRender(mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS); - return this::periodicTick; + public void render(int alpha) { + if (rendererFuture.isDone()) { + rendererFuture.resultNow().renderToFramebuffer(); + } } private static final String ERROR_URL = "https://links.neoforged.net/early-display-errors"; @@ -331,10 +246,10 @@ private void crashElegantly(String errorDetails) { /** * 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. * @@ -431,15 +346,12 @@ public void initWindow(@Nullable String mcVersion) { 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.malloc(1)) { - final ByteBuffer imgBuffer; try (GLFWImage glfwImages = GLFWImage.malloc()) { - imgBuffer = STBHelper.loadImageFromClasspath("neoforged_icon.png", 20000, x, y, channels); - glfwImgBuffer.put(glfwImages.set(x[0], y[0], imgBuffer)); + var icon = theme.windowIcon(); + glfwImgBuffer.put(glfwImages.set(icon.width(), icon.height(), icon.imageData())); glfwImgBuffer.flip(); glfwSetWindowIcon(window, glfwImgBuffer); - STBImage.stbi_image_free(imgBuffer); } } catch (NullPointerException e) { LOGGER.error("Failed to load NeoForged icon"); @@ -508,55 +420,57 @@ private static Optional getLastGlfwError() { * @return the Window we own. */ public long takeOverGlfwWindow() { - // wait for the window to actually be initialized + // While this should have happened already, wait for it now to continue + LoadingScreenRenderer renderer; try { - this.initializationFuture.get(30, TimeUnit.SECONDS); + renderer = this.rendererFuture.get(30, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } catch (TimeoutException e) { - Thread.dumpStack(); + dumpBackgroundThreadStack(); crashElegantly("We seem to be having trouble initializing the window, waited for 30 seconds"); + return -1L; // crashElegantly will never return } - // we have to spin wait for the window ticker + updateProgress("Initializing Game Graphics"); - while (!this.windowTick.isDone()) { - this.windowTick.cancel(false); - } + + // Stop the automatic off-thread rendering to move the GL context back to the main thread (this thread) try { - if (!renderLock.tryAcquire(5, TimeUnit.SECONDS)) { - crashElegantly("We seem to be having trouble handing off the window, tried for 5 seconds"); - } + renderer.stopAutomaticRendering(); + } catch (TimeoutException e) { + dumpBackgroundThreadStack(); + crashElegantly("Cannot hand over rendering to Minecraft! The background loading screen renderer seems stuck."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - // we don't want the lock, just making sure it's back on the main thread - renderLock.release(); glfwMakeContextCurrent(window); // Set the title to what the game wants glfwSwapInterval(0); // Clean up our hooks - glfwSetFramebufferSizeCallback(window, null).free(); - glfwSetWindowPosCallback(window, null).free(); - glfwSetWindowSizeCallback(window, null).free(); - this.repaintTick = this::renderThreadFunc; // the repaint will continue to be called until the overlay takes over - this.windowTick = null; // this tells the render thread that the async ticker is done + glfwSetFramebufferSizeCallback(window, null).close(); + glfwSetWindowPosCallback(window, null).close(); + glfwSetWindowSizeCallback(window, null).close(); + this.repaintTick = renderer::renderToScreen; // the repaint will continue to be called until the overlay takes over return window; } @Override public void updateModuleReads(final ModuleLayer layer) {} + // Called from Neo public int getFramebufferTextureId() { - return framebuffer.getTexture(); - } - - public RenderElement.DisplayContext context() { - return this.context; + if (!rendererFuture.isDone()) { + throw new IllegalStateException("Initialization of the renderer has not completed yet."); + } + return rendererFuture.resultNow().getFramebufferTextureId(); } @Override public void periodicTick() { + if (rendererFuture.state() == Future.State.FAILED) { + throw new RuntimeException("Initialization of the loading screen failed.", rendererFuture.exceptionNow()); + } glfwPollEvents(); repaintTick.run(); } @@ -572,20 +486,28 @@ public void completeProgress() { } public void addMojangTexture(final int textureId) { - this.elements.add(0, RenderElement.mojang(textureId, framecount)); +// TODO 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(); + try { + rendererFuture.get().close(); + } catch (ExecutionException e) { + LOGGER.error("Cannot close renderer since it failed to initialize", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // Re-interrupt and continue closing + } } @Override public void crash(final String message) { crashElegantly(message); } + + private static void dumpBackgroundThreadStack() { + BACKGROUND_THREAD_GROUP.list(); + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/EarlyFramebuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/EarlyFramebuffer.java deleted file mode 100644 index a1ba1ef3d..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/EarlyFramebuffer.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay; - -import static org.lwjgl.opengl.GL32C.*; - -import java.nio.IntBuffer; - -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(); - GlState.bindFramebuffer(this.framebuffer); - GlDebug.labelFramebuffer(this.framebuffer, "EarlyDisplay framebuffer"); - - GlState.activeTexture(GL_TEXTURE0); - GlState.bindTexture2D(this.texture); - GlDebug.labelTexture(this.texture, "EarlyDisplay backbuffer"); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, context.width() * context.scale(), context.height() * context.scale(), 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer) null); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.texture, 0); - GlState.bindFramebuffer(0); - } - - void activate() { - GlState.bindFramebuffer(this.framebuffer); - } - - void deactivate() { - GlState.bindFramebuffer(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()); - GlState.bindDrawFramebuffer(0); - GlState.bindReadFramebuffer(this.framebuffer); - final var colour = this.context.colourScheme().background(); - GlState.clearColor(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, RenderElement.clamp(wleft, 0, windowFBWidth), RenderElement.clamp(wtop, 0, windowFBHeight), RenderElement.clamp(wright, 0, windowFBWidth), RenderElement.clamp(wbottom, 0, windowFBHeight), GL_COLOR_BUFFER_BIT, GL_NEAREST); - GlState.bindFramebuffer(0); - } - - int getTexture() { - return this.texture; - } - - public void close() { - glDeleteTextures(this.texture); - glDeleteFramebuffers(this.framebuffer); - } -} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ElementShader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ElementShader.java deleted file mode 100644 index a31ddbf36..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/ElementShader.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.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); - GlDebug.labelShader(vertexShader, "EarlyDisplay vs"); - int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); - GlDebug.labelShader(fragmentShader, "EarlyDisplay fs"); - - // 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(); - GlDebug.labelProgram(program, "EarlyDisplay program"); - 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() { - GlState.useProgram(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() { - GlState.useProgram(0); - } - - public void close() { - glDeleteProgram(program); - } - - public enum RenderType { - FONT, TEXTURE, BAR; - } - - public int program() { - return program; - } -} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java new file mode 100644 index 000000000..1426f4e34 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java @@ -0,0 +1,3 @@ +package net.neoforged.fml.earlydisplay; + +public record RenderContext(float availableWidth, float availableHeight) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderElement.java deleted file mode 100644 index 69d8e2e2c..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderElement.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay; - -import static org.lwjgl.opengl.GL32C.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Supplier; -import net.neoforged.fml.loading.progress.Message; -import net.neoforged.fml.loading.progress.ProgressMeter; -import net.neoforged.fml.loading.progress.StartupNotificationManager; - -public class RenderElement { - 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(String label, final Initializer rendererInitializer) { - this.bb = new SimpleBufferBuilder(label, 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 = context.colourScheme.foreground().packedint(Math.min((int) (fade * 255f), globalAlpha)); - 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 mojang(final int textureId, final int frameStart) { - return new RenderElement("mojang logo", () -> (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); - GlState.bindTexture2D(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, ctx.colourScheme.foreground().packedint(fade)); - QuadHelper.loadQuad(bb, x0 + size, x0 + 2 * size, y0, y0 + size / 2f, 0f, 1f, 0.5f, 1f, ctx.colourScheme.foreground().packedint(fade)); - bb.draw(); - GlState.bindTexture2D(0); - }); - } - - public static RenderElement logMessageOverlay(SimpleFont font) { - return new RenderElement("log messages", RenderElement.initializeText(font, RenderElement::startupLogMessages)); - } - - public static RenderElement forgeVersionOverlay(SimpleFont font, String version) { - return new RenderElement("version overlay", 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("squir", 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 fox(SimpleFont font) { - return new RenderElement("fox", RenderElement.initializeTexture("fox_running.png", 128000, 2, (bb, context, size, frame) -> { - int framecount = 28; - float aspect = size[0] * (float) framecount / size[1]; - int outsize = size[0]; - int offset = outsize / 6; - var x0 = context.scaledWidth() - outsize * context.scale() + offset; - var x1 = context.scaledWidth() + offset; - var y0 = context.scaledHeight() - outsize * context.scale() / aspect - font.descent() - font.lineSpacing(); - var y1 = context.scaledHeight() - font.descent() - font.lineSpacing(); - int frameidx = frame % framecount; - float framesize = 1 / (float) framecount; - float framepos = frameidx * framesize; - QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, framepos, framepos + framesize, globalAlpha << 24 | 0xFFFFFF); - })); - } - - public static RenderElement progressBars(SimpleFont font) { - return new RenderElement("progress bars", () -> (bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame)); - } - - public static RenderElement performanceBar(SimpleFont font) { - return new RenderElement("performance bar", () -> (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); - } - if (acc != null) - acc.accept(buffer, context, frameNumber); - } - - private static final int BAR_HEIGHT = 20; - private static final int BAR_WIDTH = 400; - - 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 = context.colourScheme.foreground().packedint(alpha); - Renderer bar; - if (pm.steps() == 0) { - bar = progressBar(ctx -> new int[] { (ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale() }, f -> colour, frame -> indeterminateBar(frame, cnt == 0)); - } else { - bar = progressBar(ctx -> new int[] { (ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale() }, f -> colour, f -> new float[] { 0f, pm.progress() }); - } - Renderer label = (bb, ctx, frame) -> renderText(font, text((ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, pm.label().getText(), colour), bb, ctx); - return bar.then(label); - } - - private static float[] indeterminateBar(int frame, boolean isActive) { - if (RenderElement.globalAlpha != 0xFF || !isActive) { - 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[] { (ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, BAR_WIDTH * ctx.scale() }, 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) { - GlState.activeTexture(GL_TEXTURE0); - GlState.bindTexture2D(font.textureId()); - 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 () -> { - var texture = STBHelper.loadTextureFromClasspath(textureFileName, size); - return (bb, ctx, frame) -> { - GlState.activeTexture(GL_TEXTURE0); - GlState.bindTexture2D(texture.textureId()); - ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE); - renderTexture(bb, ctx, frame, texture.size(), 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 Math.min(num, max); - } - } - - public static int clamp(int num, int min, int max) { - if (num < min) { - return min; - } else { - return Math.min(num, max); - } - } - - 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/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/STBHelper.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/STBHelper.java deleted file mode 100644 index f5a21f975..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/STBHelper.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay; - -import static org.lwjgl.opengl.GL32C.*; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.util.Objects; -import org.lwjgl.BufferUtils; -import org.lwjgl.stb.STBImage; -import org.lwjgl.system.MemoryUtil; - -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 ImageTexture loadTextureFromClasspath(String file, int size) { - 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(); - GlState.activeTexture(GL_TEXTURE0); - GlState.bindTexture2D(texid); - GlDebug.labelTexture(texid, "EarlyDisplay " + file); -// 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); - GlState.activeTexture(GL_TEXTURE0); - MemoryUtil.memFree(img); - return new ImageTexture( - texid, - new int[] { lw[0], lh[0] }); - } - - public record ImageTexture(int textureId, int[] size) {} - - 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/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/SimpleFont.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/SimpleFont.java deleted file mode 100644 index b20c3fba8..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/SimpleFont.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay; - -import static org.lwjgl.opengl.GL32C.*; -import static org.lwjgl.stb.STBTruetype.*; -import static org.lwjgl.system.MemoryUtil.NULL; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -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; - -public class SimpleFont { - private final int textureId; - 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) { - 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]); - this.textureId = glGenTextures(); - GlState.activeTexture(GL_TEXTURE0); - GlState.bindTexture2D(this.textureId); - GlDebug.labelTexture(this.textureId, "font texture " + fontName); - 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); - } - } - 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 textureId() { - return textureId; - } - - 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/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java new file mode 100644 index 000000000..7f82f1053 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render; + +import static org.lwjgl.opengl.GL32C.*; + +import java.nio.IntBuffer; +import net.neoforged.fml.earlydisplay.render.elements.RenderElement; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; + +public class EarlyFramebuffer { + private final int framebuffer; + private final int texture; + private final int width; + private final int height; + + EarlyFramebuffer(int width, int height) { + this.width = width; + this.height = height; + this.framebuffer = glGenFramebuffers(); + this.texture = glGenTextures(); + GlState.bindFramebuffer(this.framebuffer); + GlDebug.labelFramebuffer(this.framebuffer, "EarlyDisplay framebuffer"); + + GlState.activeTexture(GL_TEXTURE0); + GlState.bindTexture2D(this.texture); + GlDebug.labelTexture(this.texture, "EarlyDisplay backbuffer"); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer) null); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.texture, 0); + GlState.bindFramebuffer(0); + } + + void activate() { + GlState.bindFramebuffer(this.framebuffer); + } + + void deactivate() { + GlState.bindFramebuffer(0); + } + + void blitToScreen(ThemeColor backgroundColor, int windowFBWidth, int windowFBHeight) { + var wscale = ((float) windowFBWidth / width); + var hscale = ((float) windowFBHeight / height); + var scale = Math.min(wscale, hscale) / 2f; + var wleft = (int) (windowFBWidth * 0.5f - scale * width); + var wtop = (int) (windowFBHeight * 0.5f - scale * height); + var wright = (int) (windowFBWidth * 0.5f + scale * width); + var wbottom = (int) (windowFBHeight * 0.5f + scale * height); + GlState.bindDrawFramebuffer(0); + GlState.bindReadFramebuffer(this.framebuffer); + GlState.clearColor(backgroundColor.r(), backgroundColor.g(), backgroundColor.b(), 1f); + glClear(GL_COLOR_BUFFER_BIT); + // src Y are flipped, since our FB is flipped + glBlitFramebuffer( + 0, + height, + width, + 0, + RenderElement.clamp(wleft, 0, windowFBWidth), + RenderElement.clamp(wtop, 0, windowFBHeight), + RenderElement.clamp(wright, 0, windowFBWidth), + RenderElement.clamp(wbottom, 0, windowFBHeight), + GL_COLOR_BUFFER_BIT, + GL_NEAREST); + GlState.bindFramebuffer(0); + } + + int getTexture() { + return this.texture; + } + + public void close() { + glDeleteTextures(this.texture); + glDeleteFramebuffers(this.framebuffer); + } + + public int width() { + return width; + } + + public int height() { + return height; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java new file mode 100644 index 000000000..9c3f38a0f --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java @@ -0,0 +1,25 @@ +package net.neoforged.fml.earlydisplay.render; + +public abstract class ElementRenderer { + public void render() { +// +// @Override +// public void render(float availableWidth, float availableHeight) { +// GlState.activeTexture(GL_TEXTURE0); +// GlState.bindTexture2D(texture.textureId()); +// // TODO ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE); +// +// buffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); +// var inset = 5f; +// var x0 = inset; +// var x1 = inset + texture.width(); +// var y0 = inset; +// var y1 = inset + texture.height(); +// // TODO int fade = (int) (Math.cos(frame * Math.PI / 16) * 16) + 16; +// // TODO var colour = (fade & 0xff) << 24 | 0xffffff; +// // TODO QuadHelper.loadQuad(buffer, x0, x1, y0, y1, 0f, 1f, 0f, 1f, colour); +// +// buffer.draw(); +// } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java new file mode 100644 index 000000000..2a3f4c8dc --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render; + +import static org.lwjgl.opengl.GL32C.GL_ACTIVE_UNIFORMS; +import static org.lwjgl.opengl.GL32C.GL_COMPILE_STATUS; +import static org.lwjgl.opengl.GL32C.GL_FALSE; +import static org.lwjgl.opengl.GL32C.GL_FRAGMENT_SHADER; +import static org.lwjgl.opengl.GL32C.GL_LINK_STATUS; +import static org.lwjgl.opengl.GL32C.GL_VERTEX_SHADER; +import static org.lwjgl.opengl.GL32C.glAttachShader; +import static org.lwjgl.opengl.GL32C.glBindAttribLocation; +import static org.lwjgl.opengl.GL32C.glCompileShader; +import static org.lwjgl.opengl.GL32C.glCreateProgram; +import static org.lwjgl.opengl.GL32C.glCreateShader; +import static org.lwjgl.opengl.GL32C.glDeleteProgram; +import static org.lwjgl.opengl.GL32C.glDeleteShader; +import static org.lwjgl.opengl.GL32C.glDetachShader; +import static org.lwjgl.opengl.GL32C.glGetActiveUniformName; +import static org.lwjgl.opengl.GL32C.glGetProgramInfoLog; +import static org.lwjgl.opengl.GL32C.glGetProgrami; +import static org.lwjgl.opengl.GL32C.glGetShaderInfoLog; +import static org.lwjgl.opengl.GL32C.glGetShaderi; +import static org.lwjgl.opengl.GL32C.glLinkProgram; +import static org.lwjgl.opengl.GL32C.glShaderSource; +import static org.lwjgl.opengl.GL32C.glUniform1i; +import static org.lwjgl.opengl.GL32C.glUniform2f; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import net.neoforged.fml.earlydisplay.theme.ThemeResource; +import org.lwjgl.PointerBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ElementShader implements AutoCloseable { + public static final String UNIFORM_SCREEN_SIZE = "screenSize"; + public static final String UNIFORM_SAMPLER0 = "tex"; + + private static final Logger LOGGER = LoggerFactory.getLogger(ElementShader.class); + + private final String name; + private int program; + private final Map uniformLocations; + private final Set warnedAboutUniforms = new HashSet<>(); + + private ElementShader(String name, int program, Map uniformLocations) { + this.name = name; + this.program = program; + this.uniformLocations = uniformLocations; + } + + public static ElementShader create(String name, ThemeResource vertexShader, ThemeResource fragmentShader) { + try (var vertexShaderBuffer = vertexShader.toNativeBuffer(); + var fragmentShaderBuffer = fragmentShader.toNativeBuffer()) { + return create(name, vertexShaderBuffer.buffer(), fragmentShaderBuffer.buffer()); + } catch (IOException e) { + throw new RuntimeException("Failed to read shaders for " + name); + } + } + + public static ElementShader create(String name, ByteBuffer vertexShaderSource, ByteBuffer fragmentShaderSource) { + int vertexShader = glCreateShader(GL_VERTEX_SHADER); + GlDebug.labelShader(vertexShader, "FML " + name + ".vert"); + int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + GlDebug.labelShader(fragmentShader, "FML " + name + ".frag"); + + // Bind the source of our shaders to the ones created above + var sourcePointers = PointerBuffer.allocateDirect(1); + sourcePointers.put(0, fragmentShaderSource); + glShaderSource(fragmentShader, sourcePointers, new int[] { fragmentShaderSource.remaining() }); + sourcePointers.put(0, vertexShaderSource); + glShaderSource(vertexShader, sourcePointers, new int[] { vertexShaderSource.remaining() }); + + // 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(); + GlDebug.labelProgram(program, "EarlyDisplay program"); + glBindAttribLocation(program, 0, "position"); + glBindAttribLocation(program, 1, "uv"); + glBindAttribLocation(program, 2, "color"); + 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)); + } + + glDetachShader(program, vertexShader); + glDetachShader(program, fragmentShader); + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + + var uniformCount = glGetProgrami(program, GL_ACTIVE_UNIFORMS); + Map uniformLocations = new HashMap<>(uniformCount); + for (var i = 0; i < uniformCount; i++) { + String uniformName = glGetActiveUniformName(program, i); + uniformLocations.put(uniformName, i); + } + + return new ElementShader(name, program, uniformLocations); + } + + public void activate() { + GlState.useProgram(program); + } + + public void clear() { + GlState.useProgram(0); + } + + public boolean hasUniform(String name) { + return uniformLocations.containsKey(name); + } + + public void setUniform1i(String name, int value) { + var location = uniformLocations.get(name); + if (location != null) { + glUniform1i(location, value); + } else { + warnAboutMissingUniform(name); + } + } + + public void setUniform2f(String name, float x, float y) { + var location = uniformLocations.get(name); + if (location != null) { + glUniform2f(location, x, y); + } else { + warnAboutMissingUniform(name); + } + } + + private void warnAboutMissingUniform(String name) { + if (warnedAboutUniforms.add(name)) { + LOGGER.error("Missing uniform '{}' in shader '{}'", name, this); + } + } + + @Override + public void close() { + if (program > 0) { + glDeleteProgram(program); + program = 0; + } + } + + public enum RenderType { + FONT, TEXTURE, BAR; + } + + public int program() { + return program; + } + + @Override + public String toString() { + return name; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/GlDebug.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java similarity index 97% rename from earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/GlDebug.java rename to earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java index 2db8119aa..91720f7e8 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/GlDebug.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java @@ -3,9 +3,10 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.earlydisplay; +package net.neoforged.fml.earlydisplay.render; import net.neoforged.fml.loading.FMLConfig; +import org.jetbrains.annotations.ApiStatus; import org.lwjgl.opengl.EXTDebugLabel; import org.lwjgl.opengl.EXTDebugMarker; import org.lwjgl.opengl.GL32C; @@ -15,7 +16,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -final class GlDebug { +@ApiStatus.Internal +public final class GlDebug { private static final Logger LOG = LoggerFactory.getLogger(GlDebug.class); private GlDebug() {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/GlState.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java similarity index 98% rename from earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/GlState.java rename to earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java index cb3de2435..0c62b593a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/GlState.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.earlydisplay; +package net.neoforged.fml.earlydisplay.render; import static org.lwjgl.opengl.GL20C.glIsProgram; import static org.lwjgl.opengl.GL32C.GL_ACTIVE_TEXTURE; @@ -44,13 +44,16 @@ import static org.lwjgl.opengl.GL32C.glUseProgram; import static org.lwjgl.opengl.GL32C.glViewport; +import org.jetbrains.annotations.ApiStatus; + /** * A static state manager for a subset of OpenGL states to minimize redundant state changes. *

* This class tracks the current state of various OpenGL state elements and only applies changes * when necessary, reducing overhead from redundant state changes. */ -final class GlState { +@ApiStatus.Internal +public final class GlState { // Viewport state private static int viewportX; private static int viewportY; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java new file mode 100644 index 000000000..ae219aeda --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -0,0 +1,291 @@ +package net.neoforged.fml.earlydisplay.render; + +import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; +import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; +import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; +import static org.lwjgl.glfw.GLFW.glfwSwapInterval; +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL11C.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11C.GL_DEPTH_BUFFER_BIT; +import static org.lwjgl.opengl.GL11C.GL_ONE; +import static org.lwjgl.opengl.GL11C.GL_ONE_MINUS_SRC_ALPHA; +import static org.lwjgl.opengl.GL11C.GL_RENDERER; +import static org.lwjgl.opengl.GL11C.GL_SRC_ALPHA; +import static org.lwjgl.opengl.GL11C.GL_VENDOR; +import static org.lwjgl.opengl.GL11C.GL_VERSION; +import static org.lwjgl.opengl.GL11C.glClear; +import static org.lwjgl.opengl.GL11C.glGetString; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import net.neoforged.fml.earlydisplay.render.elements.ImageElement; +import net.neoforged.fml.earlydisplay.render.elements.LabelElement; +import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; +import net.neoforged.fml.earlydisplay.render.elements.RenderElement; +import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; +import org.lwjgl.opengl.GL32C; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoadingScreenRenderer implements AutoCloseable { + private static final Logger LOGGER = LoggerFactory.getLogger(LoadingScreenRenderer.class); + + private final long glfwWindow; + private final Theme theme; + private final String mcVersion; + private final String neoForgeVersion; + + private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window - note animation is capped at 20FPS via the tickTimer + + private int animationFrame; + private long nextFrameTime = 0; + + private final EarlyFramebuffer framebuffer; + + private final ScheduledExecutorService scheduler; + + private final Semaphore renderLock = new Semaphore(1); + + // Scheduled background rendering of the loading screen + private final ScheduledFuture automaticRendering; + + private final Map shaders; + private final Map fonts; + private final List elements; + + private final SimpleBufferBuilder buffer = new SimpleBufferBuilder("shared", 8192); + + /** + * 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. + */ + public LoadingScreenRenderer(ScheduledExecutorService scheduler, + long glfwWindow, + Theme theme, + String mcVersion, + String neoForgeVersion) { + this.scheduler = scheduler; + this.glfwWindow = glfwWindow; + this.theme = theme; + this.mcVersion = mcVersion; + this.neoForgeVersion = neoForgeVersion; + + // This thread owns the GL render context now. We should make a note of that. + glfwMakeContextCurrent(glfwWindow); + // Wait for one frame to be complete before swapping; enable vsync in other words. + glfwSwapInterval(1); + var capabilities = createCapabilities(); + GlState.readFromOpenGL(); + GlDebug.setCapabilities(capabilities); + LOGGER.info("GL info: {} GL version {}, {}", glGetString(GL_RENDERER), glGetString(GL_VERSION), glGetString(GL_VENDOR)); + + // Create shader resources + this.shaders = loadShaders(theme); + this.fonts = loadFonts(theme); + this.elements = loadElements(shaders, fonts, theme); + + // we always render to an 854x480 texture and then fit that to the screen + framebuffer = new EarlyFramebuffer(854, 480); + + // TODO this.elements = new ArrayList<>(Arrays.asList( + // TODO RenderElement.fox(font), + // TODO RenderElement.logMessageOverlay(font), + // TODO RenderElement.forgeVersionOverlay(font, ), + // TODO RenderElement.performanceBar(font), + // TODO RenderElement.progressBars(font))); + // TODO if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SQUIR) || (date.get(Calendar.MONTH) == Calendar.APRIL && date.get(Calendar.DAY_OF_MONTH) == 1)) + // TODO this.elements.add(0, RenderElement.squir()); + + // Set the clear color based on the colour scheme + var background = theme.colorScheme().background(); + GlState.clearColor(background.r(), background.g(), background.b(), 1f); + GL32C.glClear(GL_COLOR_BUFFER_BIT); + + GlState.enableBlend(true); + GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glfwMakeContextCurrent(0); + this.automaticRendering = scheduler.scheduleAtFixedRate(this::renderToScreen, 50, 50, TimeUnit.MILLISECONDS); + // TODO this.performanceTick = scheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS); + // schedule a 50 ms ticker to try and smooth out the rendering + scheduler.scheduleAtFixedRate(() -> animationFrame++, 1, 50, TimeUnit.MILLISECONDS); + } + + private static Map loadShaders(Theme theme) { + var shaders = new HashMap(theme.shaders().size()); + for (var entry : theme.shaders().entrySet()) { + var shader = ElementShader.create( + entry.getKey(), + entry.getValue().vertexShader(), + entry.getValue().fragmentShader()); + shaders.put(entry.getKey(), shader); + } + return shaders; + } + + private static Map loadFonts(Theme theme) { + var fonts = new HashMap(theme.fonts().size()); + for (var entry : theme.fonts().entrySet()) { + try { + fonts.put(entry.getKey(), new SimpleFont(entry.getValue(), 1)); + } catch (IOException e) { + throw new RuntimeException("Failed to load font " + entry.getKey(), e); + } + } + return fonts; + } + + private List loadElements(Map shaders, + Map fonts, + Theme theme) { + var elements = new ArrayList(theme.elements().size()); + + for (var element : theme.elements()) { + elements.add(loadElement(theme, element)); + } + + return elements; + } + + private RenderElement loadElement(Theme theme, ThemeElement element) { + var renderElement = switch (element) { + case ThemeImageElement imageElement -> new ImageElement(imageElement.id(), Texture.create(imageElement.texture())); + + case ThemeStartupLogElement startupLogElement -> new StartupLogElement( + startupLogElement.id(), + theme.colorScheme().text()); + + case ThemeLabelElement labelElement -> { + var version = mcVersion + "-" + neoForgeVersion.split("-")[0]; + yield new LabelElement( + labelElement.id(), + labelElement.text().replace("${version}", version)); + } + + case ThemeProgressBarsElement progressBarsElement -> new ProgressBarsElement(progressBarsElement.id(), theme); + + default -> throw new IllegalStateException("Unexpected theme element: " + element); + }; + + renderElement.setLeft(element.left()); + renderElement.setTop(element.top()); + renderElement.setRight(element.right()); + renderElement.setBottom(element.bottom()); + + return renderElement; + } + + public void stopAutomaticRendering() throws TimeoutException, InterruptedException { + this.automaticRendering.cancel(false); + if (!renderLock.tryAcquire(5, TimeUnit.SECONDS)) { + throw new TimeoutException(); + } + // we don't want the lock, just making sure it's back on the main thread + renderLock.release(); + } + + /** + * 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. + */ + public void renderToScreen() { + if (!renderLock.tryAcquire()) { + return; + } + try { + long nt; + if ((nt = System.nanoTime()) < nextFrameTime) { + return; + } + nextFrameTime = nt + MINFRAMETIME; + glfwMakeContextCurrent(glfwWindow); + + GlState.readFromOpenGL(); + var backup = GlState.createSnapshot(); + + framebuffer.activate(); + GlState.viewport(0, 0, framebuffer.width(), framebuffer.height()); + renderToFramebuffer(); + framebuffer.deactivate(); + + int[] w = new int[1]; + int[] h = new int[1]; + glfwGetFramebufferSize(glfwWindow, w, h); + + GlState.viewport(0, 0, w[0], h[0]); + framebuffer.blitToScreen(this.theme.colorScheme().background(), w[0], h[0]); + // Swap buffers; we're done + glfwSwapBuffers(glfwWindow); + + GlState.applySnapshot(backup); + } catch (Throwable t) { + LOGGER.error("BARF", t); + } finally { + if (this.automaticRendering != null) + glfwMakeContextCurrent(0); // we release the gl context IF we're running off the main thread + renderLock.release(); + } + } + + public void renderToFramebuffer() { + GlDebug.pushGroup("update EarlyDisplay framebuffer"); + GlState.readFromOpenGL(); + var backup = GlState.createSnapshot(); + + GlState.viewport(0, 0, framebuffer.width(), framebuffer.height()); + framebuffer.activate(); + + // Clear the screen to our color + var background = theme.colorScheme().background(); + GlState.clearColor(background.r(), background.g(), background.b(), 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + GlState.enableBlend(true); + GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + for (var shader : shaders.values()) { + shader.activate(); + if (shader.hasUniform(ElementShader.UNIFORM_SCREEN_SIZE)) { + shader.setUniform2f(ElementShader.UNIFORM_SCREEN_SIZE, framebuffer.width(), framebuffer.height()); + } + } + + var context = new RenderContext(buffer, fonts, shaders, framebuffer.width(), framebuffer.height(), animationFrame); + + for (var element : this.elements) { + element.render(context); + } + + framebuffer.deactivate(); + + GlState.applySnapshot(backup); + GlDebug.popGroup(); + } + + @Override + public void close() { + // TODO this.context.elementShader().close(); + SimpleBufferBuilder.destroy(); + } + + public int getFramebufferTextureId() { + return framebuffer.getTexture(); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/QuadHelper.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java similarity index 94% rename from earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/QuadHelper.java rename to earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java index 6d2bc19dc..a5495172a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/QuadHelper.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.earlydisplay; +package net.neoforged.fml.earlydisplay.render; 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) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java new file mode 100644 index 000000000..c6bc4c078 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -0,0 +1,121 @@ +package net.neoforged.fml.earlydisplay.render; + +import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; + +import java.util.List; +import java.util.Map; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.util.Bounds; +import net.neoforged.fml.loading.progress.ProgressMeter; + +public record RenderContext( + SimpleBufferBuilder sharedBuffer, + Map fonts, + Map shaders, + float availableWidth, + float availableHeight, + int animationFrame) { + + public ElementShader bindShader(String shaderId) { + var shader = shaders.get(shaderId); + if (shader == null) { + throw new IllegalArgumentException("Missing shader definition in theme for " + shaderId); + } + shader.activate(); + return shader; + } + + public void blitTexture(Texture texture, float x, float y, float width, float height, int color) { + GlState.activeTexture(GL_TEXTURE0); + GlState.bindTexture2D(texture.textureId()); + + var shader = bindShader(Theme.SHADER_GUI); + shader.setUniform1i(ElementShader.UNIFORM_SAMPLER0, 0); + + sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + + float u0 = 0, u1 = 1, v0 = 0, v1 = 1; + if (texture.animationMetadata() != null) { + int frameCount = texture.animationMetadata().frameCount(); + var frameHeight = texture.physicalHeight() / frameCount; + var vUnit = frameHeight / (float) texture.physicalHeight(); + v0 = (animationFrame % frameCount) * vUnit; + v1 = (animationFrame % frameCount + 1) * vUnit; + } + + QuadHelper.loadQuad(sharedBuffer, x, x + width, y, y + height, u0, u1, v0, v1, color); + + sharedBuffer.draw(); + } + + public SimpleFont getFont(String fontId) { + var font = fonts.getOrDefault(fontId, fonts.get(Theme.FONT_DEFAULT)); + if (font == null) { + throw new IllegalStateException("Theme does not contain a default font. Available fonts: " + fonts.keySet()); + } + return font; + } + + public void renderText(float x, float y, SimpleFont font, List texts) { + GlState.activeTexture(GL_TEXTURE0); + GlState.bindTexture2D(font.textureId()); + var shader = bindShader(Theme.SHADER_FONT); + sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + font.generateVerticesForTexts(x, y, sharedBuffer, texts); + sharedBuffer.draw(); + } + + public void renderProgressBar(Bounds bounds, int cnt, float alpha, SimpleFont font, ProgressMeter pm, ThemeColor background, ThemeColor foreground) { + if (pm.steps() == 0) { + progressBar(background, foreground, bounds, alpha, frame -> indeterminateBar(frame, cnt == 0)); + } else { + progressBar(background, foreground, bounds, alpha, f -> new float[] { 0f, pm.progress() }); + } + } + interface ColourFunction { + int colour(int frame); + } + + interface ProgressDisplay { + float[] progress(int frame); + } + + interface BarPosition { + int[] location(); + } + + public void progressBar(ThemeColor background, ThemeColor foreground, Bounds bounds, float alpha, ProgressDisplay progressDisplay) { + bindShader(Theme.SHADER_COLOR); + var progress = progressDisplay.progress(animationFrame); + sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); + var inset = 2; + var x0 = bounds.left(); + var x1 = bounds.right() + 4 * inset; + var y0 = bounds.top(); + var y1 = bounds.bottom(); + QuadHelper.loadQuad(sharedBuffer, x0, x1, y0, y1, 0f, 0f, 0f, 0f, foreground.withAlpha(alpha).toArgb()); + + x0 += inset; + x1 -= inset; + y0 += inset; + y1 -= inset; + QuadHelper.loadQuad(sharedBuffer, x0, x1, y0, y1, 0f, 0f, 0f, 0f, background.toArgb()); + + x1 = bounds.left() + inset + (int) (progress[1] * bounds.width()); + x0 += inset + progress[0] * bounds.width(); + y0 += inset; + y1 -= inset; + QuadHelper.loadQuad(sharedBuffer, x0, x1, y0, y1, 0f, 0f, 0f, 0f, -1 /* TODO */); + sharedBuffer.draw(); + } + + private static float[] indeterminateBar(int frame, boolean isActive) { + if (!isActive) { + return new float[] { 0f, 1f }; + } else { + var progress = frame % 100; + return new float[] { Math.clamp((progress - 2) / 100f, 0f, 1f), Math.clamp((progress + 2) / 100f, 0f, 1f) }; + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/SimpleBufferBuilder.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java similarity index 99% rename from earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/SimpleBufferBuilder.java rename to earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java index bb1712c13..7c6c022db 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/SimpleBufferBuilder.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.earlydisplay; +package net.neoforged.fml.earlydisplay.render; import static org.lwjgl.opengl.GL20C.glEnableVertexAttribArray; import static org.lwjgl.opengl.GL32C.*; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java new file mode 100644 index 000000000..0e8d81c29 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render; + +import static org.lwjgl.opengl.GL32C.GL_CLAMP_TO_EDGE; +import static org.lwjgl.opengl.GL32C.GL_LINEAR; +import static org.lwjgl.opengl.GL32C.GL_RED; +import static org.lwjgl.opengl.GL32C.GL_TEXTURE0; +import static org.lwjgl.opengl.GL32C.GL_TEXTURE_2D; +import static org.lwjgl.opengl.GL32C.GL_TEXTURE_MAG_FILTER; +import static org.lwjgl.opengl.GL32C.GL_TEXTURE_MIN_FILTER; +import static org.lwjgl.opengl.GL32C.GL_TEXTURE_WRAP_S; +import static org.lwjgl.opengl.GL32C.GL_TEXTURE_WRAP_T; +import static org.lwjgl.opengl.GL32C.GL_UNSIGNED_BYTE; +import static org.lwjgl.opengl.GL32C.glGenTextures; +import static org.lwjgl.opengl.GL32C.glTexImage2D; +import static org.lwjgl.opengl.GL32C.glTexParameteri; +import static org.lwjgl.stb.STBTruetype.stbtt_GetPackedQuad; +import static org.lwjgl.stb.STBTruetype.stbtt_GetScaledFontVMetrics; +import static org.lwjgl.stb.STBTruetype.stbtt_InitFont; +import static org.lwjgl.stb.STBTruetype.stbtt_PackBegin; +import static org.lwjgl.stb.STBTruetype.stbtt_PackEnd; +import static org.lwjgl.stb.STBTruetype.stbtt_PackFontRanges; +import static org.lwjgl.stb.STBTruetype.stbtt_PackSetOversampling; +import static org.lwjgl.stb.STBTruetype.stbtt_PackSetSkipMissingCodepoints; +import static org.lwjgl.system.MemoryUtil.NULL; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import net.neoforged.fml.earlydisplay.theme.ThemeResource; +import net.neoforged.fml.earlydisplay.util.Size; +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; + +public class SimpleFont { + private final int textureId; + private final int lineSpacing; + private final int descent; + private final int GLYPH_COUNT = 127 - 32; + private final Glyph[] glyphs; + + public Size measureText(CharSequence text) { + var width = 0f; + var height = 0f; + var x = 0f; + var y = 0f; + + var codePoints = text.codePoints().iterator(); + + while (codePoints.hasNext()) { + int codePoint = codePoints.next(); + switch (codePoint) { + case '\n' -> { + width = Math.max(width, x); + x = 0; + y += lineSpacing(); + } + case '\t' -> x += glyphs[0].charwidth() * 4; + default -> { + if (codePoint > ' ' && codePoint - ' ' < GLYPH_COUNT) { + x += glyphs[codePoint - ' '].charwidth(); + } + } + } + } + width = Math.max(width, x); + height = y + descent(); + // Ignore the last line if it was empty + if (x > 0) { + height += lineSpacing; + } + + return new Size(width, height); + } + + 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(ThemeResource resource, int scale) throws IOException { + try (var nativeBuffer = resource.toNativeBuffer()) { + var buf = nativeBuffer.buffer(); + 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]); + this.textureId = glGenTextures(); + GlState.activeTexture(GL_TEXTURE0); + GlState.bindTexture2D(this.textureId); + GlDebug.labelTexture(this.textureId, "font texture " + resource); + 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); + } + } + 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 textureId() { + return textureId; + } + + 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(float x, float y, float 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(float x, float y, SimpleBufferBuilder textBB, Iterable texts) { + var pos = new Pos(x, y, x); + for (var text : texts) { + pos = text.generateStringArray(this, pos, textBB); + } + return textBB; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java new file mode 100644 index 000000000..2b4525efa --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -0,0 +1,50 @@ +package net.neoforged.fml.earlydisplay.render; + +import static org.lwjgl.opengl.GL11C.GL_LINEAR; +import static org.lwjgl.opengl.GL11C.GL_RGBA; +import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D; +import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MAG_FILTER; +import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MIN_FILTER; +import static org.lwjgl.opengl.GL11C.GL_UNSIGNED_BYTE; +import static org.lwjgl.opengl.GL11C.glGenTextures; +import static org.lwjgl.opengl.GL11C.glTexImage2D; +import static org.lwjgl.opengl.GL11C.glTexParameteri; +import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; + +import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; +import net.neoforged.fml.earlydisplay.theme.ThemeTexture; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL32C; + +public record Texture(int textureId, int physicalWidth, int physicalHeight, + @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { + public int width() { + return physicalWidth; + } + + public int height() { + return animationMetadata != null ? physicalHeight / animationMetadata.frameCount() : physicalHeight; + } + + /** + * Loads a resource into an OpenGL texture. + */ + public static Texture create(ThemeTexture themeTexture) { + try (var image = themeTexture.resource().loadAsImage()) { + var texId = glGenTextures(); + GlState.activeTexture(GL_TEXTURE0); + GlState.bindTexture2D(texId); + GlDebug.labelTexture(texId, "EarlyDisplay " + themeTexture); + 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, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.imageData()); + GlState.activeTexture(GL_TEXTURE0); + return new Texture(texId, image.width(), image.height(), themeTexture.animation()); + } + } + + @Override + public void close() { + GL32C.glDeleteTextures(textureId); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java new file mode 100644 index 000000000..16b70547e --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java @@ -0,0 +1,31 @@ +package net.neoforged.fml.earlydisplay.render.elements; + +import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.render.Texture; + +public class ImageElement extends RenderElement { + private final Texture texture; + + public ImageElement(String id, Texture texture) { + super(id); + this.texture = texture; + } + + @Override + public void render(RenderContext context) { + int color = -1; + if ("squir".equals(id())) { + int fade = (int) (Math.cos(context.animationFrame() * Math.PI / 16) * 16) + 16; + color = (fade & 0xff) << 24 | 0xffffff; + } + + var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), texture.width(), texture.height()); + + context.blitTexture(texture, bounds.left(), bounds.top(), bounds.width(), bounds.height(), color); + } + + @Override + public void close() { + this.texture.close(); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java new file mode 100644 index 000000000..1d3e3f4ec --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java @@ -0,0 +1,38 @@ +package net.neoforged.fml.earlydisplay.render.elements; + +import java.util.List; +import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.util.Bounds; +import net.neoforged.fml.earlydisplay.util.Size; + +public class LabelElement extends RenderElement { + private final String text; + + public LabelElement(String id, String text) { + super(id); + this.text = text; + } + + @Override + public void render(RenderContext context) { + var texts = List.of( + new SimpleFont.DisplayText(text, -1)); + + var font = context.getFont(Theme.FONT_DEFAULT); + var intrinsicSize = getIntrinsicSize(texts, font); + var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), intrinsicSize.width(), intrinsicSize.height()); + + context.renderText(bounds.left(), bounds.top(), font, texts); + } + + private static Size getIntrinsicSize(List texts, SimpleFont font) { + var bounds = new Bounds(0, 0, 0, 0); + for (var text : texts) { + bounds = bounds.union( + new Bounds(0, bounds.bottom(), font.measureText(text.string()))); + } + return new Size(bounds.width(), bounds.height()); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java new file mode 100644 index 000000000..44e971705 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java @@ -0,0 +1,49 @@ +package net.neoforged.fml.earlydisplay.render.elements; + +import java.util.List; +import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.loading.progress.StartupNotificationManager; + +public class ProgressBarsElement extends RenderElement { + private final Theme theme; + + public ProgressBarsElement(String id, Theme theme) { + super(id); + this.theme = theme; + } + + private static final int BAR_HEIGHT = 20; + private static final int BAR_WIDTH = 400; + + @Override + public void render(RenderContext context) { + var font = context.getFont(Theme.FONT_DEFAULT); + + var alpha = 0xFF; + var barsShown = 0; + for (var progress : StartupNotificationManager.getCurrentProgress()) { + if (++barsShown > 2) { + break; + } + + var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), BAR_WIDTH, BAR_HEIGHT); + + context.renderProgressBar( + bounds, + barsShown, + 1f, + font, + progress, + theme.colorScheme().background(), + theme.colorScheme().text()); + context.renderText( + bounds.left(), + bounds.bottom(), + font, + List.of( + new SimpleFont.DisplayText(progress.label().getText(), theme.colorScheme().text().toArgb()))); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java new file mode 100644 index 000000000..cf1d619af --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.elements; + +import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.util.Bounds; +import net.neoforged.fml.earlydisplay.util.StyleLength; + +public abstract class RenderElement implements AutoCloseable { + private int retireCount; + + private final String id; + + private StyleLength left = StyleLength.ofUndefined(); + private StyleLength top = StyleLength.ofUndefined(); + private StyleLength right = StyleLength.ofUndefined(); + private StyleLength bottom = StyleLength.ofUndefined(); + + public RenderElement(String id) { + this.id = id; + } + + public String id() { + return this.id; + } + + public abstract void render(RenderContext context); + + public Bounds resolveBounds(float availableWidth, float availableHeight, float intrinsicWidth, float intrinsicHeight) { + var left = resolve(this.left, availableWidth); + var right = availableWidth - resolve(this.right, availableWidth); + var top = resolve(this.top, availableHeight); + var bottom = availableHeight - resolve(this.bottom, availableHeight); + + var width = right - left; + var height = bottom - top; + + boolean widthDefined = !Float.isNaN(width); + boolean heightDefined = !Float.isNaN(height); + + // Handle aspect ratio + if (widthDefined != heightDefined) { + float ar = intrinsicWidth / intrinsicHeight; + if (widthDefined) { + height = width / ar; + } else { + width = height * ar; + } + } else if (!widthDefined && !heightDefined) { + width = intrinsicWidth; + height = intrinsicHeight; + } + + // Fill out the unspecified size based on width/height + if (Float.isNaN(left) && Float.isNaN(right)) { + left = 0; + right = width; + } else if (Float.isNaN(right)) { + right = left + width; + } else if (Float.isNaN(left)) { + left = right - width; + } + if (Float.isNaN(top) && Float.isNaN(bottom)) { + top = 0; + bottom = height; + } else if (Float.isNaN(bottom)) { + bottom = top + height; + } else if (Float.isNaN(top)) { + top = bottom - height; + } + + return new Bounds(left, top, right, bottom); + } + + private static float resolve(StyleLength length, float availableSpace) { + return switch (length.unit()) { + case UNDEFINED -> Float.NaN; + case POINT -> length.value(); + case PERCENT -> (length.value() * availableSpace) / 100.0f; + }; + } + + public StyleLength left() { + return left; + } + + public void setLeft(StyleLength left) { + this.left = left; + } + + public StyleLength top() { + return top; + } + + public void setTop(StyleLength top) { + this.top = top; + } + + public StyleLength right() { + return right; + } + + public void setRight(StyleLength right) { + this.right = right; + } + + public StyleLength bottom() { + return bottom; + } + + public void setBottom(StyleLength bottom) { + this.bottom = bottom; + } + + // +// public void retire(final int frame) { +// this.retireCount = frame; +// } +// +// public static RenderElement mojang(final int textureId, final int frameStart) { +// return new RenderElement("mojang logo", () -> (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); +// GlState.bindTexture2D(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, ctx.colourScheme.foreground().packedint(fade)); +// QuadHelper.loadQuad(bb, x0 + size, x0 + 2 * size, y0, y0 + size / 2f, 0f, 1f, 0.5f, 1f, ctx.colourScheme.foreground().packedint(fade)); +// bb.draw(); +// GlState.bindTexture2D(0); +// }); +// } +// +// public static RenderElement forgeVersionOverlay(SimpleFont font, String version) { +// return new RenderElement("version overlay", 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("squir", RenderElement.initializeTexture(SQUIR, (bb, context, size, frame) -> { +// +// })); +// } +// +// public static RenderElement fox(SimpleFont font) { +// return new RenderElement("fox", RenderElement.initializeTexture(FOX_RUNNING, (bb, context, size, frame) -> { +// int framecount = 28; +// float aspect = size[0] * (float) framecount / size[1]; +// int outsize = size[0]; +// int offset = outsize / 6; +// var x0 = context.scaledWidth() - outsize * context.scale() + offset; +// var x1 = context.scaledWidth() + offset; +// var y0 = context.scaledHeight() - outsize * context.scale() / aspect - font.descent() - font.lineSpacing(); +// var y1 = context.scaledHeight() - font.descent() - font.lineSpacing(); +// int frameidx = frame % framecount; +// float framesize = 1 / (float) framecount; +// float framepos = frameidx * framesize; +// QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, framepos, framepos + framesize, globalAlpha << 24 | 0xFFFFFF); +// })); +// } +// +// public static RenderElement progressBars(SimpleFont font) { +// return new RenderElement("progress bars", () -> (bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame)); +// } +// +// public static RenderElement performanceBar(SimpleFont font) { +// return new RenderElement("performance bar", () -> (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); +// } +// if (acc != null) +// acc.accept(buffer, context, frameNumber); +// } +// +// 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[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, BAR_WIDTH * ctx.scale()}, 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); +// } +// +// 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) { +// GlState.activeTexture(GL_TEXTURE0); +// GlState.bindTexture2D(font.textureId()); +// 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)); +// } + + public static float clamp(float num, float min, float max) { + if (num < min) { + return min; + } else { + return Math.min(num, max); + } + } + + public static int clamp(int num, int min, int max) { + if (num < min) { + return min; + } else { + return Math.min(num, max); + } + } + + 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; + } + + @Override + public void close() {} + + @Override + public String toString() { + return id; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java new file mode 100644 index 000000000..9c63219ed --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java @@ -0,0 +1,52 @@ +package net.neoforged.fml.earlydisplay.render.elements; + +import java.util.ArrayList; +import java.util.List; +import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.util.Bounds; +import net.neoforged.fml.earlydisplay.util.Size; +import net.neoforged.fml.loading.progress.Message; +import net.neoforged.fml.loading.progress.StartupNotificationManager; + +public class StartupLogElement extends RenderElement { + private ThemeColor textColor; + + public StartupLogElement(String id, ThemeColor textColor) { + super(id); + this.textColor = textColor; + } + + @Override + public void render(RenderContext 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 = textColor.withAlpha(fade).toArgb(); + texts.add(new SimpleFont.DisplayText(msg.getText() + "\n", colour)); + } + + var font = context.getFont(Theme.FONT_DEFAULT); + var intrinsicSize = getIntrinsicSize(texts, font); + var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), intrinsicSize.width(), intrinsicSize.height()); + + context.renderText(bounds.left(), bounds.top(), font, texts); + } + + private static Size getIntrinsicSize(List texts, SimpleFont font) { + var bounds = new Bounds(0, 0, 0, 0); + for (var text : texts) { + bounds = bounds.union( + new Bounds(0, bounds.bottom(), font.measureText(text.string()))); + } + return new Size(bounds.width(), bounds.height()); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java new file mode 100644 index 000000000..6c8a115a8 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java @@ -0,0 +1,8 @@ +package net.neoforged.fml.earlydisplay.theme; + +/** + * Optional metadata for a {@link ImageLoader} to animate an image. + * + * @param frameCount The number of frames vertically stacked in the source image. + */ +public record AnimationMetadata(int frameCount) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java new file mode 100644 index 000000000..618a64492 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java @@ -0,0 +1,38 @@ +package net.neoforged.fml.earlydisplay.theme; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.lwjgl.system.MemoryUtil; + +public record ClasspathResource(String path) implements ThemeResource { + public NativeBuffer toNativeBuffer() throws IOException { + var resource = getClass().getClassLoader().getResource(path); + if (resource == null) { + throw new FileNotFoundException("Couldn't find classpath resource " + path); + } + + var connection = resource.openConnection(); + try (var in = connection.getInputStream()) { + var contentLengthHint = connection.getContentLength(); + if (contentLengthHint == -1) { + contentLengthHint = 8 * 1024; + } + + ByteBuffer buffer = MemoryUtil.memAlloc(contentLengthHint); + byte[] tmp = new byte[8 * 1024]; + int read; + + while ((read = in.read(tmp)) != -1) { + if (buffer.remaining() < read) { + buffer = MemoryUtil.memRealloc(buffer, buffer.capacity() * 2); + } + buffer.put(tmp, 0, read); + } + + buffer.flip(); + + return new NativeBuffer(buffer, MemoryUtil::memFree); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java new file mode 100644 index 000000000..a43bd5268 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java @@ -0,0 +1,26 @@ +package net.neoforged.fml.earlydisplay.theme; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public record FileResource(File file) implements ThemeResource { + public NativeBuffer toNativeBuffer() throws IOException { + try (var fis = new FileInputStream(file)) { + var channel = fis.getChannel(); + + long size = channel.size(); + if (size > MAX_SIZE) { + throw new IOException("The resource " + this + " exceeds the maximum size of " + MAX_SIZE); + } + + // Allocate a ByteBuffer with the file size + var buffer = ByteBuffer.allocateDirect((int) size).order(ByteOrder.nativeOrder()); + channel.read(buffer); + buffer.flip(); + return new NativeBuffer(buffer, ignored -> {}); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java new file mode 100644 index 000000000..c45246d71 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -0,0 +1,61 @@ +package net.neoforged.fml.earlydisplay.theme; + +import org.lwjgl.stb.STBImage; +import org.lwjgl.system.MemoryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents an image referenced from a theme. + *

+ * Theme images will refer to a resource their content is loaded from, this is expected to be a PNG image. + */ +final class ImageLoader { + private static final int BROKEN_TEXTURE_DIMENSIONS = 16; + static final Logger LOGGER = LoggerFactory.getLogger(ImageLoader.class); + + /** + * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. + * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. + */ + static UncompressedImage loadImage(ThemeResource resource) { + try (var buffer = resource.toNativeBuffer()) { + var width = new int[1]; + var height = new int[1]; + var channels = new int[1]; + var decodedImage = STBImage.stbi_load_from_memory(buffer.buffer(), width, height, channels, 4); + // TODO: Handle image decoding error + return new UncompressedImage(resource.toString(), + new NativeBuffer(decodedImage, STBImage::stbi_image_free), + width[0], + height[0]); + } catch (Exception e) { + LOGGER.error("Failed to load theme image {}", resource, e); + return createBrokenImage(); + } + } + + private static UncompressedImage createBrokenImage() { + var pixelData = MemoryUtil.memAlloc(BROKEN_TEXTURE_DIMENSIONS * BROKEN_TEXTURE_DIMENSIONS * 4); + var pixelBuffer = pixelData.asIntBuffer(); // ABGR format + + for (var y = 0; y < BROKEN_TEXTURE_DIMENSIONS; y++) { + for (var x = 0; x < BROKEN_TEXTURE_DIMENSIONS; x++) { + if (x < BROKEN_TEXTURE_DIMENSIONS / 2 ^ y < BROKEN_TEXTURE_DIMENSIONS / 2) { + pixelBuffer.put(0xFFF800F8); + } else { + pixelBuffer.put(0xFF000000); + } + } + } + + var nativeBuffer = new NativeBuffer(pixelData, MemoryUtil::memFree); + return new UncompressedImage( + "broken texture", + nativeBuffer, + BROKEN_TEXTURE_DIMENSIONS, + BROKEN_TEXTURE_DIMENSIONS); + } + + private ImageLoader() {} +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java new file mode 100644 index 000000000..68d3758cc --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java @@ -0,0 +1,27 @@ +package net.neoforged.fml.earlydisplay.theme; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class NativeBuffer implements AutoCloseable { + private final ByteBuffer buffer; + private final Consumer deallocator; + private final AtomicBoolean deallocated = new AtomicBoolean(); + + public NativeBuffer(ByteBuffer buffer, Consumer deallocator) { + this.buffer = buffer; + this.deallocator = deallocator; + } + + public ByteBuffer buffer() { + return buffer; + } + + @Override + public void close() { + if (deallocated.compareAndSet(false, true)) { + deallocator.accept(buffer); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java new file mode 100644 index 000000000..54355fbdf --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -0,0 +1,91 @@ +package net.neoforged.fml.earlydisplay.theme; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; +import net.neoforged.fml.earlydisplay.util.StyleLength; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Defines a theme for the early display screen. + */ +public record Theme( + UncompressedImage windowIcon, + Map fonts, + Map shaders, + List elements, + ThemeColorScheme colorScheme) implements AutoCloseable { + + private static final Logger LOGGER = LoggerFactory.getLogger(Theme.class); + public static final String FONT_DEFAULT = "default"; + public static final String SHADER_GUI = "gui"; + public static final String SHADER_FONT = "font"; + public static final String SHADER_COLOR = "color"; + public static Theme createDefaultTheme(boolean darkMode) { + var squir = new ThemeImageElement( + "squir", + new ThemeTexture(new ClasspathResource("/squirrel.png"))); + + var startupLog = new ThemeStartupLogElement("startupLog"); + startupLog.setLeft(StyleLength.ofPoints(10)); + startupLog.setBottom(StyleLength.ofPoints(10)); + + var fox = new ThemeImageElement( + "fox", + new ThemeTexture(new ClasspathResource("/fox_running.png"), new AnimationMetadata(28))); + fox.setRight(StyleLength.ofPoints(10)); + fox.setBottom(StyleLength.ofPoints(10)); + + var forgeVersion = new ThemeLabelElement("version", "${version}"); + forgeVersion.setBottom(StyleLength.ofPoints(10)); + forgeVersion.setRight(StyleLength.ofPoints(10)); + + var progressBars = new ThemeProgressBarsElement("progressBars"); + progressBars.setRight(StyleLength.ofPoints(400)); + progressBars.setTop(StyleLength.ofPoints(250)); + + return new Theme( + ImageLoader.loadImage(new ClasspathResource("/neoforged_icon.png")), + Map.of( + FONT_DEFAULT, new ClasspathResource("/Monocraft.ttf")), + Map.of( + SHADER_GUI, + ThemeShader.DEFAULT_GUI, + SHADER_FONT, + ThemeShader.DEFAULT_FONT, + SHADER_COLOR, + ThemeShader.DEFAULT_COLOR), + List.of(squir, fox, startupLog, forgeVersion, progressBars), + ThemeColorScheme.DEFAULT); + } + + public static Theme load(File path, boolean darkMode) { + var properties = new Properties(); + try (var in = new BufferedInputStream(new FileInputStream(path))) { + properties.load(in); + // TODO: actually load custom theme + } catch (FileNotFoundException e) { + LOGGER.error("Failed to find theme {}", path); + } catch (IOException e) { + LOGGER.error("Failed to read loading window theme from {}", path, e); + } + + return createDefaultTheme(darkMode); + } + + @Override + public void close() { + windowIcon.close(); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java new file mode 100644 index 000000000..e16eb29c5 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java @@ -0,0 +1,22 @@ +package net.neoforged.fml.earlydisplay.theme; + +public record ThemeColor(float r, float g, float b, float a) { + public static ThemeColor ofBytes(int r, int g, int b, int a) { + return new ThemeColor(r / 255.f, g / 255.f, b / 255.f, a / 255.f); + } + + public static ThemeColor ofBytes(int r, int g, int b) { + return ofBytes(r, g, b, 255); + } + + public ThemeColor withAlpha(float alpha) { + return new ThemeColor(r, g, b, alpha); + } + + public int toArgb() { + return (((int) (a * 255)) & 0xFF) << 24 + | (((int) (b * 255)) & 0xFF) << 16 + | (((int) (g * 255)) & 0xFF) << 8 + | (((int) (r * 255)) & 0xFF); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java new file mode 100644 index 000000000..27f0a26c8 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java @@ -0,0 +1,35 @@ +package net.neoforged.fml.earlydisplay.theme; + +public class ThemeColorScheme { + public static final ThemeColorScheme DEFAULT = new ThemeColorScheme( + new UnresolvedThemeColor(ThemeColor.ofBytes(239, 50, 61), ThemeColor.ofBytes(0, 0, 0)), + new UnresolvedThemeColor(ThemeColor.ofBytes(255, 255, 255))); + + private final UnresolvedThemeColor background; + + private final UnresolvedThemeColor text; + + private boolean darkMode; + + public ThemeColorScheme(UnresolvedThemeColor background, + UnresolvedThemeColor text) { + this.background = background; + this.text = text; + } + + public boolean darkMode() { + return darkMode; + } + + public void setDarkMode(boolean darkMode) { + this.darkMode = darkMode; + } + + public ThemeColor background() { + return background.resolve(darkMode); + } + + public ThemeColor text() { + return text.resolve(darkMode); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java new file mode 100644 index 000000000..57a67d1ed --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java @@ -0,0 +1,20 @@ +package net.neoforged.fml.earlydisplay.theme; + +import java.io.IOException; + +public sealed interface ThemeResource permits ClasspathResource, FileResource { + /** + * Sanity check to stop going OOM instead of showing the loading screen. + */ + int MAX_SIZE = 100_000_000; + + NativeBuffer toNativeBuffer() throws IOException; + + /** + * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. + * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. + */ + default UncompressedImage loadAsImage() { + return ImageLoader.loadImage(this); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java new file mode 100644 index 000000000..84e8941bc --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java @@ -0,0 +1,15 @@ +package net.neoforged.fml.earlydisplay.theme; + +public record ThemeShader( + ThemeResource vertexShader, + ThemeResource fragmentShader) { + public static final ThemeShader DEFAULT_GUI = new ThemeShader( + new ClasspathResource("net/neoforged/fml/earlydisplay/gui.vert"), + new ClasspathResource("net/neoforged/fml/earlydisplay/gui.frag")); + public static final ThemeShader DEFAULT_FONT = new ThemeShader( + new ClasspathResource("net/neoforged/fml/earlydisplay/gui.vert"), + new ClasspathResource("net/neoforged/fml/earlydisplay/gui_font.frag")); + public static final ThemeShader DEFAULT_COLOR = new ThemeShader( + new ClasspathResource("net/neoforged/fml/earlydisplay/gui.vert"), + new ClasspathResource("net/neoforged/fml/earlydisplay/gui_color.frag")); +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java new file mode 100644 index 000000000..0b5f5bfe1 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java @@ -0,0 +1,14 @@ +package net.neoforged.fml.earlydisplay.theme; + +import org.jetbrains.annotations.Nullable; + +public record ThemeTexture(ThemeResource resource, @Nullable AnimationMetadata animation) { + public ThemeTexture(ThemeResource resource) { + this(resource, null); + } + + @Override + public String toString() { + return resource.toString(); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java new file mode 100644 index 000000000..1bce4df22 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java @@ -0,0 +1,23 @@ +package net.neoforged.fml.earlydisplay.theme; + +import java.nio.ByteBuffer; + +/** + * Image data loaded into memory and decompressed. + */ +public record UncompressedImage(String name, NativeBuffer nativeImageData, int width, + int height) implements AutoCloseable { + public ByteBuffer imageData() { + return nativeImageData.buffer(); + } + + @Override + public void close() { + nativeImageData.close(); + } + + @Override + public String toString() { + return name; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java new file mode 100644 index 000000000..55174cc27 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java @@ -0,0 +1,13 @@ +package net.neoforged.fml.earlydisplay.theme; + +import org.jetbrains.annotations.Nullable; + +public record UnresolvedThemeColor(ThemeColor lightBackground, @Nullable ThemeColor darkBackground) { + public UnresolvedThemeColor(ThemeColor lightBackground) { + this(lightBackground, null); + } + + public ThemeColor resolve(boolean darkMode) { + return darkMode && darkBackground != null ? darkBackground : lightBackground; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java new file mode 100644 index 000000000..e0b7dec4e --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -0,0 +1,57 @@ +package net.neoforged.fml.earlydisplay.theme.elements; + +import net.neoforged.fml.earlydisplay.util.StyleLength; + +public abstract class ThemeElement { + private final String id; + + private StyleLength left = StyleLength.ofUndefined(); + private StyleLength top = StyleLength.ofUndefined(); + private StyleLength right = StyleLength.ofUndefined(); + private StyleLength bottom = StyleLength.ofUndefined(); + + public ThemeElement(String id) { + this.id = id; + } + + public String id() { + return id; + } + + public StyleLength left() { + return left; + } + + public void setLeft(StyleLength left) { + this.left = left; + } + + public StyleLength top() { + return top; + } + + public void setTop(StyleLength top) { + this.top = top; + } + + public StyleLength right() { + return right; + } + + public void setRight(StyleLength right) { + this.right = right; + } + + public StyleLength bottom() { + return bottom; + } + + public void setBottom(StyleLength bottom) { + this.bottom = bottom; + } + + @Override + public String toString() { + return id; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java new file mode 100644 index 000000000..7560efcaf --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java @@ -0,0 +1,16 @@ +package net.neoforged.fml.earlydisplay.theme.elements; + +import net.neoforged.fml.earlydisplay.theme.ThemeTexture; + +public class ThemeImageElement extends ThemeElement { + private final ThemeTexture texture; + + public ThemeImageElement(String id, ThemeTexture texture) { + super(id); + this.texture = texture; + } + + public ThemeTexture texture() { + return texture; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java new file mode 100644 index 000000000..c18975dce --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java @@ -0,0 +1,14 @@ +package net.neoforged.fml.earlydisplay.theme.elements; + +public class ThemeLabelElement extends ThemeElement { + private final String text; + + public ThemeLabelElement(String id, String text) { + super(id); + this.text = text; + } + + public String text() { + return text; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java new file mode 100644 index 000000000..8b29b2783 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java @@ -0,0 +1,7 @@ +package net.neoforged.fml.earlydisplay.theme.elements; + +public class ThemeProgressBarsElement extends ThemeElement { + public ThemeProgressBarsElement(String id) { + super(id); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java new file mode 100644 index 000000000..9ef2504d0 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java @@ -0,0 +1,7 @@ +package net.neoforged.fml.earlydisplay.theme.elements; + +public class ThemeStartupLogElement extends ThemeElement { + public ThemeStartupLogElement(String id) { + super(id); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java new file mode 100644 index 000000000..0a50fed8a --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java @@ -0,0 +1,23 @@ +package net.neoforged.fml.earlydisplay.util; + +public record Bounds(float left, float top, float right, float bottom) { + public Bounds(float x, float y, Size size) { + this(x, y, x + size.width(), y + size.height()); + } + + public float width() { + return right - left; + } + + public float height() { + return bottom - top; + } + + public Bounds union(Bounds other) { + return new Bounds( + Math.min(left, other.left), + Math.min(top, other.top), + Math.max(right, other.right), + Math.max(bottom, other.bottom)); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java new file mode 100644 index 000000000..43d6387a8 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java @@ -0,0 +1,3 @@ +package net.neoforged.fml.earlydisplay.util; + +public record Size(float width, float height) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java new file mode 100644 index 000000000..b33a9da46 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java @@ -0,0 +1,52 @@ +package net.neoforged.fml.earlydisplay.util; + +/** + * A style value that can be used as a position, which can be: + *

+ */ +public final class StyleLength { + private static final StyleLength UNDEFINED = new StyleLength(Unit.UNDEFINED, Float.NaN); + private final Unit unit; + private final float value; + + private StyleLength(Unit unit, float value) { + this.unit = unit; + this.value = value; + } + + public static StyleLength ofUndefined() { + return UNDEFINED; + } + + public static StyleLength ofPoints(float points) { + if (Float.isNaN(points)) { + return ofUndefined(); + } + return new StyleLength(Unit.POINT, points); + } + + public static StyleLength ofPercent(float percent) { + if (Float.isNaN(percent)) { + return ofUndefined(); + } + return new StyleLength(Unit.PERCENT, percent); + } + + public Unit unit() { + return unit; + } + + public float value() { + return value; + } + + public enum Unit { + UNDEFINED, + POINT, + PERCENT + } +} diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.frag b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.frag new file mode 100644 index 000000000..e13f23980 --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.frag @@ -0,0 +1,9 @@ +#version 150 core +uniform sampler2D tex; +in vec2 fTex; +in vec4 fColour; +out vec4 fragColor; + +void main() { + fragColor = texture(tex, fTex) * fColour; +} diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.vert b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.vert new file mode 100644 index 000000000..ca5f0cf6b --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.vert @@ -0,0 +1,14 @@ +#version 150 core + +uniform vec2 screenSize; +in vec2 position; +in vec2 uv; +in vec4 color; +out vec2 fTex; +out vec4 fColour; + +void main() { + fTex = uv; + fColour = color; + gl_Position = vec4((position / screenSize) * 2 - 1, 0.0, 1.0); +} diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_color.frag b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_color.frag new file mode 100644 index 000000000..63c7b19a7 --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_color.frag @@ -0,0 +1,8 @@ +#version 150 core +in vec2 fTex; +in vec4 fColour; +out vec4 fragColor; + +void main() { + fragColor = fColour; +} diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_font.frag b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_font.frag new file mode 100644 index 000000000..f3ab1b984 --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_font.frag @@ -0,0 +1,9 @@ +#version 150 core +uniform sampler2D tex; +in vec2 fTex; +in vec4 fColour; +out vec4 fragColor; + +void main() { + fragColor = texture(tex, fTex).r * fColour; +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java b/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java index 6e65e81bb..5cd7b9158 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java @@ -43,7 +43,6 @@ public enum ConfigValue { 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", 1, "Early window framebuffer scale"), EARLY_WINDOW_MAXIMIZED("earlyWindowMaximized", Boolean.FALSE, "Early window starts maximized"), EARLY_WINDOW_SQUIR("earlyWindowSquir", Boolean.FALSE, "Squir?"); From 3470e2ac5d8cbb7efe1745a3940cf421aa979f62 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Fri, 18 Apr 2025 01:11:06 +0200 Subject: [PATCH 02/22] Early display theming --- earlydisplay/build.gradle | 40 ++- .../fml/earlydisplay/DisplayWindow.java | 63 ++-- .../fml/earlydisplay/PerformanceInfo.java | 45 --- .../fml/earlydisplay/render/GlState.java | 46 ++- .../render/LoadingScreenRenderer.java | 90 ++---- .../render/MaterializedTheme.java | 66 ++++ .../fml/earlydisplay/render/QuadHelper.java | 180 +++++++++++ .../earlydisplay/render/RenderContext.java | 108 ++----- .../render/SimpleBufferBuilder.java | 19 +- .../fml/earlydisplay/render/SimpleFont.java | 20 +- .../fml/earlydisplay/render/Texture.java | 8 +- .../render/elements/ImageElement.java | 7 +- .../render/elements/LabelElement.java | 7 +- .../render/elements/PerformanceElement.java | 125 ++++++++ .../render/elements/ProgressBarsElement.java | 107 +++++-- .../render/elements/RenderElement.java | 89 ++---- .../render/elements/StartupLogElement.java | 7 +- .../fml/earlydisplay/theme/ImageLoader.java | 2 + .../fml/earlydisplay/theme/NativeBuffer.java | 7 + .../earlydisplay/theme/TextureScaling.java | 21 ++ .../fml/earlydisplay/theme/Theme.java | 96 +++--- .../fml/earlydisplay/theme/ThemeColor.java | 97 +++++- .../earlydisplay/theme/ThemeColorScheme.java | 34 +- .../earlydisplay/theme/ThemeSerializer.java | 290 ++++++++++++++++++ .../fml/earlydisplay/theme/ThemeTexture.java | 6 +- .../earlydisplay/theme/UncompressedImage.java | 7 +- .../theme/UnresolvedThemeColor.java | 13 - .../theme/elements/ThemeElement.java | 28 +- .../theme/elements/ThemeImageElement.java | 11 +- .../theme/elements/ThemeLabelElement.java | 13 +- .../elements/ThemePerformanceElement.java | 58 ++++ .../elements/ThemeProgressBarsElement.java | 81 ++++- .../elements/ThemeStartupLogElement.java | 6 +- .../fml/earlydisplay/util/Bounds.java | 12 + .../fml/earlydisplay/util/StyleLength.java | 18 ++ .../src/main/resources/fox_running.png | Bin 127745 -> 0 bytes .../fml/earlydisplay/theme/Monocraft.ttf | Bin 0 -> 202764 bytes .../fml/earlydisplay/theme/fox_running.png | Bin 0 -> 86507 bytes .../fml/earlydisplay/theme/neoforged_icon.png | Bin 0 -> 2400 bytes .../earlydisplay/theme/progress_bar_bg.png | Bin 0 -> 138 bytes .../earlydisplay/theme/progress_bar_fg.png | Bin 0 -> 129 bytes .../fml/earlydisplay/theme/squirrel.png | Bin 0 -> 43999 bytes .../fml/earlydisplay/TestEarlyDisplay.java | 43 +++ .../earlydisplay/render/SimpleFontTest.java | 32 ++ .../render/WithOffScreenGLSurface.java | 85 +++++ .../earlydisplay/theme/ThemeColorTest.java | 23 ++ .../theme/ThemeSerializerTest.java | 48 +++ gradle.properties | 4 +- .../fml/loading/progress/Message.java | 2 +- 49 files changed, 1603 insertions(+), 461 deletions(-) delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/PerformanceInfo.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java delete mode 100644 earlydisplay/src/main/resources/fox_running.png create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/Monocraft.ttf create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/fox_running.png create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/neoforged_icon.png create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/progress_bar_bg.png create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/progress_bar_fg.png create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/squirrel.png create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/WithOffScreenGLSurface.java create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeColorTest.java create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java diff --git a/earlydisplay/build.gradle b/earlydisplay/build.gradle index 9c796f245..bfb9d18da 100644 --- a/earlydisplay/build.gradle +++ b/earlydisplay/build.gradle @@ -15,22 +15,38 @@ switch (OperatingSystem.current()) { dependencies { implementation(project(':loader')) - + compileOnly("org.jetbrains:annotations:${jetbrains_annotations_version}") - implementation("org.lwjgl:lwjgl:${lwjgl_version}") - implementation("org.lwjgl:lwjgl-glfw:${lwjgl_version}") - implementation("org.lwjgl:lwjgl-opengl:${lwjgl_version}") - implementation("org.lwjgl:lwjgl-stb:${lwjgl_version}") - implementation("org.lwjgl:lwjgl-tinyfd:${lwjgl_version}") + implementation(platform("org.lwjgl:lwjgl-bom:${lwjgl_version}")) + implementation("org.lwjgl:lwjgl") + implementation("org.lwjgl:lwjgl-glfw") + implementation("org.lwjgl:lwjgl-opengl") + implementation("org.lwjgl:lwjgl-stb") + implementation("org.lwjgl:lwjgl-tinyfd") implementation("org.slf4j:slf4j-api:${slf4j_api_version}") implementation("net.sf.jopt-simple:jopt-simple:${jopt_simple_version}") testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiter_version}") - + testImplementation("org.assertj:assertj-core:${assertj_version}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${jupiter_version}") - testRuntimeOnly("org.slf4j:slf4j-jdk14:${slf4j_api_version}") - testRuntimeOnly("org.lwjgl:lwjgl::${lwjglNatives}") - testRuntimeOnly("org.lwjgl:lwjgl-glfw::${lwjglNatives}") - testRuntimeOnly("org.lwjgl:lwjgl-opengl::${lwjglNatives}") - testRuntimeOnly("org.lwjgl:lwjgl-stb::${lwjglNatives}") + testRuntimeOnly("org.lwjgl:lwjgl") { + artifact { + classifier = lwjglNatives + } + } + testRuntimeOnly("org.lwjgl:lwjgl-glfw") { + artifact { + classifier = lwjglNatives + } + } + testRuntimeOnly("org.lwjgl:lwjgl-opengl") { + artifact { + classifier = lwjglNatives + } + } + testRuntimeOnly("org.lwjgl:lwjgl-stb") { + artifact { + classifier = lwjglNatives + } + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 1e15ffc9b..52c77a545 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -47,11 +47,11 @@ import static org.lwjgl.opengl.GL32C.GL_TRUE; import java.awt.Desktop; -import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Locale; import java.util.Map; @@ -71,6 +71,7 @@ import net.neoforged.fml.earlydisplay.render.SimpleFont; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; import net.neoforged.fml.loading.FMLConfig; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.fml.loading.progress.ProgressMeter; @@ -110,8 +111,6 @@ public class DisplayWindow implements ImmediateWindowProvider { private int framecount; private ScheduledFuture rendererFuture; - 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 @@ -169,12 +168,11 @@ public Runnable initialize(String[] arguments) { LOGGER.warn("Failed to read dark-mode settings from options.txt", e); } } - this.theme = Theme.load(new File(""), darkMode); + this.theme = loadTheme(darkMode); this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED); var forgeVersion = parsed.valueOf(forgeversionopt); StartupNotificationManager.modLoaderConsumer().ifPresent(c -> c.accept("NeoForge loading " + forgeVersion)); - performanceInfo = new PerformanceInfo(); this.renderScheduler = Executors.newSingleThreadScheduledExecutor( Thread.ofPlatform().group(BACKGROUND_THREAD_GROUP) @@ -190,20 +188,40 @@ public Runnable initialize(String[] arguments) { initWindow(mcVersion); this.rendererFuture = renderScheduler.schedule(() -> new LoadingScreenRenderer(renderScheduler, window, theme, mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS); + + updateProgress("Initializing Game Graphics"); StartupNotificationManager.addModMessage("BLAHFASEL"); - while (true) { - try { - periodicTick(); - Thread.sleep(100L); - } catch (InterruptedException e) { - throw new RuntimeException(e); + var pp = StartupNotificationManager.prependProgressBar("Minecraft Progress", 1000); + pp.setAbsolute(250); + + return this::periodicTick; + } + + private static Theme loadTheme(boolean darkMode) { + Path themePath = getThemePath(darkMode); + Theme theme; + try { + theme = ThemeSerializer.load(themePath); + } catch (NoSuchFileException ignored) { + LOGGER.info("No theme found at {}", themePath); + theme = Theme.createDefaultTheme(darkMode); + if (Boolean.getBoolean("fml.writeMissingTheme")) { + ThemeSerializer.save(getThemePath(true), Theme.createDefaultTheme(true)); + ThemeSerializer.save(getThemePath(false), Theme.createDefaultTheme(false)); } + } catch (Exception e) { + LOGGER.error("Failed to load theme {}", themePath, e); + theme = Theme.createDefaultTheme(darkMode); } - //return this::periodicTick; + return theme; + } + + private static Path getThemePath(boolean darkMode) { + return FMLPaths.CONFIGDIR.get().resolve(darkMode ? "fml/theme_dark.json" : "fml/theme.json"); } // Called from NeoForge - public void render(int alpha) { + public void renderToFramebuffer() { if (rendererFuture.isDone()) { rendererFuture.resultNow().renderToFramebuffer(); } @@ -346,15 +364,14 @@ public void initWindow(@Nullable String mcVersion) { glfwSetWindowPos(window, (vidmode.width() - this.winWidth) / 2 + monitorX, (vidmode.height() - this.winHeight) / 2 + monitorY); // Attempt setting the icon - try (var glfwImgBuffer = GLFWImage.malloc(1)) { - try (GLFWImage glfwImages = GLFWImage.malloc()) { - var icon = theme.windowIcon(); - glfwImgBuffer.put(glfwImages.set(icon.width(), icon.height(), icon.imageData())); - glfwImgBuffer.flip(); - glfwSetWindowIcon(window, glfwImgBuffer); - } - } catch (NullPointerException e) { - LOGGER.error("Failed to load NeoForged icon"); + try (var glfwImgBuffer = GLFWImage.malloc(1); + var glfwImages = GLFWImage.malloc(); + var icon = theme.windowIcon().loadAsImage()) { + glfwImgBuffer.put(glfwImages.set(icon.width(), icon.height(), icon.imageData())); + glfwImgBuffer.flip(); + glfwSetWindowIcon(window, glfwImgBuffer); + } catch (Exception e) { + LOGGER.error("Failed to load NeoForged icon", e); } getLastGlfwError().ifPresent(error -> LOGGER.warn("Failed to set window icon: {}", error)); @@ -444,6 +461,8 @@ public long takeOverGlfwWindow() { Thread.currentThread().interrupt(); } + completeProgress(); + glfwMakeContextCurrent(window); // Set the title to what the game wants glfwSwapInterval(0); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/PerformanceInfo.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/PerformanceInfo.java deleted file mode 100644 index d86d38d16..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/PerformanceInfo.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.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("Memory: %d/%d MB (%.1f%%) %s", heapusage.getUsed() >> 20, heapusage.getMax() >> 20, memory * 100.0, cpuText); - } - - String text() { - return text; - } - - float memory() { - return memory; - } -} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java index 0c62b593a..1d5e86d31 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java @@ -5,6 +5,9 @@ package net.neoforged.fml.earlydisplay.render; +import static org.lwjgl.opengl.GL11C.GL_SCISSOR_BOX; +import static org.lwjgl.opengl.GL11C.GL_SCISSOR_TEST; +import static org.lwjgl.opengl.GL11C.glScissor; import static org.lwjgl.opengl.GL20C.glIsProgram; import static org.lwjgl.opengl.GL32C.GL_ACTIVE_TEXTURE; import static org.lwjgl.opengl.GL32C.GL_ARRAY_BUFFER; @@ -91,6 +94,10 @@ public final class GlState { private static int boundElementArrayBuffer; private static int boundArrayBuffer; + // Scissor test + private static boolean scissorEnabled; + private static int[] scissorBox = new int[4]; + /** * Private constructor to prevent instantiation of this utility class. */ @@ -142,6 +149,10 @@ public static void readFromOpenGL() { // Read buffer states boundElementArrayBuffer = glGetInteger(GL_ELEMENT_ARRAY_BUFFER_BINDING); boundArrayBuffer = glGetInteger(GL_ARRAY_BUFFER_BINDING); + + // Read scissor state + glGetIntegerv(GL_SCISSOR_BOX, scissorBox); + scissorEnabled = glIsEnabled(GL_SCISSOR_TEST); } /** @@ -325,6 +336,33 @@ public static void bindArrayBuffer(int bufferId) { } } + /** + * Configures the rectangle for the scissor test, which can be enabled or disabled by {@link #scissorTest}. + */ + public static void scissorBox(int x, int y, int width, int height) { + if (x != scissorBox[0] || y != scissorBox[1] || width != scissorBox[2] || height != scissorBox[3]) { + glScissor(x, y, width, height); + scissorBox[0] = x; + scissorBox[1] = y; + scissorBox[2] = width; + scissorBox[3] = height; + } + } + + /** + * Enables or disables the scissor test against the box defined by {@link #scissorBox}. + */ + public static void scissorTest(boolean enabled) { + if (enabled != scissorEnabled) { + if (enabled) { + glEnable(GL_SCISSOR_TEST); + } else { + glDisable(GL_SCISSOR_TEST); + } + scissorEnabled = enabled; + } + } + /** * A snapshot of the OpenGL state. */ @@ -337,7 +375,8 @@ public record StateSnapshot( int boundTexture2D, int activeTextureUnit, int boundVertexArray, int boundDrawFramebuffer, int boundReadFramebuffer, - int boundElementArrayBuffer, int boundArrayBuffer) {} + int boundElementArrayBuffer, int boundArrayBuffer, + boolean scissorEnabled, int[] scissorBox) {} /** * Creates a snapshot of the current OpenGL state. @@ -354,7 +393,8 @@ public static StateSnapshot createSnapshot() { boundTexture2D, activeTextureUnit, boundVertexArray, boundDrawFramebuffer, boundReadFramebuffer, - boundElementArrayBuffer, boundArrayBuffer); + boundElementArrayBuffer, boundArrayBuffer, + scissorEnabled, scissorBox); } /** @@ -385,5 +425,7 @@ public static void applySnapshot(StateSnapshot snapshot) { } bindElementArrayBuffer(snapshot.boundElementArrayBuffer); bindArrayBuffer(snapshot.boundArrayBuffer); + scissorTest(snapshot.scissorEnabled); + scissorBox(snapshot.scissorBox[0], snapshot.scissorBox[1], snapshot.scissorBox[2], snapshot.scissorBox[3]); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index ae219aeda..a013421f1 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -16,11 +16,8 @@ import static org.lwjgl.opengl.GL11C.glClear; import static org.lwjgl.opengl.GL11C.glGetString; -import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; @@ -28,6 +25,7 @@ import java.util.concurrent.TimeoutException; import net.neoforged.fml.earlydisplay.render.elements.ImageElement; import net.neoforged.fml.earlydisplay.render.elements.LabelElement; +import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; import net.neoforged.fml.earlydisplay.render.elements.RenderElement; import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; @@ -35,6 +33,7 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; import org.lwjgl.opengl.GL32C; @@ -45,7 +44,7 @@ public class LoadingScreenRenderer implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(LoadingScreenRenderer.class); private final long glfwWindow; - private final Theme theme; + private final MaterializedTheme theme; private final String mcVersion; private final String neoForgeVersion; @@ -56,15 +55,11 @@ public class LoadingScreenRenderer implements AutoCloseable { private final EarlyFramebuffer framebuffer; - private final ScheduledExecutorService scheduler; - private final Semaphore renderLock = new Semaphore(1); // Scheduled background rendering of the loading screen private final ScheduledFuture automaticRendering; - private final Map shaders; - private final Map fonts; private final List elements; private final SimpleBufferBuilder buffer = new SimpleBufferBuilder("shared", 8192); @@ -80,9 +75,7 @@ public LoadingScreenRenderer(ScheduledExecutorService scheduler, Theme theme, String mcVersion, String neoForgeVersion) { - this.scheduler = scheduler; this.glfwWindow = glfwWindow; - this.theme = theme; this.mcVersion = mcVersion; this.neoForgeVersion = neoForgeVersion; @@ -95,10 +88,9 @@ public LoadingScreenRenderer(ScheduledExecutorService scheduler, GlDebug.setCapabilities(capabilities); LOGGER.info("GL info: {} GL version {}, {}", glGetString(GL_RENDERER), glGetString(GL_VERSION), glGetString(GL_VENDOR)); - // Create shader resources - this.shaders = loadShaders(theme); - this.fonts = loadFonts(theme); - this.elements = loadElements(shaders, fonts, theme); + // Create GL resources + this.theme = MaterializedTheme.materialize(theme); + this.elements = loadElements(); // we always render to an 854x480 texture and then fit that to the screen framebuffer = new EarlyFramebuffer(854, 480); @@ -120,72 +112,58 @@ public LoadingScreenRenderer(ScheduledExecutorService scheduler, GlState.enableBlend(true); GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glfwMakeContextCurrent(0); - this.automaticRendering = scheduler.scheduleAtFixedRate(this::renderToScreen, 50, 50, TimeUnit.MILLISECONDS); + this.automaticRendering = scheduler.scheduleWithFixedDelay(this::renderToScreen, 50, 50, TimeUnit.MILLISECONDS); // TODO this.performanceTick = scheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS); // schedule a 50 ms ticker to try and smooth out the rendering - scheduler.scheduleAtFixedRate(() -> animationFrame++, 1, 50, TimeUnit.MILLISECONDS); - } - - private static Map loadShaders(Theme theme) { - var shaders = new HashMap(theme.shaders().size()); - for (var entry : theme.shaders().entrySet()) { - var shader = ElementShader.create( - entry.getKey(), - entry.getValue().vertexShader(), - entry.getValue().fragmentShader()); - shaders.put(entry.getKey(), shader); - } - return shaders; - } - - private static Map loadFonts(Theme theme) { - var fonts = new HashMap(theme.fonts().size()); - for (var entry : theme.fonts().entrySet()) { - try { - fonts.put(entry.getKey(), new SimpleFont(entry.getValue(), 1)); - } catch (IOException e) { - throw new RuntimeException("Failed to load font " + entry.getKey(), e); - } - } - return fonts; + scheduler.scheduleWithFixedDelay(() -> animationFrame++, 1, 50, TimeUnit.MILLISECONDS); } - private List loadElements(Map shaders, - Map fonts, - Theme theme) { - var elements = new ArrayList(theme.elements().size()); + private List loadElements() { + var themeElements = theme.theme().elements(); + var elements = new ArrayList(themeElements.size()); - for (var element : theme.elements()) { - elements.add(loadElement(theme, element)); + for (var element : themeElements) { + elements.add(loadElement(element)); } return elements; } - private RenderElement loadElement(Theme theme, ThemeElement element) { + private RenderElement loadElement(ThemeElement element) { var renderElement = switch (element) { - case ThemeImageElement imageElement -> new ImageElement(imageElement.id(), Texture.create(imageElement.texture())); + case ThemeImageElement imageElement -> new ImageElement(imageElement.id(), theme, Texture.create(imageElement.texture())); case ThemeStartupLogElement startupLogElement -> new StartupLogElement( startupLogElement.id(), - theme.colorScheme().text()); + theme, + theme.theme().colorScheme().text()); case ThemeLabelElement labelElement -> { var version = mcVersion + "-" + neoForgeVersion.split("-")[0]; yield new LabelElement( labelElement.id(), + theme, labelElement.text().replace("${version}", version)); } - case ThemeProgressBarsElement progressBarsElement -> new ProgressBarsElement(progressBarsElement.id(), theme); + case ThemeProgressBarsElement progressBarsElement -> new ProgressBarsElement( + progressBarsElement.id(), + theme, + progressBarsElement); - default -> throw new IllegalStateException("Unexpected theme element: " + element); + case ThemePerformanceElement performanceElement -> new PerformanceElement( + performanceElement.id(), + theme, + performanceElement); + + default -> throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); }; renderElement.setLeft(element.left()); renderElement.setTop(element.top()); renderElement.setRight(element.right()); renderElement.setBottom(element.bottom()); + renderElement.setMaintainAspectRatio(element.maintainAspectRatio()); return renderElement; } @@ -231,13 +209,13 @@ public void renderToScreen() { glfwGetFramebufferSize(glfwWindow, w, h); GlState.viewport(0, 0, w[0], h[0]); - framebuffer.blitToScreen(this.theme.colorScheme().background(), w[0], h[0]); + framebuffer.blitToScreen(this.theme.theme().colorScheme().background(), w[0], h[0]); // Swap buffers; we're done glfwSwapBuffers(glfwWindow); GlState.applySnapshot(backup); } catch (Throwable t) { - LOGGER.error("BARF", t); + LOGGER.error("Unexpected error while rendering the loading screen", t); } finally { if (this.automaticRendering != null) glfwMakeContextCurrent(0); // we release the gl context IF we're running off the main thread @@ -254,20 +232,20 @@ public void renderToFramebuffer() { framebuffer.activate(); // Clear the screen to our color - var background = theme.colorScheme().background(); + var background = theme.theme().colorScheme().background(); GlState.clearColor(background.r(), background.g(), background.b(), 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); GlState.enableBlend(true); GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - for (var shader : shaders.values()) { + for (var shader : theme.shaders().values()) { shader.activate(); if (shader.hasUniform(ElementShader.UNIFORM_SCREEN_SIZE)) { shader.setUniform2f(ElementShader.UNIFORM_SCREEN_SIZE, framebuffer.width(), framebuffer.height()); } } - var context = new RenderContext(buffer, fonts, shaders, framebuffer.width(), framebuffer.height(), animationFrame); + var context = new RenderContext(buffer, theme, framebuffer.width(), framebuffer.height(), animationFrame); for (var element : this.elements) { element.render(context); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java new file mode 100644 index 000000000..b05a80bf0 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java @@ -0,0 +1,66 @@ +package net.neoforged.fml.earlydisplay.render; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import net.neoforged.fml.earlydisplay.theme.Theme; + +/** + * A themes resources loaded for rendering at runtime. + */ +public record MaterializedTheme( + Theme theme, + Map fonts, + Map shaders) implements AutoCloseable { + public static MaterializedTheme materialize(Theme theme) { + return new MaterializedTheme( + theme, + loadFonts(theme), + loadShaders(theme)); + } + + private static Map loadShaders(Theme theme) { + var shaders = new HashMap(theme.shaders().size()); + for (var entry : theme.shaders().entrySet()) { + var shader = ElementShader.create( + entry.getKey(), + entry.getValue().vertexShader(), + entry.getValue().fragmentShader()); + shaders.put(entry.getKey(), shader); + } + return shaders; + } + + private static Map loadFonts(Theme theme) { + var fonts = new HashMap(theme.fonts().size()); + for (var entry : theme.fonts().entrySet()) { + try { + fonts.put(entry.getKey(), new SimpleFont(entry.getValue(), 1)); + } catch (IOException e) { + throw new RuntimeException("Failed to load font " + entry.getKey(), e); + } + } + return fonts; + } + + public SimpleFont getFont(String fontId) { + var font = fonts.getOrDefault(fontId, fonts.get(Theme.FONT_DEFAULT)); + if (font == null) { + throw new IllegalStateException("Theme does not contain a default font. Available fonts: " + fonts.keySet()); + } + return font; + } + + public ElementShader getShader(String shaderId) { + var shader = shaders.get(shaderId); + if (shader == null) { + throw new IllegalArgumentException("Missing shader definition in theme for " + shaderId); + } + return shader; + } + + @Override + public void close() { + shaders.values().forEach(ElementShader::close); + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java index a5495172a..caa6f26bb 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java @@ -5,7 +5,182 @@ package net.neoforged.fml.earlydisplay.render; +import net.neoforged.fml.earlydisplay.theme.TextureScaling; +import net.neoforged.fml.earlydisplay.util.Bounds; + public class QuadHelper { + public static void fillSprite(SimpleBufferBuilder buffer, + Texture texture, + float x, + float y, + float z, + float width, + float height, + int color, + SpriteFillDirection fillDirection, + int animationFrame) { + // Too large values for width / height cause immediate crashes of the VM due to graphics driver bugs< + // These maximum values are picked without too much thought. + width = Math.min(65535, width); + height = Math.min(65535, height); + + float u0 = 0, u1 = 1, v0 = 0, v1 = 1; + if (texture.animationMetadata() != null) { + int frameCount = texture.animationMetadata().frameCount(); + var frameHeight = texture.physicalHeight() / frameCount; + var vUnit = frameHeight / (float) texture.physicalHeight(); + v0 = (animationFrame % frameCount) * vUnit; + v1 = (animationFrame % frameCount + 1) * vUnit; + } + + switch (texture.scaling()) { + case TextureScaling.Tile tiled -> { + fillTiled(buffer, x, y, z, width, height, color, tiled.width(), tiled.height(), u0, u1, v0, v1, fillDirection); + } + case TextureScaling.Stretch stretch -> { + addQuad(buffer, x, y, z, width, height, color, u0, u1, v0, v1); + } + case TextureScaling.NineSlice nineSlice -> { + addTiledNineSlice(buffer, x, y, z, width, height, color, nineSlice, u0, u1, v0, v1); + } + default -> {} + } + } + + private static void addTiledNineSlice(SimpleBufferBuilder buffer, + float x, + float y, + float z, + float width, + float height, + int color, + TextureScaling.NineSlice nineSlice, + float u0, + float u1, + float v0, + float v1) { + var leftWidth = Math.min(nineSlice.left(), width / 2); + var rightWidth = Math.min(nineSlice.right(), width / 2); + var topHeight = Math.min(nineSlice.top(), height / 2); + var bottomHeight = Math.min(nineSlice.bottom(), height / 2); + var innerWidth = nineSlice.width() - nineSlice.left() - nineSlice.right(); + var innerHeight = nineSlice.height() - nineSlice.top() - nineSlice.bottom(); + + // The U/V values for the cuts through the nine-slice we'll use + var leftU = u0 + leftWidth / nineSlice.width() * (u1 - u0); + var rightU = u1 - rightWidth / nineSlice.width() * (u1 - u0); + var topV = v0 + topHeight / nineSlice.height() * (v1 - v0); + var bottomV = v1 - bottomHeight / nineSlice.height() * (v1 - v0); + + // Destination pixel values of the inner rectangle + var dstInnerLeft = x + leftWidth; + var dstInnerTop = y + topHeight; + var dstInnerRight = x + width - rightWidth; + var dstInnerBottom = y + height - bottomHeight; + var dstInnerWidth = dstInnerRight - dstInnerLeft; + var dstInnerHeight = dstInnerBottom - dstInnerTop; + + // Corners are always untiled, but may be cropped + addQuad(buffer, x, y, z, leftWidth, topHeight, color, u0, leftU, v0, topV); // Top left + addQuad(buffer, dstInnerRight, y, z, rightWidth, topHeight, color, rightU, u1, v0, topV); // Top right + addQuad(buffer, dstInnerRight, dstInnerBottom, z, rightWidth, bottomHeight, color, rightU, u1, bottomV, v1); // Bottom + // right + addQuad(buffer, x, dstInnerBottom, z, leftWidth, bottomHeight, color, u0, leftU, bottomV, v1); // Bottom left + + // The edges can be tiled + if (nineSlice.stretchHorizontalFill()) { + addQuad(buffer, dstInnerLeft, y, z, dstInnerWidth, topHeight, color, leftU, rightU, v0, topV); // Top Edge + addQuad(buffer, dstInnerLeft, dstInnerBottom, z, dstInnerWidth, bottomHeight, color, leftU, rightU, bottomV, v1); // Bottom Edge + } else { + fillTiled(buffer, dstInnerLeft, y, z, dstInnerWidth, topHeight, color, innerWidth, nineSlice.top(), leftU, rightU, v0, + topV); // Top Edge + fillTiled(buffer, dstInnerLeft, dstInnerBottom, z, dstInnerWidth, bottomHeight, color, innerWidth, nineSlice.bottom(), + leftU, rightU, bottomV, v1); // Bottom Edge + } + if (nineSlice.stretchVerticalFill()) { + addQuad(buffer, x, dstInnerTop, z, leftWidth, dstInnerHeight, color, u0, leftU, topV, bottomV); // Left Edge + addQuad(buffer, dstInnerRight, dstInnerTop, z, rightWidth, dstInnerHeight, color, rightU, u1, topV, bottomV); // Right Edge + } else { + fillTiled(buffer, x, dstInnerTop, z, leftWidth, dstInnerHeight, color, nineSlice.left(), innerHeight, u0, leftU, topV, + bottomV); // Left Edge + fillTiled(buffer, dstInnerRight, dstInnerTop, z, rightWidth, dstInnerHeight, color, nineSlice.right(), innerHeight, rightU, + u1, topV, bottomV); // Right Edge + } + + // The center is tiled too + if (nineSlice.stretchHorizontalFill() && nineSlice.stretchVerticalFill()) { + addQuad(buffer, dstInnerLeft, dstInnerTop, z, dstInnerWidth, dstInnerHeight, color, leftU, rightU, topV, bottomV); + } else if (nineSlice.stretchHorizontalFill()) { + fillTiled(buffer, dstInnerLeft, dstInnerTop, z, dstInnerWidth, dstInnerHeight, color, dstInnerWidth, innerHeight, leftU, + rightU, topV, bottomV); + } else if (nineSlice.stretchVerticalFill()) { + fillTiled(buffer, dstInnerLeft, dstInnerTop, z, dstInnerWidth, dstInnerHeight, color, innerWidth, dstInnerHeight, leftU, + rightU, topV, bottomV); + } else { + fillTiled(buffer, dstInnerLeft, dstInnerTop, z, dstInnerWidth, dstInnerHeight, color, innerWidth, innerHeight, leftU, + rightU, topV, bottomV); + } + } + + private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float destTileWidth, + float destTileHeight, float u0, float u1, float v0, float v1) { + fillTiled(buffer, x, y, z, width, height, color, destTileWidth, destTileHeight, u0, u1, v0, v1, + SpriteFillDirection.TOP_TO_BOTTOM); + } + + private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float destTileWidth, + float destTileHeight, float u0, float u1, float v0, float v1, SpriteFillDirection fillDirection) { + if (destTileWidth <= 0 || destTileHeight <= 0) { + return; + } + + var right = x + width; + var bottom = y + height; + + if (fillDirection == SpriteFillDirection.BOTTOM_TO_TOP) { + for (var cy = bottom; cy >= y; cy -= destTileHeight) { + // This handles not stretching the potentially partial last column + var tileHeight = Math.min(cy - y, destTileHeight); + var tileV0 = v1 - (v1 - v0) * tileHeight / destTileHeight; + + for (var cx = x; cx < right; cx += destTileWidth) { + // This handles not stretching the potentially partial last row + var tileWidth = Math.min(right - cx, destTileWidth); + var tileU1 = u0 + (u1 - u0) * tileWidth / destTileWidth; + + addQuad(buffer, cx, cy - tileHeight, z, tileWidth, tileHeight, color, u0, tileU1, tileV0, v1); + } + } + } else { + for (var cy = y; cy < bottom; cy += destTileHeight) { + // This handles not stretching the potentially partial last column + var tileHeight = Math.min(bottom - cy, destTileHeight); + var tileV1 = v0 + (v1 - v0) * tileHeight / destTileHeight; + + for (var cx = x; cx < right; cx += destTileWidth) { + // This handles not stretching the potentially partial last row + var tileWidth = Math.min(right - cx, destTileWidth); + var tileU1 = u0 + (u1 - u0) * tileWidth / destTileWidth; + + addQuad(buffer, cx, cy, z, tileWidth, tileHeight, color, u0, tileU1, v0, tileV1); + } + } + } + } + + public static void addQuad(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float minU, float maxU, + float minV, float maxV) { + if (width < 0 || height < 0) { + return; + } + + loadQuad(buffer, x, x + width, y, y + height, minU, maxU, minV, maxV, color); + } + + public static void loadQuad(SimpleBufferBuilder bb, Bounds bounds, float u0, float u1, float v0, float v1, int colour) { + loadQuad(bb, bounds.left(), bounds.right(), bounds.top(), bounds.bottom(), u0, u1, v0, v1, colour); + } + 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(); @@ -19,4 +194,9 @@ public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0 bb.pos(x0, y1).tex(u0, v1).endVertex(); bb.pos(x1, y1).tex(u1, v1).endVertex(); } + + public enum SpriteFillDirection { + TOP_TO_BOTTOM, + BOTTOM_TO_TOP + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java index c6bc4c078..0e296b34c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -3,29 +3,33 @@ import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; import java.util.List; -import java.util.Map; import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.util.Bounds; -import net.neoforged.fml.loading.progress.ProgressMeter; public record RenderContext( SimpleBufferBuilder sharedBuffer, - Map fonts, - Map shaders, + MaterializedTheme theme, float availableWidth, float availableHeight, int animationFrame) { - public ElementShader bindShader(String shaderId) { - var shader = shaders.get(shaderId); - if (shader == null) { - throw new IllegalArgumentException("Missing shader definition in theme for " + shaderId); - } + var shader = theme.getShader(shaderId); shader.activate(); return shader; } + public void blitTexture(Texture texture, Bounds bounds) { + blitTexture(texture, bounds, -1); + } + + public void blitTexture(Texture texture, Bounds bounds, int color) { + blitTexture(texture, bounds.left(), bounds.top(), bounds.width(), bounds.height(), color); + } + + public void blitTexture(Texture texture, float x, float y, float width, float height) { + blitTexture(texture, x, y, width, height, -1); + } + public void blitTexture(Texture texture, float x, float y, float width, float height, int color) { GlState.activeTexture(GL_TEXTURE0); GlState.bindTexture2D(texture.textureId()); @@ -35,87 +39,27 @@ public void blitTexture(Texture texture, float x, float y, float width, float he sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); - float u0 = 0, u1 = 1, v0 = 0, v1 = 1; - if (texture.animationMetadata() != null) { - int frameCount = texture.animationMetadata().frameCount(); - var frameHeight = texture.physicalHeight() / frameCount; - var vUnit = frameHeight / (float) texture.physicalHeight(); - v0 = (animationFrame % frameCount) * vUnit; - v1 = (animationFrame % frameCount + 1) * vUnit; - } - - QuadHelper.loadQuad(sharedBuffer, x, x + width, y, y + height, u0, u1, v0, v1, color); + QuadHelper.fillSprite( + sharedBuffer, + texture, + x, + y, + 0, + width, + height, + color, + QuadHelper.SpriteFillDirection.TOP_TO_BOTTOM, + animationFrame); sharedBuffer.draw(); } - public SimpleFont getFont(String fontId) { - var font = fonts.getOrDefault(fontId, fonts.get(Theme.FONT_DEFAULT)); - if (font == null) { - throw new IllegalStateException("Theme does not contain a default font. Available fonts: " + fonts.keySet()); - } - return font; - } - public void renderText(float x, float y, SimpleFont font, List texts) { GlState.activeTexture(GL_TEXTURE0); GlState.bindTexture2D(font.textureId()); - var shader = bindShader(Theme.SHADER_FONT); + bindShader(Theme.SHADER_FONT); sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); font.generateVerticesForTexts(x, y, sharedBuffer, texts); sharedBuffer.draw(); } - - public void renderProgressBar(Bounds bounds, int cnt, float alpha, SimpleFont font, ProgressMeter pm, ThemeColor background, ThemeColor foreground) { - if (pm.steps() == 0) { - progressBar(background, foreground, bounds, alpha, frame -> indeterminateBar(frame, cnt == 0)); - } else { - progressBar(background, foreground, bounds, alpha, f -> new float[] { 0f, pm.progress() }); - } - } - interface ColourFunction { - int colour(int frame); - } - - interface ProgressDisplay { - float[] progress(int frame); - } - - interface BarPosition { - int[] location(); - } - - public void progressBar(ThemeColor background, ThemeColor foreground, Bounds bounds, float alpha, ProgressDisplay progressDisplay) { - bindShader(Theme.SHADER_COLOR); - var progress = progressDisplay.progress(animationFrame); - sharedBuffer.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS); - var inset = 2; - var x0 = bounds.left(); - var x1 = bounds.right() + 4 * inset; - var y0 = bounds.top(); - var y1 = bounds.bottom(); - QuadHelper.loadQuad(sharedBuffer, x0, x1, y0, y1, 0f, 0f, 0f, 0f, foreground.withAlpha(alpha).toArgb()); - - x0 += inset; - x1 -= inset; - y0 += inset; - y1 -= inset; - QuadHelper.loadQuad(sharedBuffer, x0, x1, y0, y1, 0f, 0f, 0f, 0f, background.toArgb()); - - x1 = bounds.left() + inset + (int) (progress[1] * bounds.width()); - x0 += inset + progress[0] * bounds.width(); - y0 += inset; - y1 -= inset; - QuadHelper.loadQuad(sharedBuffer, x0, x1, y0, y1, 0f, 0f, 0f, 0f, -1 /* TODO */); - sharedBuffer.draw(); - } - - private static float[] indeterminateBar(int frame, boolean isActive) { - if (!isActive) { - return new float[] { 0f, 1f }; - } else { - var progress = frame % 100; - return new float[] { Math.clamp((progress - 2) / 100f, 0f, 1f), Math.clamp((progress + 2) / 100f, 0f, 1f) }; - } - } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java index 7c6c022db..5950bd74c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java @@ -11,6 +11,7 @@ import java.io.Closeable; import java.nio.ByteBuffer; import java.util.Arrays; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; import org.lwjgl.system.MemoryUtil; /** @@ -224,23 +225,13 @@ public SimpleBufferBuilder colour(float r, float g, float b, float a) { } /** - * @see ColourScheme.Colour#packedint(int) - * @param packedColor an ABGR packed int + * @param packedColor an ARGB packed int * @return the same buffer. + * @see ThemeColor#toArgb() */ 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; + var color = ThemeColor.ofArgb(packedColor); + return colour(color.r(), color.g(), color.b(), color.a()); } /** diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java index 0e8d81c29..275989060 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java @@ -33,14 +33,15 @@ import net.neoforged.fml.earlydisplay.theme.ThemeResource; import net.neoforged.fml.earlydisplay.util.Size; import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL32C; 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; -public class SimpleFont { - private final int textureId; +public class SimpleFont implements AutoCloseable { + private int textureId; private final int lineSpacing; private final int descent; private final int GLYPH_COUNT = 127 - 32; @@ -53,7 +54,6 @@ public Size measureText(CharSequence text) { var y = 0f; var codePoints = text.codePoints().iterator(); - while (codePoints.hasNext()) { int codePoint = codePoints.next(); switch (codePoint) { @@ -64,7 +64,7 @@ public Size measureText(CharSequence text) { } case '\t' -> x += glyphs[0].charwidth() * 4; default -> { - if (codePoint > ' ' && codePoint - ' ' < GLYPH_COUNT) { + if (codePoint >= ' ' && codePoint - ' ' < GLYPH_COUNT) { x += glyphs[codePoint - ' '].charwidth(); } } @@ -80,6 +80,14 @@ public Size measureText(CharSequence text) { return new Size(width, height); } + @Override + public void close() { + if (textureId != 0) { + GL32C.glDeleteTextures(textureId); + textureId = 0; + } + } + 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]; @@ -155,7 +163,7 @@ public SimpleFont(ThemeResource resource, int scale) throws IOException { } } - int lineSpacing() { + public int lineSpacing() { return lineSpacing; } @@ -163,7 +171,7 @@ int textureId() { return textureId; } - int descent() { + public int descent() { return descent; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java index 2b4525efa..87f5e293e 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -12,18 +12,20 @@ import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; +import net.neoforged.fml.earlydisplay.theme.TextureScaling; import net.neoforged.fml.earlydisplay.theme.ThemeTexture; import org.jetbrains.annotations.Nullable; import org.lwjgl.opengl.GL32C; public record Texture(int textureId, int physicalWidth, int physicalHeight, + TextureScaling scaling, @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { public int width() { - return physicalWidth; + return scaling.width(); } public int height() { - return animationMetadata != null ? physicalHeight / animationMetadata.frameCount() : physicalHeight; + return scaling.height(); } /** @@ -39,7 +41,7 @@ public static Texture create(ThemeTexture themeTexture) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.imageData()); GlState.activeTexture(GL_TEXTURE0); - return new Texture(texId, image.width(), image.height(), themeTexture.animation()); + return new Texture(texId, image.width(), image.height(), themeTexture.scaling(), themeTexture.animation()); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java index 16b70547e..5600bd6c4 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java @@ -1,13 +1,14 @@ package net.neoforged.fml.earlydisplay.render.elements; +import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.Texture; public class ImageElement extends RenderElement { private final Texture texture; - public ImageElement(String id, Texture texture) { - super(id); + public ImageElement(String id, MaterializedTheme theme, Texture texture) { + super(id, theme); this.texture = texture; } @@ -21,7 +22,7 @@ public void render(RenderContext context) { var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), texture.width(), texture.height()); - context.blitTexture(texture, bounds.left(), bounds.top(), bounds.width(), bounds.height(), color); + context.blitTexture(texture, bounds, color); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java index 1d3e3f4ec..829efacb3 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java @@ -1,17 +1,17 @@ package net.neoforged.fml.earlydisplay.render.elements; import java.util.List; +import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.earlydisplay.util.Size; public class LabelElement extends RenderElement { private final String text; - public LabelElement(String id, String text) { - super(id); + public LabelElement(String id, MaterializedTheme theme, String text) { + super(id, theme); this.text = text; } @@ -20,7 +20,6 @@ public void render(RenderContext context) { var texts = List.of( new SimpleFont.DisplayText(text, -1)); - var font = context.getFont(Theme.FONT_DEFAULT); var intrinsicSize = getIntrinsicSize(texts, font); var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), intrinsicSize.width(), intrinsicSize.height()); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java new file mode 100644 index 000000000..644439352 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java @@ -0,0 +1,125 @@ +package net.neoforged.fml.earlydisplay.render.elements; + +import com.sun.management.OperatingSystemMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import net.neoforged.fml.earlydisplay.render.GlState; +import net.neoforged.fml.earlydisplay.render.MaterializedTheme; +import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; +import net.neoforged.fml.earlydisplay.util.Bounds; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PerformanceElement extends RenderElement { + private static final Logger LOG = LoggerFactory.getLogger(PerformanceElement.class); + + private static final long REFRESH_AFTER_NANOS = TimeUnit.SECONDS.toNanos(1L); + private final OperatingSystemMXBean osBean; + private final MemoryMXBean memoryBean; + private Future performanceUpdateFuture; + private volatile PerformanceInfo currentPerformanceData; + + private final Texture barBackground; + private final Texture barForeground; + private final float[] lowColorHsb; + private final float[] highColorHsb; + + public PerformanceElement(String id, MaterializedTheme theme, ThemePerformanceElement element) { + super(id, theme); + + osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); + memoryBean = ManagementFactory.getMemoryMXBean(); + + performanceUpdateFuture = CompletableFuture.runAsync(this::updatePerformanceData); + + this.barBackground = Texture.create(element.barBackground()); + this.barForeground = Texture.create(element.barForeground()); + + this.lowColorHsb = element.lowColor().toHsb(); + this.highColorHsb = element.highColor().toHsb(); + } + + @Override + public void render(RenderContext context) { + var performanceData = currentPerformanceData; + + // Schedule an update if the performance data is outdated and there's no future running + var now = System.nanoTime(); + if (performanceData != null && performanceData.createdNanos + REFRESH_AFTER_NANOS < now && performanceUpdateFuture.isDone()) { + performanceUpdateFuture = CompletableFuture.runAsync(this::updatePerformanceData); + } else if (performanceData == null) { + return; // No data is available yet + } + + var areaBounds = resolveBounds(context.availableWidth(), context.availableHeight(), 250, 50); + float memoryBarFill = performanceData.memory(); + + final int colour = ThemeColor.ofHsb( + lowColorHsb[0] + (highColorHsb[0] - lowColorHsb[0]) * memoryBarFill, + lowColorHsb[1] + (highColorHsb[1] - lowColorHsb[1]) * memoryBarFill, + lowColorHsb[2] + (highColorHsb[2] - lowColorHsb[2]) * memoryBarFill).toArgb(); + + var barBounds = new Bounds( + areaBounds.left(), + areaBounds.top(), + areaBounds.right(), + areaBounds.top() + barBackground.height()); + context.blitTexture(barBackground, barBounds); + GlState.scissorTest(true); + memoryBarFill = 0.5f; + GlState.scissorBox( + (int) barBounds.left(), + (int) barBounds.top(), + (int) (barBounds.width() * memoryBarFill), + (int) barBounds.height()); + context.blitTexture(barForeground, barBounds, colour); + GlState.scissorTest(false); + + // Draw the detailed performance text centered below the progress bar + var textMeasurement = font.measureText(performanceData.text()); + context.renderText( + (int) (areaBounds.horizontalCenter() - textMeasurement.width() / 2), + barBounds.bottom(), + font, + List.of( + new SimpleFont.DisplayText( + performanceData.text(), + theme.theme().colorScheme().text().toArgb()))); + } + + @Override + public void close() { + super.close(); + performanceUpdateFuture.cancel(false); + } + + private void updatePerformanceData() { + try { + var heapusage = memoryBean.getHeapMemoryUsage(); + var memory = (float) heapusage.getUsed() / heapusage.getMax(); + var cpuLoad = osBean.getProcessCpuLoad(); + String cpuText; + if (cpuLoad == -1) { + cpuText = String.format(Locale.ROOT, "*CPU: %d%%", Math.round(osBean.getCpuLoad() * 100f)); + } else { + cpuText = String.format(Locale.ROOT, "CPU: %d%%", Math.round(cpuLoad * 100f)); + } + + var text = String.format(Locale.ROOT, "Memory: %d/%d MB (%d%%) %s", heapusage.getUsed() >> 20, heapusage.getMax() >> 20, Math.round(memory * 100.0), cpuText); + currentPerformanceData = new PerformanceInfo(System.nanoTime(), memory, text); + } catch (Exception e) { + LOG.error("Failed to update performance data.", e); + } + } + + private record PerformanceInfo(long createdNanos, float memory, String text) {} +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java index 44e971705..802a125e8 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java @@ -1,49 +1,104 @@ package net.neoforged.fml.earlydisplay.render.elements; import java.util.List; +import net.neoforged.fml.earlydisplay.render.GlState; +import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; +import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.loading.progress.StartupNotificationManager; public class ProgressBarsElement extends RenderElement { - private final Theme theme; + private static final int BAR_AREA_WIDTH = 400; + private static final int BAR_AREA_HEIGHT = 200; - public ProgressBarsElement(String id, Theme theme) { - super(id); - this.theme = theme; - } + private final ThemeProgressBarsElement themeElement; + private final Texture background; + private final Texture foreground; + private final Texture foregroundIndeterminate; - private static final int BAR_HEIGHT = 20; - private static final int BAR_WIDTH = 400; + public ProgressBarsElement(String id, + MaterializedTheme theme, + ThemeProgressBarsElement themeElement) { + super(id, theme); + this.background = Texture.create(themeElement.background()); + this.foreground = Texture.create(themeElement.foreground()); + this.foregroundIndeterminate = Texture.create(themeElement.foregroundIndeterminate()); + this.themeElement = themeElement; + } @Override public void render(RenderContext context) { - var font = context.getFont(Theme.FONT_DEFAULT); + var areaBounds = resolveBounds(context.availableWidth(), context.availableHeight(), BAR_AREA_WIDTH, BAR_AREA_HEIGHT); - var alpha = 0xFF; + float yOffset = 0; var barsShown = 0; for (var progress : StartupNotificationManager.getCurrentProgress()) { if (++barsShown > 2) { break; } - var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), BAR_WIDTH, BAR_HEIGHT); - - context.renderProgressBar( - bounds, - barsShown, - 1f, - font, - progress, - theme.colorScheme().background(), - theme.colorScheme().text()); - context.renderText( - bounds.left(), - bounds.bottom(), - font, - List.of( - new SimpleFont.DisplayText(progress.label().getText(), theme.colorScheme().text().toArgb()))); + String text = progress.label().getText(); + if (!text.isEmpty()) { + context.renderText( + areaBounds.left(), + areaBounds.top() + yOffset, + font, + List.of(new SimpleFont.DisplayText(text, theme.theme().colorScheme().text().toArgb()))); + yOffset += font.lineSpacing() + themeElement.labelGap(); + } + + var barBounds = new Bounds( + areaBounds.left(), + areaBounds.top() + yOffset, + areaBounds.right(), + areaBounds.top() + yOffset + background.height()); + context.blitTexture(background, barBounds); + + if (progress.steps() == 0) { + if (themeElement.indeterminateBounce()) { + // Indeterminate progress bars are rendered as a 20% piece that travels back and forth + var barX = 0; + var barWidth = (int) (barBounds.width() * 0.2f); + var availableSpace = (int) (barBounds.width() - barWidth); + if (availableSpace > 0) { + float f = (context.animationFrame() % 200) / 100.0f; + if (f > 1) { + f = 1 - (f - 1); + } + barX = (int) (f * availableSpace); + } + context.blitTexture( + foregroundIndeterminate, + barBounds.left() + barX, + barBounds.top(), + barWidth, + barBounds.height()); + } else { + // Indeterminate progress bars are rendered as a 20% piece that's scrolling left-to-right and then resets + var centerPercentage = (context.animationFrame() % 120) - 10; + var start = Math.clamp((centerPercentage - 10) / 100f, 0f, 1f); + var end = Math.clamp((centerPercentage + 10) / 100f, 0f, 1f); + context.blitTexture( + foregroundIndeterminate, + (int) (barBounds.left() + barBounds.width() * start), + barBounds.top(), + (int) (barBounds.width() * (end - start)), + barBounds.height()); + } + } else { + GlState.scissorTest(true); + GlState.scissorBox( + (int) barBounds.left(), + (int) barBounds.top(), + (int) (barBounds.width() * progress.progress()), + (int) barBounds.height()); + context.blitTexture(foreground, barBounds); + GlState.scissorTest(false); + } + yOffset += barBounds.height() + themeElement.barGap(); } } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java index cf1d619af..66626e771 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java @@ -5,22 +5,28 @@ package net.neoforged.fml.earlydisplay.render.elements; +import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.earlydisplay.util.StyleLength; public abstract class RenderElement implements AutoCloseable { - private int retireCount; - private final String id; + protected final MaterializedTheme theme; + private boolean maintainAspectRatio = true; private StyleLength left = StyleLength.ofUndefined(); private StyleLength top = StyleLength.ofUndefined(); private StyleLength right = StyleLength.ofUndefined(); private StyleLength bottom = StyleLength.ofUndefined(); + protected SimpleFont font; - public RenderElement(String id) { + public RenderElement(String id, MaterializedTheme theme) { this.id = id; + this.theme = theme; + this.font = theme.fonts().get(Theme.FONT_DEFAULT); } public String id() { @@ -43,11 +49,17 @@ public Bounds resolveBounds(float availableWidth, float availableHeight, float i // Handle aspect ratio if (widthDefined != heightDefined) { - float ar = intrinsicWidth / intrinsicHeight; - if (widthDefined) { - height = width / ar; - } else { - width = height * ar; + if (maintainAspectRatio) { + float ar = intrinsicWidth / intrinsicHeight; + if (widthDefined) { + height = width / ar; + } else { + width = height * ar; + } + } else if (widthDefined) { + height = intrinsicHeight; + } else if (heightDefined) { + width = intrinsicWidth; } } else if (!widthDefined && !heightDefined) { width = intrinsicWidth; @@ -75,10 +87,11 @@ public Bounds resolveBounds(float availableWidth, float availableHeight, float i return new Bounds(left, top, right, bottom); } - private static float resolve(StyleLength length, float availableSpace) { + private float resolve(StyleLength length, float availableSpace) { return switch (length.unit()) { case UNDEFINED -> Float.NaN; case POINT -> length.value(); + case REM -> length.value() * font.lineSpacing(); case PERCENT -> (length.value() * availableSpace) / 100.0f; }; } @@ -115,6 +128,14 @@ public void setBottom(StyleLength bottom) { this.bottom = bottom; } + public boolean maintainAspectRatio() { + return maintainAspectRatio; + } + + public void setMaintainAspectRatio(boolean maintainAspectRatio) { + this.maintainAspectRatio = maintainAspectRatio; + } + // // public void retire(final int frame) { // this.retireCount = frame; @@ -232,56 +253,6 @@ public static int clamp(int num, int min, int max) { } } - 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; - } - @Override public void close() {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java index 9c63219ed..712f34cde 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java @@ -2,9 +2,9 @@ import java.util.ArrayList; import java.util.List; +import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.earlydisplay.util.Size; @@ -14,8 +14,8 @@ public class StartupLogElement extends RenderElement { private ThemeColor textColor; - public StartupLogElement(String id, ThemeColor textColor) { - super(id); + public StartupLogElement(String id, MaterializedTheme theme, ThemeColor textColor) { + super(id, theme); this.textColor = textColor; } @@ -34,7 +34,6 @@ public void render(RenderContext context) { texts.add(new SimpleFont.DisplayText(msg.getText() + "\n", colour)); } - var font = context.getFont(Theme.FONT_DEFAULT); var intrinsicSize = getIntrinsicSize(texts, font); var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), intrinsicSize.width(), intrinsicSize.height()); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java index c45246d71..db141f566 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -26,6 +26,7 @@ static UncompressedImage loadImage(ThemeResource resource) { var decodedImage = STBImage.stbi_load_from_memory(buffer.buffer(), width, height, channels, 4); // TODO: Handle image decoding error return new UncompressedImage(resource.toString(), + resource, new NativeBuffer(decodedImage, STBImage::stbi_image_free), width[0], height[0]); @@ -52,6 +53,7 @@ private static UncompressedImage createBrokenImage() { var nativeBuffer = new NativeBuffer(pixelData, MemoryUtil::memFree); return new UncompressedImage( "broken texture", + null, nativeBuffer, BROKEN_TEXTURE_DIMENSIONS, BROKEN_TEXTURE_DIMENSIONS); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java index 68d3758cc..fdb17b69a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java @@ -18,6 +18,13 @@ public ByteBuffer buffer() { return buffer; } + public byte[] toByteArray() { + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + buffer.position(0); + return data; + } + @Override public void close() { if (deallocated.compareAndSet(false, true)) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java new file mode 100644 index 000000000..166a06a33 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java @@ -0,0 +1,21 @@ +package net.neoforged.fml.earlydisplay.theme; + +public sealed interface TextureScaling { + /** + * The intrinsic layout width of this image. + * This is required to support images that are larger in physical pixels for High DPI. + */ + int width(); + + /** + * The intrinsic layout height of this image. + * This is required to support images that are larger in physical pixels for High DPI. + */ + int height(); + + record Stretch(int width, int height) implements TextureScaling {} + + record Tile(int width, int height) implements TextureScaling {} + + record NineSlice(int width, int height, int left, int top, int right, int bottom, boolean stretchHorizontalFill, boolean stretchVerticalFill) implements TextureScaling {} +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index 54355fbdf..d86dfd669 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -1,64 +1,91 @@ package net.neoforged.fml.earlydisplay.theme; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.Properties; import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; import net.neoforged.fml.earlydisplay.util.StyleLength; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Defines a theme for the early display screen. */ public record Theme( - UncompressedImage windowIcon, + ThemeResource windowIcon, Map fonts, Map shaders, List elements, - ThemeColorScheme colorScheme) implements AutoCloseable { + ThemeColorScheme colorScheme) { - private static final Logger LOGGER = LoggerFactory.getLogger(Theme.class); public static final String FONT_DEFAULT = "default"; public static final String SHADER_GUI = "gui"; public static final String SHADER_FONT = "font"; public static final String SHADER_COLOR = "color"; public static Theme createDefaultTheme(boolean darkMode) { - var squir = new ThemeImageElement( - "squir", - new ThemeTexture(new ClasspathResource("/squirrel.png"))); + var squir = new ThemeImageElement(); + squir.setId("squir"); + squir.setTexture(new ThemeTexture(classpathResource("squirrel.png"), new TextureScaling.Stretch(112, 112))); - var startupLog = new ThemeStartupLogElement("startupLog"); + var startupLog = new ThemeStartupLogElement(); + startupLog.setId("startupLog"); startupLog.setLeft(StyleLength.ofPoints(10)); startupLog.setBottom(StyleLength.ofPoints(10)); - var fox = new ThemeImageElement( - "fox", - new ThemeTexture(new ClasspathResource("/fox_running.png"), new AnimationMetadata(28))); + var fox = new ThemeImageElement(); + fox.setId("fox"); + fox.setTexture( + new ThemeTexture( + classpathResource("fox_running.png"), + new TextureScaling.Stretch(151, 128), + new AnimationMetadata(28))); fox.setRight(StyleLength.ofPoints(10)); - fox.setBottom(StyleLength.ofPoints(10)); + fox.setBottom(StyleLength.ofREM(1)); - var forgeVersion = new ThemeLabelElement("version", "${version}"); + var forgeVersion = new ThemeLabelElement(); + forgeVersion.setId("version"); + forgeVersion.setText("${version}"); forgeVersion.setBottom(StyleLength.ofPoints(10)); forgeVersion.setRight(StyleLength.ofPoints(10)); - var progressBars = new ThemeProgressBarsElement("progressBars"); - progressBars.setRight(StyleLength.ofPoints(400)); + var barBackground = new ThemeTexture( + classpathResource("progress_bar_bg.png"), + new TextureScaling.NineSlice(40, 20, 2, 2, 2, 2, true, true)); + var barForeground = new ThemeTexture( + classpathResource("progress_bar_fg.png"), + new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)); + var barIndeterminate = new ThemeTexture( + classpathResource("progress_bar_fg.png"), + new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)); + + var progressBars = new ThemeProgressBarsElement(); + progressBars.setId("progressBars"); + progressBars.setBackground(barBackground); + progressBars.setForeground(barForeground); + progressBars.setForegroundIndeterminate(barIndeterminate); + progressBars.setLabelGap(4); + progressBars.setBarGap(5); + progressBars.setLeft(StyleLength.ofPoints(220)); + progressBars.setRight(StyleLength.ofPoints(220)); progressBars.setTop(StyleLength.ofPoints(250)); + progressBars.setMaintainAspectRatio(false); + + var performance = new ThemePerformanceElement(); + performance.setId("performance"); + performance.setBarBackground(barBackground); + performance.setBarForeground(barForeground); + performance.setLowColor(ThemeColor.ofBytes(0, 127, 0)); + performance.setHighColor(ThemeColor.ofBytes(255, 127, 0)); + performance.setLeft(StyleLength.ofPoints(220)); + performance.setRight(StyleLength.ofPoints(220)); + performance.setTop(StyleLength.ofPoints(10)); return new Theme( - ImageLoader.loadImage(new ClasspathResource("/neoforged_icon.png")), + classpathResource("neoforged_icon.png"), Map.of( - FONT_DEFAULT, new ClasspathResource("/Monocraft.ttf")), + FONT_DEFAULT, classpathResource("Monocraft.ttf")), Map.of( SHADER_GUI, ThemeShader.DEFAULT_GUI, @@ -66,26 +93,11 @@ FONT_DEFAULT, new ClasspathResource("/Monocraft.ttf")), ThemeShader.DEFAULT_FONT, SHADER_COLOR, ThemeShader.DEFAULT_COLOR), - List.of(squir, fox, startupLog, forgeVersion, progressBars), + List.of(squir, fox, startupLog, forgeVersion, progressBars, performance), ThemeColorScheme.DEFAULT); } - public static Theme load(File path, boolean darkMode) { - var properties = new Properties(); - try (var in = new BufferedInputStream(new FileInputStream(path))) { - properties.load(in); - // TODO: actually load custom theme - } catch (FileNotFoundException e) { - LOGGER.error("Failed to find theme {}", path); - } catch (IOException e) { - LOGGER.error("Failed to read loading window theme from {}", path, e); - } - - return createDefaultTheme(darkMode); - } - - @Override - public void close() { - windowIcon.close(); + private static ClasspathResource classpathResource(String name) { + return new ClasspathResource("net/neoforged/fml/earlydisplay/theme/" + name); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java index e16eb29c5..870961f2c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java @@ -1,6 +1,10 @@ package net.neoforged.fml.earlydisplay.theme; +import java.awt.Color; + public record ThemeColor(float r, float g, float b, float a) { + + public static final ThemeColor WHITE = new ThemeColor(1, 1, 1, 1); public static ThemeColor ofBytes(int r, int g, int b, int a) { return new ThemeColor(r / 255.f, g / 255.f, b / 255.f, a / 255.f); } @@ -9,14 +13,103 @@ public static ThemeColor ofBytes(int r, int g, int b) { return ofBytes(r, g, b, 255); } + public static ThemeColor ofArgb(int color) { + var aByte = (color >> 24) & 0xFF; + var rByte = (color >> 16) & 0xFF; + var gByte = (color >> 8) & 0xFF; + var bByte = (color) & 0xFF; + return ofBytes(rByte, gByte, bByte, aByte); + } + + public static ThemeColor ofRgb(int color) { + var rByte = (color >> 16) & 0xFF; + var gByte = (color >> 8) & 0xFF; + var bByte = (color) & 0xFF; + return ofBytes(rByte, gByte, bByte); + } + public ThemeColor withAlpha(float alpha) { return new ThemeColor(r, g, b, alpha); } public int toArgb() { return (((int) (a * 255)) & 0xFF) << 24 - | (((int) (b * 255)) & 0xFF) << 16 + | (((int) (r * 255)) & 0xFF) << 16 | (((int) (g * 255)) & 0xFF) << 8 - | (((int) (r * 255)) & 0xFF); + | (((int) (b * 255)) & 0xFF); + } + + public int rByte() { + return (int) (r * 256); + } + + public int gByte() { + return (int) (g * 256); + } + + public int bByte() { + return (int) (b * 256); + } + + public int aByte() { + return (int) (a * 256); + } + + 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 = Math.clamp((int) (f4 * 255.0F), 0, 255); + int k = Math.clamp((int) (f5 * 255.0F), 0, 255); + int l = Math.clamp((int) (f6 * 255.0F), 0, 255); + return 0xFF << 24 | j << 16 | k << 8 | l; + } + + public float[] toHsb() { + return Color.RGBtoHSB(rByte(), gByte(), bByte(), null); + } + + public static ThemeColor ofHsb(float h, float s, float b) { + return ofRgb(Color.HSBtoRGB(h, s, Math.clamp(b, 0, 1))); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java index 27f0a26c8..2df512cce 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java @@ -1,35 +1,7 @@ package net.neoforged.fml.earlydisplay.theme; -public class ThemeColorScheme { +public record ThemeColorScheme(ThemeColor background, ThemeColor text) { public static final ThemeColorScheme DEFAULT = new ThemeColorScheme( - new UnresolvedThemeColor(ThemeColor.ofBytes(239, 50, 61), ThemeColor.ofBytes(0, 0, 0)), - new UnresolvedThemeColor(ThemeColor.ofBytes(255, 255, 255))); - - private final UnresolvedThemeColor background; - - private final UnresolvedThemeColor text; - - private boolean darkMode; - - public ThemeColorScheme(UnresolvedThemeColor background, - UnresolvedThemeColor text) { - this.background = background; - this.text = text; - } - - public boolean darkMode() { - return darkMode; - } - - public void setDarkMode(boolean darkMode) { - this.darkMode = darkMode; - } - - public ThemeColor background() { - return background.resolve(darkMode); - } - - public ThemeColor text() { - return text.resolve(darkMode); - } + ThemeColor.ofBytes(239, 50, 61), + ThemeColor.ofBytes(255, 255, 255)); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java new file mode 100644 index 000000000..17d3de5f0 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java @@ -0,0 +1,290 @@ +package net.neoforged.fml.earlydisplay.theme; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; +import net.neoforged.fml.earlydisplay.util.StyleLength; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApiStatus.Internal +public final class ThemeSerializer { + private static final Logger LOG = LoggerFactory.getLogger(ThemeSerializer.class); + + private ThemeSerializer() {} + + public static Theme load(Path path) throws IOException { + try (var in = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + return createGson(path.toAbsolutePath().getParent()).fromJson(in, Theme.class); + } + } + + public static void save(Path path, Theme theme) { + LOG.info("Saving theme to {}", path); + try (var out = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { + createGson(path.toAbsolutePath().getParent()).toJson(theme, out); + } catch (IOException e) { + LOG.error("Failed to save theme to {}", path, e); + } + } + + private static Gson createGson(Path outputFolder) { + return new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(TextureScaling.class, new TextureScalingSerializer()) + .registerTypeAdapterFactory(new ThemeElementAdapterFactory()) + .registerTypeHierarchyAdapter(ThemeResource.class, new ThemeResourceAdapter(outputFolder)) + .registerTypeAdapter(UncompressedImage.class, new UncompressedImageSerializer()) + .registerTypeAdapter(StyleLength.class, new StyleLengthAdapter()) + .registerTypeAdapter(ThemeColor.class, new ThemeColorAdapter()) + .create(); + } + + private static class StyleLengthAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, StyleLength value) throws IOException { + switch (value.unit()) { + case UNDEFINED -> out.nullValue(); + case POINT -> out.value(value.value()); + case REM -> out.value(value.value() + "rem"); + case PERCENT -> out.value(value.value() + "%"); + } + } + + @Override + public StyleLength read(JsonReader in) throws IOException { + return switch (in.peek()) { + case NULL -> StyleLength.ofUndefined(); + case STRING -> { + var value = in.nextString(); + if (value.endsWith("%")) { + yield StyleLength.ofPercent(Float.parseFloat(value.substring(0, value.length() - 1))); + } else if (value.endsWith("rem")) { + yield StyleLength.ofREM(Float.parseFloat(value.substring(0, value.length() - 3))); + } else { + throw new JsonParseException("Unexpected value: " + value); + } + } + case NUMBER -> StyleLength.ofPoints((float) in.nextDouble()); + default -> throw new JsonParseException("Unexpected token type @ " + in.getPath()); + }; + } + } + + private static class UncompressedImageSerializer implements JsonSerializer, JsonDeserializer { + @Override + public UncompressedImage deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + var resource = (ThemeResource) context.deserialize(json, ThemeResource.class); + return resource.loadAsImage(); + } + + @Override + public JsonElement serialize(UncompressedImage value, Type typeOfSrc, JsonSerializationContext context) { + if (value.source() != null) { + return context.serialize(value.source()); + } + return JsonNull.INSTANCE; + } + } + + private static class ThemeResourceAdapter extends TypeAdapter { + private final Path themeFolder; + + public ThemeResourceAdapter(Path themeFolder) { + this.themeFolder = themeFolder; + } + + @Override + public void write(JsonWriter out, ThemeResource value) throws IOException { + switch (value) { + case ClasspathResource classpathResource -> { + var idx = Math.max( + classpathResource.path().lastIndexOf('/'), + classpathResource.path().lastIndexOf('\\')); + var filename = classpathResource.path().substring(idx + 1); + var diskPath = themeFolder.resolve(filename); + try (var buffer = value.toNativeBuffer()) { + Files.write(diskPath, buffer.toByteArray()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + out.value(filename); + } + case FileResource fileResource -> { + var diskPath = themeFolder.resolve(fileResource.file().getName()); + Files.copy(fileResource.file().toPath(), diskPath, StandardCopyOption.REPLACE_EXISTING); + out.value(fileResource.file().getName()); + } + } + } + + @Override + public ThemeResource read(JsonReader in) throws IOException { + var text = in.nextString(); + if (text.startsWith("classpath:")) { + return new ClasspathResource(text.substring("classpath:".length())); + } + return new FileResource(themeFolder.resolve(text).toFile()); + } + } + + private static class ThemeColorAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, ThemeColor value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + var hexColor = Integer.toHexString(value.toArgb()); + hexColor = "#" + "0".repeat(Math.max(0, 8 - hexColor.length())) + hexColor; + out.value(hexColor); + } + } + + @Override + public ThemeColor read(JsonReader in) throws IOException { + var text = in.nextString(); + if (!text.startsWith("#")) { + throw new JsonParseException("Cannot parse theme color value '" + text + "'"); + } + text = text.substring(1); + if (text.length() <= 6) { + return ThemeColor.ofRgb(Integer.parseUnsignedInt(text, 16)); + } else { + return ThemeColor.ofArgb(Integer.parseUnsignedInt(text, 16)); + } + } + } + + private static class TextureScalingSerializer implements JsonSerializer, JsonDeserializer { + @Override + public TextureScaling deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + var type = ((JsonObject) json).getAsJsonPrimitive("type").getAsString(); + return switch (type) { + case "nine_slice" -> context.deserialize(json, TextureScaling.NineSlice.class); + case "stretch" -> context.deserialize(json, TextureScaling.Stretch.class); + case "tile" -> context.deserialize(json, TextureScaling.Tile.class); + default -> throw new JsonParseException("Unknown image type " + type); + }; + } + + @Override + public JsonElement serialize(TextureScaling src, Type typeOfSrc, JsonSerializationContext context) { + var obj = new JsonObject(); + obj.addProperty("type", switch (src) { + case TextureScaling.NineSlice ignored -> "nine_slice"; + case TextureScaling.Stretch ignored -> "stretch"; + case TextureScaling.Tile ignored -> "tile"; + }); + for (var entry : ((JsonObject) context.serialize(src, src.getClass())).entrySet()) { + if ("type".equals(entry.getKey())) { + throw new IllegalStateException("Cannot serialize texture scaling with 'type' property"); + } + obj.add(entry.getKey(), entry.getValue()); + } + return obj; + } + } + + private static class ThemeElementAdapterFactory implements TypeAdapterFactory { + private static final Map> TYPE_MAP = Map.of( + "image", ThemeImageElement.class, + "label", ThemeLabelElement.class, + "performance", ThemePerformanceElement.class, + "progress", ThemeProgressBarsElement.class, + "startupLog", ThemeStartupLogElement.class); + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type == null) { + return null; + } + if (!ThemeElement.class.isAssignableFrom(type.getRawType())) { + return null; + } + + TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); + Map> labelToDelegate = new HashMap<>(); + Map, TypeAdapter> subtypeToDelegate = new HashMap<>(); + Map, String> subtypeToLabel = new HashMap<>(); + for (var entry : TYPE_MAP.entrySet()) { + var delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + subtypeToLabel.put(entry.getValue(), entry.getKey()); + } + + return (TypeAdapter) new TypeAdapter() { + @Override + public ThemeElement read(JsonReader in) throws IOException { + var jsonElement = jsonElementAdapter.read(in); + var labelJsonElement = jsonElement.getAsJsonObject().remove("type"); + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize theme element because it does not define a type field at " + in.getPath()); + } + + String label = labelJsonElement.getAsString(); + var delegate = labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException( + "unknown theme element type '" + label + "'. known types: " + labelToDelegate.keySet()); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override + public void write(JsonWriter out, ThemeElement value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + // The registration in this map guarantees the type bound of the key equals that of the value + var delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize theme element " + srcType.getName()); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + JsonObject clone = new JsonObject(); + + if (jsonObject.has("type")) { + throw new JsonParseException("theme element " + value + " must not define its own type field"); + } + clone.add("type", new JsonPrimitive(label)); + for (var e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + jsonElementAdapter.write(out, clone); + } + }.nullSafe(); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java index 0b5f5bfe1..3b6016387 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java @@ -2,9 +2,9 @@ import org.jetbrains.annotations.Nullable; -public record ThemeTexture(ThemeResource resource, @Nullable AnimationMetadata animation) { - public ThemeTexture(ThemeResource resource) { - this(resource, null); +public record ThemeTexture(ThemeResource resource, TextureScaling scaling, @Nullable AnimationMetadata animation) { + public ThemeTexture(ThemeResource resource, TextureScaling scaling) { + this(resource, scaling, null); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java index 1bce4df22..87186675e 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java @@ -1,11 +1,16 @@ package net.neoforged.fml.earlydisplay.theme; import java.nio.ByteBuffer; +import org.jetbrains.annotations.Nullable; /** * Image data loaded into memory and decompressed. */ -public record UncompressedImage(String name, NativeBuffer nativeImageData, int width, +public record UncompressedImage( + String name, + @Nullable ThemeResource source, + NativeBuffer nativeImageData, + int width, int height) implements AutoCloseable { public ByteBuffer imageData() { return nativeImageData.buffer(); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java deleted file mode 100644 index 55174cc27..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UnresolvedThemeColor.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.neoforged.fml.earlydisplay.theme; - -import org.jetbrains.annotations.Nullable; - -public record UnresolvedThemeColor(ThemeColor lightBackground, @Nullable ThemeColor darkBackground) { - public UnresolvedThemeColor(ThemeColor lightBackground) { - this(lightBackground, null); - } - - public ThemeColor resolve(boolean darkMode) { - return darkMode && darkBackground != null ? darkBackground : lightBackground; - } -} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java index e0b7dec4e..a6610c55f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -1,29 +1,31 @@ package net.neoforged.fml.earlydisplay.theme.elements; +import java.util.Objects; import net.neoforged.fml.earlydisplay.util.StyleLength; public abstract class ThemeElement { - private final String id; + private String id; + private boolean maintainAspectRatio = true; private StyleLength left = StyleLength.ofUndefined(); private StyleLength top = StyleLength.ofUndefined(); private StyleLength right = StyleLength.ofUndefined(); private StyleLength bottom = StyleLength.ofUndefined(); - public ThemeElement(String id) { - this.id = id; - } - public String id() { return id; } + public void setId(String id) { + this.id = id; + } + public StyleLength left() { return left; } public void setLeft(StyleLength left) { - this.left = left; + this.left = Objects.requireNonNull(left); } public StyleLength top() { @@ -31,7 +33,7 @@ public StyleLength top() { } public void setTop(StyleLength top) { - this.top = top; + this.top = Objects.requireNonNull(top); } public StyleLength right() { @@ -39,7 +41,7 @@ public StyleLength right() { } public void setRight(StyleLength right) { - this.right = right; + this.right = Objects.requireNonNull(right); } public StyleLength bottom() { @@ -47,7 +49,15 @@ public StyleLength bottom() { } public void setBottom(StyleLength bottom) { - this.bottom = bottom; + this.bottom = Objects.requireNonNull(bottom); + } + + public boolean maintainAspectRatio() { + return maintainAspectRatio; + } + + public void setMaintainAspectRatio(boolean maintainAspectRatio) { + this.maintainAspectRatio = maintainAspectRatio; } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java index 7560efcaf..f2495b348 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java @@ -3,14 +3,13 @@ import net.neoforged.fml.earlydisplay.theme.ThemeTexture; public class ThemeImageElement extends ThemeElement { - private final ThemeTexture texture; - - public ThemeImageElement(String id, ThemeTexture texture) { - super(id); - this.texture = texture; - } + private ThemeTexture texture; public ThemeTexture texture() { return texture; } + + public void setTexture(ThemeTexture texture) { + this.texture = texture; + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java index c18975dce..75e1ade17 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java @@ -1,14 +1,15 @@ package net.neoforged.fml.earlydisplay.theme.elements; -public class ThemeLabelElement extends ThemeElement { - private final String text; +import java.util.Objects; - public ThemeLabelElement(String id, String text) { - super(id); - this.text = text; - } +public class ThemeLabelElement extends ThemeElement { + private String text = ""; public String text() { return text; } + + public void setText(String text) { + this.text = Objects.requireNonNull(text); + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java new file mode 100644 index 000000000..0417206af --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java @@ -0,0 +1,58 @@ +package net.neoforged.fml.earlydisplay.theme.elements; + +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.theme.ThemeTexture; + +public class ThemePerformanceElement extends ThemeElement { + /** + * The background image being rendered as the base for a progress bar. + */ + private ThemeTexture barBackground; + /** + * The image that will be rendered on top of the background for progress bars that are being filled normally + * from the left. + */ + private ThemeTexture barForeground; + /** + * The color to use for coloring the bar when resource usage is low. + * The actual color will be interpolated between this and {@code highColor}. + */ + private ThemeColor lowColor; + /** + * The color to use for coloring the bar when resource usage is high. + * The actual color will be interpolated between this and {@code highColor}. + */ + private ThemeColor highColor; + + public ThemeTexture barBackground() { + return barBackground; + } + + public void setBarBackground(ThemeTexture barBackground) { + this.barBackground = barBackground; + } + + public ThemeTexture barForeground() { + return barForeground; + } + + public void setBarForeground(ThemeTexture barForeground) { + this.barForeground = barForeground; + } + + public ThemeColor lowColor() { + return lowColor; + } + + public void setLowColor(ThemeColor lowColor) { + this.lowColor = lowColor; + } + + public ThemeColor highColor() { + return highColor; + } + + public void setHighColor(ThemeColor highColor) { + this.highColor = highColor; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java index 8b29b2783..83201d3a0 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java @@ -1,7 +1,84 @@ package net.neoforged.fml.earlydisplay.theme.elements; +import net.neoforged.fml.earlydisplay.theme.ThemeTexture; + public class ThemeProgressBarsElement extends ThemeElement { - public ThemeProgressBarsElement(String id) { - super(id); + /** + * The background image being rendered as the base for a progress bar. + */ + private ThemeTexture background; + /** + * The image that will be rendered on top of the background for progress bars that are being filled normally + * from the left. + */ + private ThemeTexture foreground; + /** + * The image that will be rendered on top of the background for progress bars that are actively animating + * as an indeterminate progress bar. + */ + private ThemeTexture foregroundIndeterminate; + + /** + * The gap in virtual pixels between a bars label and the bar itself. + */ + private int labelGap; + + /** + * The gap in virtual pixels between a bar and the next label or bar. + */ + private int barGap; + + /** + * Makes the indeterminate progress bars bounce back and forth instead of trying to + * emulate an infinite scroll, which doesn't work that well with more complex progress bars. + */ + private boolean indeterminateBounce; + + public ThemeTexture background() { + return background; + } + + public void setBackground(ThemeTexture background) { + this.background = background; + } + + public ThemeTexture foreground() { + return foreground; + } + + public void setForeground(ThemeTexture foreground) { + this.foreground = foreground; + } + + public ThemeTexture foregroundIndeterminate() { + return foregroundIndeterminate; + } + + public void setForegroundIndeterminate(ThemeTexture foregroundIndeterminate) { + this.foregroundIndeterminate = foregroundIndeterminate; + } + + public int labelGap() { + return labelGap; + } + + public void setLabelGap(int labelGap) { + this.labelGap = labelGap; + } + + public int barGap() { + return barGap; + } + + public void setBarGap(int barGap) { + this.barGap = barGap; + } + + public boolean indeterminateBounce() { + return indeterminateBounce; + } + + public void setIndeterminateBounce(boolean indeterminateBounce) { + this.indeterminateBounce = indeterminateBounce; } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java index 9ef2504d0..557db9ec6 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java @@ -1,7 +1,3 @@ package net.neoforged.fml.earlydisplay.theme.elements; -public class ThemeStartupLogElement extends ThemeElement { - public ThemeStartupLogElement(String id) { - super(id); - } -} +public class ThemeStartupLogElement extends ThemeElement {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java index 0a50fed8a..030d683a1 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java @@ -13,6 +13,10 @@ public float height() { return bottom - top; } + public float horizontalCenter() { + return (right + left) / 2; + } + public Bounds union(Bounds other) { return new Bounds( Math.min(left, other.left), @@ -20,4 +24,12 @@ public Bounds union(Bounds other) { Math.max(right, other.right), Math.max(bottom, other.bottom)); } + + public Bounds deflate(float inset) { + return new Bounds( + left + inset, + top + inset, + right - inset, + bottom - inset); + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java index b33a9da46..15b27d5ef 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java @@ -29,6 +29,13 @@ public static StyleLength ofPoints(float points) { return new StyleLength(Unit.POINT, points); } + public static StyleLength ofREM(float rem) { + if (Float.isNaN(rem)) { + return ofUndefined(); + } + return new StyleLength(Unit.REM, rem); + } + public static StyleLength ofPercent(float percent) { if (Float.isNaN(percent)) { return ofUndefined(); @@ -44,9 +51,20 @@ public float value() { return value; } + @Override + public String toString() { + return switch (unit) { + case UNDEFINED -> "undefined"; + case POINT -> String.valueOf(value); + case REM -> value + "rem"; + case PERCENT -> value + "%"; + }; + } + public enum Unit { UNDEFINED, POINT, + REM, PERCENT } } diff --git a/earlydisplay/src/main/resources/fox_running.png b/earlydisplay/src/main/resources/fox_running.png deleted file mode 100644 index f9cb15382e56f4f5ca3acb3d1cc9188d6696f41d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127745 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͠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

aqJ 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 H`;q?xJ@PKt literal 0 HcmV?d00001 diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/fox_running.png b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/fox_running.png new file mode 100644 index 0000000000000000000000000000000000000000..4414e05ac33c47c963045cf02885dcf946630b24 GIT binary patch literal 86507 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

R~imvR2XzZ^O; zhDmKYJ9X<(cup`p4XV}Cu9ikyR*2$>Bem^6Dc#x%ISY*GCU_#4-YQs?u7hWa5Gv4!MZFiSwx+und^Wjc(Pqn8@bTMMJ=3 zr;#gm4hBaYy@}m&t&}od~}y3yZBX*zPI-#Qn9pYqC5DST}ivEM;K{JjfMozs%O; z(`QDBcPGdKbFi;_36qyhjz1S$yY9l%t`nMNE=wkm9j?VZ*$M_kJ|`7=9pY04brz% zVov_#kz-ppS5$BGEyr^oZRC;{?n)@+#5Nt2ZmRCAt2P%xYp=xOb%ka)Tq(7do8aVc z2G3K`FAz{;YXP)Tln<&N2JM{m7)7R2ivKnge9o9ze>KwNTkPDfFmzY;b?W+HdX<)p zl>DM(G%vtYC+`l^n}UD}9smbK3qm?1%$XGXKfk5iVi}q`nc3x)AQ%c~AHqa7T{)e} z%JuK3ItRVT27<^aK+QfJ#bnk5RnTm72~lg40#`xxT>&b!W`X4DefI z6Q$t6FM&Y*;qf@+m3_wMkjLNax3-=S+bz4H&QtQ*9$l9aez!!jVazOA+R_Pnk`IH7 zqHY+3-Hk{a6-IsI7>{s(}C(?hFHy50M6W^y;0l`l63T6y+>Ia-7 zSCGmVc1vtDYZAD(*VmZbZz6zK?*zm;TY~28}s2 zkOuiLS0c((-(SH_#+>b|b2Dm(oP=#}hfb{k?nZ7qo*}J4nGQV|u7z*%=i0wMq?U8N z?>cPGBx#(npA4d&->j`v{Rce_F62=1MV^)ktW5^Hgt@!K+SXA|doGt!jVa0lF%|At z_GV76t)iaASn}LdB#AI9^BbBcUAlIc6#k0>`u}ht#(zIR zhn?p#RT?aIR_@)zubg@p6}34Cp;)CkFEq!|XwG_uYy+xs+48H>T!j#-NxX51^$TGW zYn3!h@0Tkx#H-TrSfqKyCE02JdeF}SpPrx|r$*k>c%03jU2=w|3Ul}H20k1n6i{}m z2ahsnKf1mEP{QF9A62z#vo*^T%N1U4BmU$$cj#+cLaMX~BPo_465;`eSod3a12s2o z)%>jY7PTx3V9%uEcqM+j5fv%*C_kFCaz1KZMyK{W2FID*IQ$QC%w*B~Bb(7hj2c*# z21>1s>yQcT)$><2TZv&}HCpZzQwSA{4lV6cvB3gk=WB|IirGBZ505FV*XT+r)fWQS zlOhr?ZqvSsXV*Rq!dxemxVxqog$1D?%S4!PIFhtzywvI~q3ys|PU#!QpBMP*>TwtA z-l5Ka3-W5OS8kJIQE%9G{tBk0QKYDSeZ)hu;hD8Bgn_A~sNlLZT7K|=yTr{pj#M+I z!x8v!Rdt)mx^f&7UM}6sxHg-Nf+1IkZ*|~iI-skt5J--$|CLMyvV5Azn!J{iQ5UUT zm%9-(U6fgyE4rgd&vK@MZtkGMCY#)n0+9D^+nTV_zmVTwbf&SIpzJ^p=cX1tP=mrM zy?EucIGU}WW9daL1Z5Ipi4UITZeEzgp@90qVrwmhBJKwFO37%Ss78ZCSsoI`k&x_9 zxZjyC`z^R3W996b^d`>E)5aVM0Jc*ko77Mcq2$==Z%5cnb&AA_xDjl=KFn+FP>_zh zz+&o`c;{&yJ4p>Hdkf^5V~wORA=6F&0Z}S3^)3h*kJpIDbLti+?osj7j2JE5Pk^>a zVUuyIvJl19i<6gpPG%j?W#kE!ZP6T_t^9<`cae;*M(=3HX0BerQQ=qgugIPOjc6vv zYjvuMp1q~$gW}@J%~gN4)q$3g+9IBIc*paN!X%*iu`X!W}Iau;{X`?6PnGFTZeRzU5>V-aJd#FfOcJjYPObW5XES1=@NLwqf=pxGu(X2% zNh_d!8mhwY?T_-&dJD5Ask>ot_fB|BjO3|jr`qX3-BGi!LvDHh2eABaTD66br5cO- zuCeg9E$&Uy78P_$0*W3(7u`{S)k*(4F7CYGqiW*znze6Y34lhiNCLl=5;Xe4wl!`9 z)Q7toNENoluHMb5+j_Yl?;;7A_KWL;bEXr_X+f$>k>U)2CMNbv6Yv4yD4W^y`-i7U z-j;g`#`CAEMXn`e0sE3d-V)=N>JRTb{)$;c?=y%ocR}nFaHhWL?I(1emRjH81mY{A zEH%a+d@P9N{H+&bf#&_Cg`A`cW5US*T`rq-lay4DX!HP-)W(KKkuWp9sQ?5 zB%R+nCeO=8*pN0M!YA)+0<^s2G!ZR*^Ot$AHoD|7z|*-GVg`P_gHTcA9%{DrYbWH> zaOkDiA3Q4S?~WJMNtnsmT_QkALRZUXzj~k4O!hxTi3jbfr50jTqXzhL%M!X06(N(G~;g)CS%Sj>MrcWC% z>nD`K6k6Qjl7T@NUuF+Po5L_vXyGtY^9BymVp^Fkp!-EGdSl#ejAC4UHFwoU)SYZn zj%<9`a+4$kB6!glt0;%=%Ls}tB|?op>%dz!}b!V1&^ zI`Ae$t$tK@-st`HA>GcjP;=E;t`fpp&O;vy^nZm*NYY;iNy1H!zlrB(#^LEDlyXIu zhXN={UHs&OXLnAG)|X~d;y#Z^$S5aY?eC-|*C)u-u(MM6sj$K~EVdeg#(r@{Xfux? zU)f%`>?MmV-5uaRE)`oiyc{jp1vAF;$*YCU+GOa|P`d)ehV45#qNF$_Ru8P^N8>SQ zK%N}W4;Elh$Ixxbsh3{_BCuHfp$-VZMj;xu4D; zsEY6^t^KxD4WQ!H*!PH@&vu|$a!Y-Qk6;`jS%>a%=zKuN`j*wv-o0-1l(Fw4mzm6n z)lIops`M0d`W)j^vijb`cE$acOU;bn6#tYt_^;7K)Q2@s9`xEmwhQ!iP`HSU0duX( zFp&9v5_?}yzrZu&H+$4k%ScEbu9v;MH59N)&y=_NEB@D^u=tL?iXIo-uKEWOtu*z} zIHV~&&B}~{#>bp!Dn`o?i>u3Wl!G)Bi!Yv#n&C}J(8njAP`q{ zUO%>r_lk2)t{rrqdxQmAbkYP0Q&z+yA|sggl+i~=*RTk>MoN1s_IGD7-oevO)EoxC zOpo85k2LDqT z_o7D-5sxBxd2TjD{pWRWk!U{bMMn|WoME(L|uES zQ!TqMY6y0?nOlLzxGA{MW>VgHv^zyC1LbyT7O&svt|yzfC(5uUrT3p6|7)8u7gpg@ z(>(o@wb?m4GZ$@Rx4@}6*w#OEnr8I8CV}zGAzoc&>g_O5?A;p07_DAwRKv%EX(P~? z@upPo$KXp$C@kwuqC+m&K!y+^V9)E;K{aXG$?qt=NplNy6@ZrTZ^^TpU@L^cspBh>mjgT$wfwE%nSz`wI)(0;Zc(xKJ z<3w1zqDxm^H=dkzZ}-mC2?Kg7InOxTei_Y zu?>D*3T7Q&4`SDRJ}f7$H{I^kb}AUCPR?koZvb_>Gy2TaWaZyT4HwoNKQ?zFt^nZ_2X0rGhM#YG5c>RG%cj+hog`*EW;g zR36Z;x%+(FdF*|;UnQWyZ@E1_vCc>Hr5fcgo|;K9%=v(1|BC-9Q6s9l-R8cP-`%?A z={eWC{5rJN+kbvyM+R}IZHQnO*UGma7qg1X^!~z6ZZ@qrFKqqDU`yVI&hpo@uIKtF zjc1|&+ZcI_C+r-fw8wM?g(dIy#y?u=_{T%%NFV7sRwvIquDzVtVFp_eY)gLmxV4Z*4JM~(4r~aj zOT4zw$b8adAD_KbDq5ORS$}50xbI^qJ9}iFVoxev+Xw~?O64FUokZqr=TV%LmhwIM z0zc78Cu?tMVac_%1GBe}mpV2npUkYhaX?9cmqy_F?eP1ML*V$T^z&8)RCpsHMl#YY z`eB3|8CP3D@I>1(qR62V`zwF2%M%?FH}%KY?bm1E)%GJ)Bl#UwSim?X0i`94=Nej& zaghEHf%qYe?({ZPzkMXvnv5NqV6Zy4t>&I<%+%?*Sc<#8I}$M6qJPlI+Ih5%+xt;w z+_ytKc-=7=Tw2kOlBj58(cZZdQe5B{$|MnrR7w1x_Tz;!inEnlO1V}ae8LR(94O28 zD+GhlF5CSn@Ir9cv8}%~{VWE!XXtu||BxuiP-#oezM{7F-_8hfeVj<=?tPSOoE(@6$#fLVLGs!Xe&% za*!ZGy@NZs1{~0@;BJu?kyT;^vv*&#OIAe4Th)Jr53o`Y3D{R%he%ednxltoXhF5? z(XTQ~rg|s^N9vCr9I(d~x?4FGZBK2QozLzWUs_&E$KEqL$nXQYtq_Hn+BjuVSrcD% zgRDcCpxMs-wWsc~IdrV+=&IxD5F+k6YwY#6j@`gzi+KiPpKxU1=geSS$gVn(Ku`|fgvpJYjc`TC626a$Q{2GGk=4YOaj$ZHEPY+)*Hi})n+I9 zN=9zbJ93MEgLgK~9Yqgjuh-9U%MHB5k`SfVu!pCJp1s`4@ zuX)gKm5->NedX59{C{KQ9v%;$xzF z#I8-(N9iI^JAVPzLLAawTPq*5Q30M%cvNi`CivL1BY94vzMw2`AF@TME6B=|pG04rr)`#9T$JDP|d>$h%m zqKdx5J{|&=)3#yfy-Zd}zw8~im++sL(ll539fI>lyqAA@%RW4%qbTn<=-oJ%8@`}a6H+1(?)8-2*u{g&c4iYb}ZWqMfDl?*1pn-wiMTa(vyZ9!G9zEU9vUhaL$JDzz`evIP+ibfNWJ= z-2hBjy`pumO0TG)5|ryVOOL0{z1fQIAI&_}{Y&*zf<=hJmMk_%aAAhI%G zhQpU&h{-4l-SSXpBXNCO-`^!@qw+K@q2;7|`5Q)+loTLWa~iZ?GAwU)%@iu}IjyJ?%@eD}5NM|pw6pC`9@4p}r!SSZ>XF(CV? zd!|kr=>f)8P|#Q7-(oj|dRGHr3tvhcN!>wHDmub>Daq zhG$T%*j2`;Ar=bsnnZ9nLpe-z9J_q5t|wn*(3qY6jQm=^d~G7ukI7KJG@o z<{aByv%bQzFuRc0homwKKK+(8hqPQ&kcn9`BUAd`8ON|Jn-aZE$VP@CPNAOxvvjM> zyMMZ+7IqGlptLlq3mG#s*iOo69AnjeDy1}Pz}{5VboVvs^29cQO;cO7RMR4xYO?}M z?=?f@K2tbC|I^3x{P|8}m9q7Sgydn3zI0}!W))_MVmKZA*b^#0*+4eEsTEFi9g;&s zpgk#buEvuXqc-m^4TY#ZU+gAdqBTC7s7|Tc!h9ZgauMdkxz;_F>R8!yz6bNQ<*dRg zF9@<-(aVjbM5Vrg;a=-RhJ^Sj8soI+~p$uAcdmhq#BxX0 zHB0;D3YIUptN2RBut8Fb!nMSLN(A`@QIp`;($-Y1X~dUxGk2AUoDT#6yic14YOHAp z_KrZEIZh8ljHAV^w!b4qQVVgFdTR0N@^$sOs3Hhuz7#%vT)OT^qdj5zZ@dZYDLL5lkBRwXbTQK;C7!1*1j#}RZbqo zFPM1WC9s>g8?SR8v2p6OBr6C$3f*rRO8CHBq=xu34^{#$iF;-uDAm!7tt#m>=2KDM z`8QP07?qlZB@SIf7CRHFY26l)DsCgQj?%0<{f3qX2r@V(obqvNp}{6&UGaa>_Ete{ zcv0K$zd{8{iv@}ow*W-~#fwXDmjH$0!HPRAMT2_@R)U1!?ouJRG+2=0?!`6maW3AQ zGxMFR_vA9NCo{?3v(|pr^LuFPZU)X{@Y5x%Cz5(&Zw~nIEP_J9nV~$-X)(uA7vBCs zJICfzR6|rOy&xS)H+S#Gv?l6r3zBmw^%acwCRlHpkUv5G0lVBJ@Z1r9pAvT@E2)5- zw&nbI)A4%d8?21h$M)(1->2>Z;qhpN29E9VUS6;z-@gt(7)vl9V=g~%et3|D{}ZNc@?hofR>ae zFr-n~@%fZGfAIJySy+l6=G(}LbVVT%dYzg_9~ok%SKrWMWnb601u>Tp(I$p%voWo13^!ZVvSx)59sWZ#fUVGU zXvwO_imlipWmh2jvFVh##Vp0c#*yd?FT;>a`BC*ZQTiS(pDN&Jgu9p;TZ`P@JBuLB zHekJrl;k$9@Kw9TWSqqL<;eG<7;`=IYBvdEum_!r-K0(X9=P)2)fc2fmCTw$dAw=x zTWa!6gU(9S1h0>+okew9jH#$acLuNicfaU^0yK;A0E@Wr zuZJ%5|6jvj|Ld=PV0|WISX-nbS;^fq@&{`pvNZ8Q#Y&#n9}rJlkf>TjBM7H6>8k-uUm{8lWd#y7Q_&HJ(cT65;^ z<=T?4VqTXusEO^QkJ1!b8F}DK^ zc5~y_i}n~85*}?2zIfh_``8yZ zmif01lrt^jYfcIqJK@8=HyM$T0hTMDG2+Z$3WCoIud*l#S{MvK8-cRS_5roY5;>ko zRyK}IB8i>4GVIH<+k?Lz3G;XU2OVbjRVMBn$E1)oW+hj#!ad?6?d03eLCM>LkXbzE zCsXqY-8SYtmIyHANh5+Kuqw2W_S4c9K9Tt3o&>lXuG#Bx%MomzY99n}VNWX!k=dj4 zM{@K8Iq+>l*&^LhCFz<|<8Po2h>H`1HG(M4pNYzU-waESVz(@-6oj7n<29&KOt7Hp z&3eMEX5-NCvWv==v@?uyX1LwXQpx;4z!sXio}fa6{e6Y}=bhx3XzoPzOd@tN{;nNL zDrYr!$z~v*I0pu%wC@?H#htY~hQ=(gO_(rd)bD9hxP7`Jh0p&^VMeAKX`EM^ty}aL zT~a80&+YKbK3jI7C4PeiE}C9CPtFPyYNDj;S~Ome6(W(mZ$&*>QZ@37XK@Woo(WgM*31#abI?sM}0)G=>SpY;uOGkih9Yo-7}AoFWvp6 z%^k=0!e+2HMxIRfzcBS6m~94o1Nqp#F|rct12lloH`KX>^?jJK?@b}k^WYS9`h5*$ z?+lPeco-X4%8XmZ%BV8Rr=D&kA+#!rk_PDjYI))Cf}ulx(00Yh%1k^<>S^J!Hu<)r zSc!t{y`DnlsQoqY#3$geM=M=@l_cahCd1`|O09-RG&kmnx+=Z2k1<${7dEdx@nny- zbc}KuD8KStEc+0P!xLSI*Y2AR@l5HP_~*&0Stg zhp{JnHSobjqMww;WB4Y&PJDYS@#%}74uVbFL2Em=u{0JchuR3wiY z`|{Ov2z?m~STs7E#n&D1cdYTf+I0+!BSclKZRfcSvi5{Y1uWmA7{)lhep2a?VXi6G zxB6Muoi?Of9KRsWP{!U|)ZJYt$B3C|`Doz0bNeX#qk101osgi)Y`kl;;I@1tEU<%h z%)T_grH@pv*W4Te^&hn$SdP8kZqX(#y!(c* zM5#o;NN6*3ZQ3f)NzlFm^u4N==RPdiy0@cm`~qmpyg_Ypp1mVCleqK4D$Hua@#2t= ziMwUK6FY!@`3UBIX;|VPUS4~&VVVolPFpDu!e`2y9ZbP)rv77s7_$01pIud zu4}j8GZvTNhwqF}7V8xU3CQ+Q9pm*AHaq1Ky?1ek-h)vSnG`q6dL)K-fBHrrI@GJo z(l;#xNoE~;;UOJLp!5DAV8V?Sv+U)j|e z{-luO5>O{}aJ^xg$T39gYlSu`Zj5%=YmmtBmchQSv>{lJx?+IRmggH%-)*1jQu@Ylp5p?-qi!Fk z?=13Gnx){F86T2tg#KNB1U<(*hixW5?mDirOpaVzRD5X zOl}0;yx&fu%)4(qgG$!e*f_}IJnC=1xO74%uah1z_G`w!en*z5(()@H{nNbZ{ZOU( z_qfm^DcpWYhfb(xZHY!c2C{P3-c%U>;2yS+_{5KVeYwU2xZ|d8bLuejFGM(z-5@gPwj(u_JTsOx zl~utoSYjvZn_U0%XuD> zB=Se8hESYA47ZuB9je#Ur^iW`Zu($rs4Vbra+b@=NTuk0Pw^9odL3h?SV^JIoPzdF zoQ}mjdReP~n}O;nua5QZe}&<~<>JljOmjo_)NmyW=~Ka$&ZQ0ix&mzw!dSy5Xe^(F zoT5alR5QllOco+2cHcyBQNH+|pC`dF1QSfRBnlW4UW{C_6s`@ns^|SGSh{ zEl@CzW~-AB%UK3CSHt5Z`0I}1P!rhey{ngF?6f<|?0V9h<~;E;3lmoA&JuDlo@pn2 zFyv`11`7rCf2&XYwTcn^!U{GXn0i6+U7MJLT!({hPuuvXpGzh{@M9awn3O&Hg&7MK zL(4ALLgTBA+}wBKd(BHIT+3emL({7c7EBSibREWQ1B7+*u4|CwwnKscsBc_?MXZD; z<;)76MO$7ioGI6*J+myRJp1w{OHjK3!Zp3Tonb~|ClfdNp?J&pdgFkS^qzuHC8Qs% z$&YyvV-_&olPge++llS|z!n^D!Y`eUy{NcE4_6n zwiZre)NC%%n(^LKTbHUN+RK_D%jvi{JZQ&k-qvc{_g_d5QN4Kj>e3utaQ7oQLXd|y zJ`zT`E>l6>)b+VDua?<=rMa|jXP@45wDYvOU={8XHM7*rvjN{oR0zCKXH%)h_7B#3 zp$14kBP&Nty-=+5eN(igdV9|A^7U=C1j=e&hE0ur(G-jMwX@|CRl6K~aJE2%=5MLm-)p*DZdZrv8|todjFSwM-2FuzejK5LG)TSbSYEvv zd^*pc-%SjLC-m5^sIVew(uC zUoB%;sD&9}0KFRw+LTwQs15s`yYFIEJ<5yOYD-}C`qagnYW*l^HRJ2WhEZCWq0Rj# zpelRLiz<38G-TG8o!i-imjUIBPb11p_ZwE&(~lt}JAxFi`u7-km}zMG}e z?0{}-nCi5@*9K+haa&Nla#`UD)B7)Normv2w^ws7(DR7W3-1Lk*D6n4(U6@C=u|-; z>QEr>-xC&y{FUkbX5;AK$PT7TES-0IoU>=@OrP7fdv!luB&_vS8#~bGwEfMGe?v0j zY{5^gJnA);o}dgnmCRSL%AxxGrmV8 z&BX1H{rHblqks3n&FPHlWrRBxij=xU<%lu9!QwCJxLo^BhCzK017Cq~BMwIqMXGT~ zPiteJLq2We!h~muY2TvN;CeyzFI=)qP?RaxQq2E)Q}=&a5KQs_`N5Z%c#JA|P>psn z7wPYx?noVufeb`cC5)F{?%eH2Nz|+zD{`a`F&ciJ9ze74kmagxo0n^hh~WhHT1HF@ z8Ms4yqaazafkwE)HcyMI!#Iwldd?Zr!8oK7)nwTwKyb*)WGx`*Cljqhg>{#_iELZw zIj|M?Aw!*Bh;b?o<6wZ%Ad+jhsok!ZS$fSPK>5H;`t3rJpcAzwI{>?xUb%o6sM$IS z9D})`dn)glVD_RZyko(4Jy=gn$G;qdKPGp3&|_9gw-f%Q4XMj0{o9{XV^dbL!8`-% z`m( z*}&;7Pb@*WoyKdD&9rBto?}w@7S#5V-3Z=UkF@W4VdV9D$$sC3WQ!Dn#pne6N6lfFz=yx#gQ zD60ljEe*1dQ1*ocmgR-Tpsxv!z-Zl@iCwIoJPV)Ys;C-&gM~|@lV91N?g_rY+fBK# z{nx7Nv6u73??39vRj_2P54KFDaD2z=W-t$d=-HFG7^jaenTSk=&Bt^b+PWJ^ZAQPj z9j-ZJcActlrcKlD;9~3!Rx2eB2bGyM0==_hv0ai3I4540VnXqV(9$_84; z-F#rTeDe2Fh;68Rn@>ikV>uG-@9xZBI+>`SVF!rmW4=J7G7ZUn5Uj;}F=ZAS46L+&MiDcYm`9#%(STmAw9hzk8+Npv|Oypr!16G9ZTWxobKK96!tKYGUYx#OuoCvgroPmwv*I6MtPM&UBwuMqNR+>}NEQ|)HZ zP2AC3REOuM$Kl0-C__DV$tgw7WUkdXV@FyBiSZe+@qCq`C{^H4)S!bRYkq69OU>U2 z+YL+f(8ve*>bJ9W@m|g{WoM#-dNoX>>BYP?|AZ@e{F@)=;TpNGy>n|fqNRc;3h`fw z9k>g3fIq7aUPB@aofyhCAUQAmmGq8J{te4s9SJm4HKw@v(H4a-17qIZ>%rKtQEU8Z z{xu^PZeTC1b&a+u^Z1pqI8|HOuhGU0g5O6z9o^@pUgGHz?R+ofeV*`~?~R2&`C3`I z=d;NG?_VqKtkejW^yxuzXp50IIfF}%3TqW6h=ECb4Z@P}vFKG!u%fd5s$#}}IAu18 zLxO%k4^ZyL&P^IcIada-%&D2+?9H~C09@X%KiTAC_d#62W{TWT^+VKWY6Fd+t^1zU(3a&3Ew#hqIutQd+gXsd59z7hpgg+b2T&?L`AT{nrc{rWk+RJvE=b z&;i5hUO_Q1wfuhi%GYe2kaUzWp8^xO#>(ZmZbAW%mY}u;#fh1(7M&>AXH;)A)NWpj ze(abT?-IL_y^(nx04n52(+jq~WKk1fH;9wy76sz3x(NsgDP#&c{*&W1N*hXM$H>Nu z%V^h1*L0;h1p&%u+#C6JKb*xKdAidSJFcchskkd#Z(ujxwWB8QS@Gp)9*J7N&-;<1 zJcvsNZ+D3m9G?M(4geTQ_&Om#)9cV+7k_ir>gfM))dAIRcw!6^lvC@AWN&UeJk5r~ z`TeZI7{#L0wDnT=7mSP&K2|Zi8`P||jQyCCu+xRjQUN6nwt6;^rtoTxwdPLuLzqsg{{x zcxzX`LV=a#X*5*yp2wbEm0ph#1gWb0Xu_y!)5d#US<~ngTgulV?U{0C0$oN(z#Npq zq(?u;D*pI1Hz)lCXY$7(7T`j zv9%gu#hJgDH?0VlyxNQX0rgJe^MA4xn`*54DScQ{WFN@9#s62(Q;H#+u~6A4=sb>hCj0Gx8Beoz zRrJ+p$J5=lcBJXsQnPF}LNDLc$OS3%K}OAEvA2@717K<5SQpqpSLG{(X4yz$Sln={Xl|1%39p7%XWM}ph3naRj+7QB0jp$>kB z3=EeW%~xjLO-1SIKSVH@bjG@ue0!sX(h(m-khpU&g>xi>f?U9du?FQM3Y)H4qiD{Q zR*mMKWKu-qx7JfJ?t#CSo&>x_Vrsda?lOy{owFa_kQV}L!!@kKKrKXn|70JHULD`T zC4B{E&BRNh0Vz~-FRUJ$l&$@EuJtGQ;?>G8kOqexqNqwCC(r@z6w9wq#7?;(MT=@s zVhk5O*>}EzR}UNUexHgn*UDc42-1v-%4ra~ag&n<-?~tV-Z8rTc2I!Vg!<0{%uN#> zrYnAPF#SL(mKRua%jUTJ;@s-_I$d;82^SSR>~;B zKOMvo6h(WUAnRIZWc#XAO^WB`D{bs=vtyM$NH5~OOOLzjN(4=NP6?tiqXF-BM_COI zTL|5<--I%i(>@?LH=lL=c~tgaBWv`x%Cwf%{>tY8@O`fg2U8RMjpuGdA-fHog|1W2 z$-``1nM>?{Qe162h2LJf{Ak51?d2N@(oKDXRe4jh=ORuhW$ya(wrWQ!5EfbH?z$g+ zg8OIhGJQcRB>AUbzLvGu^Pz<)QN!cpq1G4UzC2~-7t0%zzhA1pm1GGG=b4Ya0X6{u3P*n1d)mc4paN8BmWt^XHMewB@w42bN=tUU(7&{`7F%3{m=l^+v z3s@YwQt)25xEnOMio{xjFKL#ZdT)H$$Z619A;e{ksbg>V5w=<@Ik{UQdl|aB{*&d= zgK*aE;8Wa|39H71dwkuyR~Jl5yHUKOOops^+t~juy9$@b_L$l|CUPCPmfPNN^AJJa zo)*!MZjFHO|Iy=CivZp)iJI3FslE43Z53;DZdkyf&jO9xGz*io1w_j$g&%@|66yk3`mlXR5v`?1iA z&3JEmxMO0u$A|M6Xrs&BHRwxyR@vm^-uhoncF?gb>VLpWFjXqu)oD!Q`Pr)gnaa6~ z0<8OQ=vKHQo%Cvm)yoK^V2i6#0WRIiS@}~Y@4^D#y*|L;f2+&}`UX*o=87}FJ4!d+ zms)tQx2~60p`9Y4m8vV7(4?>vvBKQFTp%={U_q<46CWXY4OGhzU7Vie$fc{^xDTJ0 zJ}y0baX-ClzuV~;?X{VAFuGXsOb<>qG1c`*z1re&iJj8--R;wG-Eb4;0#ihjB}Mvh z$=qK<&%ejR@4f~d|7lOykz2oaDLIs0$(h7md@(thUg}^k9OWB@>zR-@D$$`JGR@|l zb~|=I=$;k}>*sH@-5hKcHpbai-c4&gaLd*t?nhQ1E=*P|jK`@* z24^p=vsFbDh{{L6*f-O?X9P+qQ?Y8Q?BzxROjyIk>n4v7jtD}NZZsy0FMa3WRgbbm zQOtkXZ_KIT*?afkkEp_E&eswnJ|6qI#BSU*n)xCVaq4vT0sQrHOr}3-I_T27j2Fz} z`_TFDZI~Mzf<%2Hul~koF&y((X;k6ljL1B4y_Z03s6;Z z5CHj~6_Ii1e1mC}=z^D=6ghNgc=Z4H`Eh#oq0vjf0c+tZYktWH;zXg7@WE%+Cz9ON z3SiDXs>I@{lS#u^)>Y#g35lHFz&vK|K|eqJN~g#{*Cnt&$DJ!#v{JVy+(m*5(2}1E ztPN?SL|DorKIu!P8VZU+_+CU2Sg6Q9u6#G$Fm9EqMKeg|KEG#hj_aXtFC}7ez}E^|W!IChEBGUP@AC1gvE#nq z4(mwRk_kQZ&*aiG^fYB`sL=y%cG{lwn<BbXp*2Cg-0ZJog*>E%!shb5=+WB@AFy)-L|V`1~M` zg_Oah#*mYCO27l(J^#rH_7P5Al6No@_MM>KTerxtu3gjFGBIt5mvcPOtJ+1j^`!9@ z&xPe`t(5^AAeZ-(?m&6nGiLrD!l1~CbF?N_NiJ``PGNGW9TNEjs%Z2=r(?XJe(Rk) zppQ~Z7+q7~ps%9v^uSPVj|)eCU)6UyoD%KL3adb$ek2<>N#e9uRrtdEW$#CdgM-yyJEF8BT|K4@fZR1} z$PT-_`zwG?wAZ<4nydcf(po2nlGK-k7}n@+6$yJ}#Jg`Vc4SHFzqW3!+4i3tX4-fU z8+4gR1{{bjV9YKM6!n3DmX^PADsL{bQ%ZIj_#}4KIEv)BPm?t`g96nlCP25$NviRC znapW?cT)Zb57-p-=dN>0wrPsk>}{B1>yo?*hcf2y$b~*DKY8?oG=^UMt;p-+`Gqgm zzTM((KHolsD0fdi`8a5)Dp=*4KIB_Cp<*0{S^Xp89d_5=*ius3Qpz@B_v@fFcc$Y0 zVXqeGIoynNu4kut8*xVqygHN-Uls7zqLB==VJ?h%C}vt(k89H>T!R& zM+Sg@8oj@b9`%LMn7Mqa_UYP=zO-v8hIw2TK1IGA@)dgcKq?i?f*;ndD49%3Dkvc! z+b$s?XJDp?X_XpOfMyu6xZwy11uLctb$l(qM8xbeA08=`=YHxKW>QA+j2afHO0$xu z?^xB$_X4>)KK2F20MIk?B5KfsD82E6ZM|Xy6znW}Pd^nEYu#M^_jd0!%?a6VmitKI zqwDi%#h=Z0>N>eCA0|H<6PFZlk`cmtfE)bFzg(0-p}NzIE_QHV_WY9j44n+y%TUSN zpbqgIYx`P+e^J9dO+iiAcywj=7bwnL+fxiA>Te7wtG4=Br}Ro)x}sTW_?wR5+3vI~ z4B>;%>QGxI0{A}V`9NMf9{)kp`(>+%%4R*IATar0a@z9T0V#_LU!qM(y^)k`hgF9Z zdD^T(ZZpi?mQb&3u*1C8m8D)wjy0NLJ1#}}D6ChL`l%a?MIl-$kYW{H`Qgwf>&cTR zaD-+#PrS-2$&3HE{5(g`Z!|#xiQ8DWi*H)!lvHofEyhsRgFQr4V0+jC(4|*(_`JiS$$W8Gy~j|Mdi&e+HtGxX#6!R2F6SD zf=i%TzBE_J;gOx_+?9aL&&l(E-80JSmGpgDd*9#wy!2!)7e9YT=oeHsY_0PD%GL!A zx!@~1*?8zlChO4~p&To~BGYYujT4T4%|~iJ9lEpm%{wpFhM1dC)A^GQTW2l~q=1O; z1Wl|qwX5Eofw^v*OC;TEa*k!H8I@}r=;f}C@w$X({-GnilDlB1FN3|*aKplCt5iX| zW>n&LZup7_T@C}K$~0;=u0QM^Y{AE2m-5t3#Px9$N$~q(6_&cG(2coL|0&Kx;ur>-{h%< zF3K4F6MT>s=!#kU+a}~S1M1Uj5Bl8Z+A(} zQ#w56a;fJi+T{kzk+{31inWu;^PFuz=-340hWhAs=_pK(ak%&d%_2nQ%V3787Nkh- zDnB6wm=^Hm2yM(Y_*_KD1)cX4JD72tj^Bwr%{NPEmRPW%=;_ef{Gif@-IdbT->NrGoi(K*}xlE&Qsa5C)`uB(Z2lrcCymE6~W z&MjN!XBrq0_N;pi_H1WWm`sIM7ip$xbvagbog&C*3LVTEM4WPDbJeqldm?1rxl8U zhwHLnez^*@xYu)s<`+zN&R`*?j`fkf9%IU_ymez_l95v=o4{PGkPL457?soaxwrm7 zarT|cOcIWea$GF}`B3gmE+T{Q*j8@UxIC#875i^ByL0*?*e2_2A}s#j#SrSL%Ux=_ znjBAetOlL&sFdN$=Ff`~2H8&U){Md2kN+>?+yC0r>V(aZ;l%;tYlvhP{4~xn;5M1Q z9&2KbEz~*fW6sJtKulOPzR>$s2U9cepOPzFxwlo-G(F1$iVLUV zcvz1}$WE5+{_fG=dHQ}W8xNo9pqKF>atmRQ%qWU19{*fJ`NF+KD%!h6XR+mR8I!0w z&P0;$hk40$`IVopyaP|BYLMc+^}|uU`~c5(>(&xtQ~lP|I=eEC;|%_8rl7OnF|i?v z{NMC}MTfWas-uRiYnSf$)6A)xZx7 zvA-P_iKiw6*SWO5d!tq;IAgXD6iWCqu862vNU-OdZv8)@Q7Gk%kmf=$V5peo(x`y5 zrWjCtzEQ*BXl@`;Nj|rg7{!s?8m8y<7{$pos9vBOLoUo8A$8>OTho2nk2bx?h(vy9 zT*tDDg*bhpCQzIcrf;Y)VLU4Yx*3-mLNxoYPNTn9R4`q%jY^+;tXV3NJ{m;y#+heR|- zYU*vMp>egmD0Wl(s5R%9cwMPPhSVs#D4Nq#kwseo{+jt)C9jjsqgCUto)aF%NaOaw z!)u9|XDU-HsrAnC%)A*{9M$>)b}8~D$;#O;hg14`9>3%yiM8G~t!}$a;Mqj1*ZOFYbwhH?yrJV}%pDxUTa8{Q5-%mLf(jnV9toqCud^0qkl=qrU3 zQW35$Md}|WJ0y|mYJM~Cio=D!AGS~F+$e_ zXr=?g>P@=SwO?pEx@qz;m(+~(QG8s8P9VpMdUv`b-{q4U#gd&FWDcx-WNXl5=j6C(e&qJyj z@Mwm@tr*@XzW@C;Xh~E@P-?`;f7tL}F2EgOkXJD?&orhxQ}%s(!!X6D&8`v8bx<^c zEDRqQ^TKflw6GGNq|23HuBJ1zV9MJxxKfC;DHdSW$zvP~R0Jc;Csd`KbX{)!@awFi zudKvE2$3OzQxgeX=1V%hgCiHs{ym?*b{i8rKpTCvYveO16dRP6nf zNJI&Wt0u1q00XH6o4Dlina0K!wwTHVKPsfwn8m-BQ|#7*_iyD`f{-%clJ(*utZ+IF zs+o!WyRLD5#V|USiUG@C)Y?%IuIYwTfDeCT2#xA4|MfR<)R1-pn9f>#f86UVe5S0A z%l=j%tMM|U^1kraS4AHW65Zb>i}cr>OeY1c{Ky4Wmt zeh0tGz4Jnt-x01Ft7Qz|u6{f_{z%_4MP9;0>`_nbn719)oQ-f$7Be;~Ko;#qcIE4* za-)*uOf6(1Q!CQVAtj_4oT-NWwb~}{6U7xnhP{5veCs$6X6DUv4S^86N$#7nwL96V z{Kzk&{daP%YbB!c?J`VVJr9@xuRgaWE-eAD>-`DUEUZzG^{sv4H`AawJ5q>c()^Jh z`jQjOTUwjXMsX@hc5=Mf`}2{)-5G7|{_zyI6luKa(>wHyi3v@v9(!Vvp)r>L6ST!> z?1e@;$3V^M&n>rs4%^eZeoz;q0Yb*Y3Rhp&Z4fcoC;-p5vEM z4wbgEQjeP@G5dipE;Y581#UttSB6h}2A8jUX-tptvb+3D*W_yrr$Z=NT1nkUc({uh z?!)TGAzZPm$7BEch__31*)h}dUt9~E$f)T>Ri>jzfua|7RtMB}o41+DQdbKDoZ~Xv zoILazooR+<^t029_H7!b9^)iZPiKHJeSP+#0h@2Flg4xht-G8|5eHq917YHYMQo_n zU@=-z5#3vO`Y}gCAubhno1_da$!8tUlsXe%cFFS ztTB$e7Z&^}Z(?)A)j8$|b$KJ22KD^s5@k;!~RdN~NY+r+y!Rc3u{{a0Nf)v%8 z_Zo0v$ILCvv?$#Re;ZZXoRY>TB;}%tzV#{NPn*?=FEcx8RzDWc+>EsDGf?GDizeC^ zck?EJOUFNlK%hiO&FO%4{q4*^=Bb;%B;5BYDSQ+#-k+bkMqzDHq}$JBjz zgX^}1qIZ&M+~eXbC%(OwQQeB+H#G^K_WgfY4kdRay7B$OG0WoCZf+M*iY5*9q^0MD z*eGk3MUr7wM(}4c0%%>WY|+nemb}CFmER=g^ZTl{&1!je-LkL%P7o{4))5*=LKylG zxqJSknf%R^W$_vk&7VtV%ROJYQ(-Ms&1s$6Br7UYZb0G?-r&$FFb;{nq^67+F)8aa z545)k^FJQ348~oawp$Iiw~Os_zfOlW(!oSKoAc|3Gueq&jR&%PhcAn0QoH|pp(M-0 zg|)fcy5L~;Qne3&nYK*2SN7b<{-pBe@`MVw_Y3(|gso)>i~#$ZyQ2M92TwAuUCxi+ zPw5)W!L(xRbYde-e`>3EQ#jr63wx6;yrj^`T3k7=g+OV#&p7Ak^hR-`%H=g9!|zlQ z?qyw7kyW2rQ@BVDk5lF*XU#vA4TK&Ex*jN6o7jpfw(+A#r|O)9d-znvZQ zowzz={WUk8w$jV)_BDrET-og#Ck?x^;{f>xNM^>~r`W3ii)&wRUB;mV1HXeVFhzKVJjtGg*oU+Mtu;wuU3M(B45@#O9p=EYg#9RHuT zDmm{MmHzTsH9|V-AxxUVOP?j}Dy!yo%etM263K~<_8h48s<%FlBVJs3<6xJ_ zavIbBQ3}k7bcJxD=rD4jr7jwe3H_|(@=sgzy*C`HIdG2n)%Yody_ZCqgRe#jGv*exlc z-eAMSML89(bc(#85 zr+o=?*9t`)80bDJn0*6NMZ**nkz7C+$nP?!GuHv+?FaJkuDmmekNxlxrx0Qe46?4vQaBwvv#x>1k~F-I}W+7 zo$5%HjDU=A9X-)^&ihy_@rRlr3Bm!+MNmT}LMZ{t^eIKY)FbL|BRjV6lROtvSHB$) zSw)=ZTkXrNSBvI1dHDoTVm#naMt@Hg82wc3CZ9p_NjBhxnn{b59U^CWrX*#E>h?CU z&KfIubh?-Cuwj;N2r2Qh{A5#PyZHn2yya7H(ph}!#;H3gL%zHaPg*1l#0BRz<&mq< zd7SRBNpHrouIh9-Mh{0Y+uCjZcrbH$K{41%nxHfgQzy}_JO8~$QN_c<&wh-e$x$H9 z?sQVWngkLqgm37Hno;}Z?|2u4Jw6_hNH6NZaUNS44B3$?(UWJNWwfcB=d#xo*pPGd3vF;4d$k>i+OK3KVqQNX|=6pkD}!wDJ~@cI=9OT|j(9!xz1vHUa>#bU-q$y42?P%bGS4$1tIm$y=m60EwoC8f`E zh#Qbz_SK07QfV__i^?$H3{A`rzu?8ZoCBY4rEBK>Ee`Dun{1G+cM$raJzZG(1rCkX zgg#SOQNI9hJ0VIN*Lj$8Hb|7!C)k%nE>8wGc(O1_`H&pqk?dQW|%4#Th=P=p*> z=ebt%`x*h4n;Eo#?+g4O0cUs{JX3mre6%NZ-mV~)4At}@OM=35?{ePQt)SeHrn8fn z%g0_1SW5Oh18HdcDfGPd;IQvdM+Ywn`_ zRlT}}CrC0(R8(0_;}51aND)VC-M<0Sei%U3r;w`^8~fTC|M7Nvg~W*afa^tPlt$C@ z3(n}PdEWvwNM(Yek;2v^*^srUuMPZaDi=q{_7c-ChoSEob*e$rRY+`IZ`C>8 zs*bMc-jOX6w=?sw=mCj8Zu58JXoEm*H!K%FmPSNoc35&m^?;PW zae3#{o`)rf);&W#YlXDACJ+nhQiiR?lkWdGJ}ZOxi1P_vlY0cL52| z3B_n@A(ap&;r4WySrO)cMaO& zPH~48C%8jOaJS$trD$*nuEiaSJKVf?t@|h3_ftOPxKhXcb&kg6f>>xM!A?Q0x1E#MMxs~D z=NXoB9vpC8E<1^8j~J_r5+};wg}PObvvPk~z?FSQ_sb|jIw3HoxSz(X^a}DksN1n7 zE72YI*LK<4LIbt{<`Ok9p6ndHqG1KcHEEST$VEy0k40#o1F->dwL=7kgN>aetk;q zeBSI7#&`cr%}+~4{#gl5Ybbmkjig+i^E&E1ekxWgptXp%>`Ra`4PNhFvBgfbC#6#0 z!ejCsxp^Gm!Vns-M~QM0RK1EhA!UWG3W~gIhr0H9+edosC+Zx#4_`aBgnY@mM_Rvlb3)VL=`9ymkWIkWb%m8%k2SEPHXx4TC z=dv#l0f2^MIJ~j_;(WWFTjUSjNR#BP?krx`R0gmUIk9pFl2)7L) z_D5?V1mdwa`(o!J*cTd9_=RylzR5DmrD^?%L)EQF+WgoMIruJJBi;$ZnF*Wqe#WO; z;Ee`+aIMlZ3k-Q^4rQRor0dTF7=0iMvyG%f9U^HMvU{kY~RFMzA3kDFlfO< ztfIWfVDJ%J#LT6P_?ujlT#O>axTeJ)jqN?+_S$)esRd86H-hxmkyaOYn%DiYbd>W# z1?rp(#11qSTX31fcoZAMk{po1k0@+U=|WiyNqaIv0h3D)USHF4sWui8!MHsh1mBNw zPsQ1~vaA_KtdJz#zO{{D^E8Ggji+GDc8!rn-0ND^EY%m1V~yhrz8gE$douX2au!F@ z&+Lg*zYfL?j0z;_AdM}|o{N{F#1)OjZf&2K_0lNkt-ju#b&p(&=TQz=c@@i?Z&`|$@NZ5C!DUI+xQL1& z>uy$DD=1xh<41fkh7Ksgk}h3>lrqy*@Y2k@vLYl!zNAE5b;>F>+%lC(hsf2D&pUP& zmlOUiH1@O4)nOd3s->5nbfHA{TNx(z$YNb$TXl3kJI>zLRr35yGw=l-LYirs+FYQ% z)nwAImmxX6($X_>ic&S`9I%VfYCEtnFIkEgF%UH6Cq#!dI4jtJ{nXYsCO|06DmiJj1QEgcFJKX`R z3Bt$+UOCeh(oZw+snXs;oA}P`EZFopXZ^rty}FJH0YMMzFmH&R z!=rb(#^md2z!rul66u47{3UQ#6zYgoYX;#wJc&&NMDl>1f-}jK?nQRsK8KbXvFtjH zyo&vAadhl(h?JBOMKS5cpZP8ou0w+StF_65M%5t7%#yO@I|{TO&@PNfEDW#Nx-J&)T}a{V{s} zp6c<*-KK8fJ(qMz5bYPlYrECoYg9*1kQ$F*L@{i9rc@(dKGsV_Oz@}{5B62*jjN+< zA0{+za|CEAXkTeH{B9z0=G@g&azn_oIdhA7?>tOLtGq)Rte|2`}An@ex{E>WhIDNrRKW+pqT~+ z0?K=xHT7?~?_z=2UY_YM++Jcp0|QYbUy-*BtcQ|>^RqsSDMbAgH4AY6>*x+E(&NdW z*(Ow3@ru?zBOac735hVA{&smD`dg>BEA)LUADTIRToXW1H4{uEBuLXYQk>k-eWv5? zu=_fGwD)gZ`pZwFU(B@l7*r=we>&{uRHowCE*{7HJ_LX^8P^KpMZR$FH0*II>D;bL zZ!QqCK6r8sHL417iVSR}>qBYPA_qwy+45~>vOI|621*UsmLA>C-Hi*juTs21r)khp zFypZcq#G?;I#65Z7++4|eOq_#AUwU;#iBI#G;gT_T#FQ~l+R*ot}zY@#V@J_whcr1 zKov@uj~nauTh#-W^8vEXFl5iOf|9_OSf9@B@{19^@JPD^43;Q6cu|f&ti!ri+u$Kp z!pLxihGyK?SE;jKVP_?}`q#4BE(Jjby?=0?7K@3qPeTjCXD(~`m4DlSX{xVfReznh z-{;o{mH0PhUp#fqjfn8hW|4LB;uj|xVY)hc(qXL~`F3qh?=>GM6OnrqNio~$cx0kk z&KQ0=4#JDVAuy7KR<9`qd70w7I%cD$mdO?Ob*$rl0GpP%@6($$!SnDMkFl)+AW8#8 z6$c}iK9uuqAoX2LqGxO44ICDLZDo4&DHQ2!si}aLw8_z&14i{GHcs>(N5J0HIqfYK zg2*$6u{31`gmr$O@(!&}?-0cJBY1CL{fo8haeeSfFZmBTPXUoezg7LLvhA@*>T{OVp0T+RlFP zqyG8=uXV}pfX(f4RAj!NMiYzevmCU^F-px|%SU&s|4C#QHFn55V&D}38Ekd7CFPCe zY)?+E8*Ttf_Mf>M#~^dwGi?X|TI`qMUY^l|?GaYGqi@JnS04Tf`l0NTzntgvKNKyu z?>={#&!2I=oWv0ulH{5d**SMtPMD_K?}%}48_)~%dw->w)unkM++TzRLx>W};q-l5 zsn0@1BY_Q6zn|bs@h|g8H+)Jof+-ZHm0FedVIN3I6sAelv`Z@w0UvQOJ^9UsX@!H` z=d9zaBT*ua?6ahT8kg^;M45DESqtq9lnmhLwwn3H@`^3>JI{(d64u8`38sP)nfZ!| zxym7Ez>Z=&kx8OW>*L2OPBd3Xs;+y`8z~IwMT79{PMv)JS#kS|4XVbi2DloohB7Kp zabB&9<~{cZEwe61L0SX`&r04B6mmnE3XmJyd&e2&k3O|lHCM*nj<(}J9;n8E??u|1 z1Ujpb#!;pVNwpZIq|1b|l@RquV!ea`n_1<`LlL{kvk3F_L@5)Nbq`FJ>~y(Ifvi;| zRQ_YI0BKdt$n%Xh0J*g<2HTfp1JHS}6Q@{?G%wDW4u_=_A%0qt)C+BYd^}2_k<`h*BWQ z0YM^K#kOg2kQAUxaC3q6TQpFS5lk4ccycOKH6c^Y(@_B{Yx^}yyCz@) zL2jZ}QAMG+DrXYgOEm!IB>g3sT3l{CA)$h)7tU0y7~OupwX_<1uLi~W=pZTi?UivP z?&!eV389;xMKbKt)r~BA8gx?Wj5w7(UE?VJawX+FV;%osIQ^0iGwEu-s3ksv!`!0z z?bF{S+H=eDwzBu}~K%Z;I5_8l(No&3>dGT|czPIB8IM+C>U8 zg7OOZu5M~lQ_T}w-MyIl%^4)-Qod)#i!9%)i>0alILE{7O8TpM@+;NoAqOl@eOzSk z*jA7l=+-2dMN1PWGka4y&1@&5sSid~Xok|TuC<|! z`OgHto!G8b-ZgaGM9Pe21ZalV*w&uI*=;b^P z4|N!RMVfQuP&}eskXU^F!;kZk4A*PbCLU`wxE5Ry_@c9_9RKp*q^6=H{oK!w^6cN8 zyC7C0fEHYrh!eoexcaW1W=v`BmOwMDP%L?hrfcg}l8~`j6p{)^gK`iF}vue89!& zuccGd>O&S*@KiLxEwNvah{NO>Yb?C-?sGCm02k6#$LfgdsTUVeirI|XZLDz~1b3<# zYP`K&r%A(qL)t^DXBR^#DABEKh#NR)!$G+_Zx}rb+f&k+Jz2!=m>W*VMA&Uw4-f72 zQ8kSlC;k71J(J-_jUV1(kHnav(YN)*lK9(py%2uYi0K|rFPbxuk3Nb_?>deB6T0lw z*>9<@QK5NxO=RCNq*d^#A{*JK&~3T9H;H#$*77IU;&(u&Q;?L+D>T1?zB z52PD^fTqA^Fpw36z2)l@U&GE@*D7wr7`{krWwhFcVj67eHT?bXY-wQNYjR7 z$d@k@W{)ZNT07SfsA2Y)V`` zu}sidhu^Y*4?9Hph>F&C$_E?wdj5Y5WMS+p{A|yQaO4>FRdKd}^L|6%-zfDw{an`c zj-fT{f3rEo^f~_1*InEbB5;%Lt()aBM+d>DK32z|xw(3FvsZ*3gU*H@R5|0$HJv=u z4%r9scXU@Nk6k$}Qjx_YuPZj1uGCfLAdtkTI+^?*4(i)P&=P+!wSKci|E_@p%=d4z z&xDUyxMjqi{`ICXXEN4mCNlyiDG5pmwyrhHDx66~eIA1nZ${JAX>giMf}3 zmW11V`^>GZ>L~3LtX(Nx-wf=TEI2D$lJ?)OG28gsm7lz%x^I!VHJyp(l+U7<6+P7j zu_1hi0(`6*EF7;`fi-TwP4;l+=&*3Ye2pIW!R<=dN&L@eNX|Mp?3fFksZ<{O%Zl7lYv(;4KeN*oM>S?!oU#c;cdDkD<9p8&g zn^CFfptDM`ai|poAMA^pvxy-Hu2Hq^b?C!2_V(KmmMjyd$={QXg@{c^5RGbiN=*?R zhVM2Q?nN|BEnl7cns@Hf1X(x9)0V!=5BQm0e7M*dy$9kNKOXB2x$T#eeK0Pv! zJ>SQ});FFZWjXB!fAd8UFZ=I=nqBtI62|TQ^|@QAegixYH{wCFS^zPE?(a)Gj&VNU zHFDmoXxEq_L^GU9I_Ce+Am{(?USAGS2b4;DiyVmi6&SWT(*8PFfhmKvFmb;8Mrl(N z8dSvyFnn?yGT9p`dvU-)Xrcp*iK8#@#UOmJiD=QD-oU}4z;!b1oMnhhZI+$CK&-t| zX`D1v|MmCd8ubN8viK`_gEVZ-S3**RY)DaChqpG+C7y`;lq}dNH;xvtH~H+pEZJyoxb>7CA->>6lg#wcH zuU0;#>6YC(sk&&E3pitG#AhGn?xAIxF1eAgZSX-M-+Vr&mI_l3A=!Ot%hgEL*-A1g zK3Q(!fZ0hmesDa)n*3UlL-j5Dg1A3G%ryT%IT2YObU7kK^oA{&u^159P^=V@G2Zhd zRm{hw4{7OSqPo+)CcKI`fIZgnK2elOLD33GwA8q(&*52 zn}G(A>`aIo^@}M>^4h7utiQIMTo5buzn6p5ch$HC!(iLf_a0iDB^u)ijAq(p{MeKQ zy4=sD;&Fv`fNvViWSClN`WG?URWd=v&-QVE*3TLr5|04X3Tt}_N_4dhz0^zYszrH~ z%G~|nNDImL-9#?Mi%sUXQA|2yi8#BOYKgjin-f(L9)CDG6!s~12 z4YIJFG!Mp%1s4qN+H6-MW=mDS6;H|FRbi9i81g{B@0T;Nb{_!tm~i$@^V#WqF#Mv_ z;2P`od4-Wzs|>A2Q6*mpsZO)V&30|RQ5@PU*3trq3Vs1-3f!O;T^>G7=Sy{ghV3}% z^k`-hDuzN4!>_;gz18BO(6H|7_)s_`6o#|PS~gwr+p8F6)`q>|UEr0M9{;IT)soXx zNrTIJvZc+mM#5aKl( zO_B(L{ifY0qD~XZq7rilbvM6rGDayWmu0_S5Vf zbtjoRWjys5Pwxkx*k+!CSEA>ne7po2eDo_Q$tqdgc- zh^QqN5JlymV}X=*!_ITnaNN29wxDvbPt5n<6ED!DLiuQ)w~|+C(34AK6;abkXzn4KmIK#B7YhqiRoGu19v>`H7Wbl z&WUN(D9c>xj4MJG9dxC0l+-Slq1PUQN={8VYlN+y;T^N6cNRm9CjWAt-!2KZgg{Us z!)@_Nw=dcbIk3k0l1f;m!L+E1U7X4I3{e%qUaZIxk7nhB`;NPZ@EO@K`{4v~>l~_o zg8-+PQ!&G03`K$0tEzw5?Eo>%W>ar~8>sJX^%jh;jY$5;ec=zM`cxV$)D_7atIMke6{rJ#w1g6G- z-ffB3PTWkqTn>gC7%r4n7W2JVlE#~$${FcLLGxs)w7|dO?vTwx7*)=b(qHogqPNDuv`K^`L^`+HHm<_>FiEJ(EMb+Og?poZyRc?rv2E&!(a z#XsXG+EZ`F;GcE6M^BMVF%qZrGt?$z=YQ#lcw#w;rSn+QNUs5wduAPdDqp?%RbnFJ z4*#?BC+x^$fPg$B$aq9n2LAuq1gW7JPNb2Dva2#VtLrJ_EDNaVJTi#6X9?jz>dAxw?(3vVAuNO>Sm`gd39swy7TkP=QiSY;Fa!;g`@CLM&3M7&?ROoYH6qmv!I7iK$H5lwU8 zt}DbXfB5sP)gobDP;hfx>}rv~kYxkxx~Vbzk!%~{YAH1&pr^~SlXSDGS|#KWP7sKO zYU|P&i+?7__Pk>VAK>aW}aCJ^kd~o~3d~$8W+S+0h0dIV{ z8|Cj}PF3m`G{idyK5a{kl&k(C_YvKfL++=s)6jJzN#Hl(tJrk@F5Z&Xj{`XoLTx7v z1aa-oDX}!ZMvQgmV%*5jIo6JxVy=S#HX~>EEXl^+wt8!^vUqs?Wuv}+Vc{pW zBmiijzZ%xi(!-EK@|LCHU@^}72CY!$i#r8g;Ybr$cWjo+kqoa%3jSdTN%?8RWLhgZ zJD|ps(Uta#!OD;0c;ii`UCbA}&1*|u1eE(4DYk&5M^yBcoXGCE`k=~s{HQZRRat$~ zdc1UDaXv#(!oF> zkUha0-}kmI+`Ydl4XTFGU8*cI8^3!!ZRO9E19VX;t&GX2iHP-LziD z2|Gk#kqXy3iSZ*IeG58SgcESU-)8^jp@4p%e+#JuGt!cEtG7(}8p&5A@w-zJ_*T$$ zLggwu+OPk?QQt+nR}QB#up72Zt^W&-79cSxYmE$U9R^dd+iJv%S* zWaG;ZA^+Hv`zt7xLIQ?*JKtMq!OIb=AtF0xNj|;ooKw+iq>u;)(nL$qgg1;?iyme- zQrqIt?Kh|@Wn~St7C*@}H8j4bm?usgVH#f#%$5%NzLT5zR!U=n-vE0|lGQ5I=1sS9 zk9^9ki(suNagwcTu|u`oyUqLTc(NpldE^MZ{cydXFMfL{`$-r#d@p5TlCGo{8=idGUJ+Q334&GgvN2kq5}p4?>~!-NEBLT&}I( z2{?%2+*R3fr+q*+$j{)O22S5oMWr!VH}M}P1vu=`Nafh43i|xT9xmtd=#Nq)Ev9(K z8OFABP+$$g&6A#?@2kOAtxCu<*P;~?0@A4H^qX}KgmJm6u$<0hLO&vp&ff+4*II04 zU+k0}-BvHq(lk)%W@_lZLwefv^>PdjV~!F6{$5-r&;R(xmI@)8BDnQ#=CM3bf=&{DiXJ$~OcsHKEjKDi7X5BT@+^zRsa!1f3gskEC} zI?*QMggx0{m{%He&@irSrrEYgWSM`tOReq$lK8fPjF`*ykHkm?0OnYiX`V9c;|DdO z!kQYdo9a2OfdYLL-0Emd zH9_4QuWKU*bMJfd^)a0ps1bb6t3=|6))&7SvKBhr!aK<>OQt(qfA%>{F|- ze4UsMHq`G^$~_rQr3;MZzFV|mH_WS=?*a`ty=+uEM}^X+|In>(6m}R0 zc1>;m^g+WfwL({^$A7#aV<7_e+3cXss4R+Mjz!hb-53?fqQhv&#{g7k)Z5C|)mdhr8m3H`TBUj_uNEm^n#x&!OUW3TQ z@}R33=)2&@llx}n5?l1wdv(seOR_9DE;z6X8#JJBCazSuhZC{?kCneA{2-6QIql() zN=FpKhdxccnuj>WDwe4Oe`_@Wzpzw)=MndM!1BiJ84<(LqGq(<8Aq%IOR-sV_54@^ z8Snu}Z5!0@2b93CRMzW{nAoFM`UB0$Aw{irzoyKjIM{1P4JwDHm!*^91w58k*|i9i zV#Ufh&4c-kt6CqPk`g@*U}v{la=|t=6^igMM1<$mCc4|~tlqM<@pw#Q404G+QxhSmB;KPUBN|Bvj4#d37iulr zI~}`{Y{5W3V6ewvr(Coyt8R6P%&d({yqZetelVogw{Li1%ATz!&Zv7{z&tK`Il&&! zXNvs!<3Y9XnFC{jx4jvqYt&FL=<&-l&V*5c%siSiIE^!Vri@>+*s*iBbo2XJbQz+o zy{EdYn8wc4+ylm5{Srf@_gv8DaG6}y=^jf=5#nMoS#AnQ7+-8$UQ6$`!k*zl zxXwsmFaLO(kW8>+ldFLx@&%Cv%Vh#o>L9!m<8xoRZe>rvR#N7~USzQHZ~DOz_Y+>L z_o8Ez$!@DhKF@J3Xs%VskvFe_&R-U$cv#o)<)s_SRcECM`k8>lIh z&_*(N0-c#*;a zF<#wZon3+kypQLz7>TdHZ2k+G9n(Um0h)hW*lkLVvTL|(c!aH)S(dLaG0NpVF##`k z>w^*0XiM_0uLmU&f1WzX>$ZkIn^QD?u9~ThY1Ogxzxz|i=F5^(X!s? zoY};3PW$n!XrmXUl&7UC(X*%;D84lW?M7+kLs#v&G6?hjfOhW=MujLm?@*tDOKnV^ z(smXx9$C}7e#bZz@yK{)DV15?WwkujOZ>G>GHnNVq!!eaYhxy&^9Edt<02F_|4Ykv zZZ`n2fA?qd&TZe6%S&Ug7M2;KG7Ek9{wpoTgq4>IhsBY#kh?nSTKK%A|GW(2qn5<) zSLAi`M9MKf1o=1ntBAy@SyB+#OoSeG4e7fS!k7%3i7UpYl%#7mwjARP71b6owwJz$ zcz5jD(X)gq1L<|VMg{`5rbcjBI-d_VYlBA8ng)r)+ty7Gh+*@L99&#-vDc7chep;|3cUg-)ah;lI#!oe>Ucaq6+OqcF{S#!%D?wKo?n%1^(pGwdaE7^TIeIBhboM>f@v$Ydu#58jTg2OW zAYEQ9;7`t8SgOgKH7y$bu)W0cPnPi&%P+8dW4`;fSMZmqTP#ZngR-F=Etq9!hQLFv z`@DoqYI`XrcnmB~3LWRI^Iup{Bv3GKc&!bQ-n%3!S*ick zj91erDvt7_+0iuk@cC}@h2X9OAJMJfd;2kcAF@0_A+EwAU!&B>k*bJ?jsgIwk<$n1 ziE9+e&j*~{$gBoOB3N_5$6$;HA$F zz_}dERz3>d@_Ju0UFl65p_f&U8s&f43(GHesT}D4Te!YLkxtN@p>Cq_Z}Z!<4+IC- zQ2+bzG$jPM_HgxaoOk!=&B7%r@j3aIRR6#~lY`<4p)v_@3X0Y@?0pD-;C}cH*6Xr4 zJ3a9#YWG{~gtr_;s+NC4*R@o2sU0SkVB-j?UAv8IUgE`%*Uj2+TUl`8TwX@yt!W5vwwtMJE+8HBiKK<_Rl!If}?0pe@VOSrf%W5BKW%SbA^ z_yza`VR~@~@#vzm0z_Q8#7{=k9va;*XiiLFW|V`RK#19Qc|jIEAla1N;_53Oti4b80zvz;`Y**T-0-T zf^~Y6sSa9k?f>z3MP|&hLo?e6JN%3~Lb$)J`~gPJg`_Q2jL2n_kKQ6*am=!VA=22- zA26EK%bDO4SgMmWll&+O&sn^Z77o28lUe_i|Y>2f(D zra5zKEfSv(m#FjEC`s}bL~Ivuqj-SAXqYTl=tc1Pm>cIfE zadm6w_!8;e9*^|zl59Z{?cu*<>bB;8O*zaDrip_oU0L0>lRiV7JQq869<5g`-ljpoo~xc z*S~_0I}?gON1Gt&Yb4QOb5<}CZt%BTobtfvyAhLC^B+zfH$?DrfX2jxhj>CRufGn~3J zvY9dg_QYLsp=6MWPvYL|kIsmqjwyjG5QOM!syhUDa4OjGrp6V~EP3u=j3qzaP3(%u z3hcA$QJWEE=gUZ43J)4Jd{*)>syt7jI&2lg*FfzYzG51?xo;vTuj3%)7PhImsKme;+J7&@*+ilqq)a2+XiZF6%4DN+BKlj#MfQqhto{jBg=7!s*B6 zIgLXSCo^vzpn+GMl#3Xq_d*E3nBZW2O~`Wv4{`_mrGsbg{^=0Ki|t-xvKXCOs>#=6 zU>MV!fL$uqG9~-)ltt~QRuDS?7&U_Q3Sh%fEmv4lo#1-+R+2c6*7ILDiEa1GP>Bb9 zn;*?kqvChY3yfm|-m{5El{yOnyO!27NaGS9vvJztVkloY^Av-A?92VRbd*zLa~nzeqHxElWO|6>((UC6O#D|CSb0v7Nm4>iA~yT{FB_ZB2C` zj+r5Gzp$rALM<~+~;O6=+!&vHoi6*^Go+?`X*>OH9FffL~Q7 zUu9UJQ~4dge=^0J!7^!5K{EJ>qr-NGL)2`Us>eYoI8yYb$7g?RTxflE?8m029r@Qg za@hfy3>k@4-GTN*Ca-uBVs8A*q@` zma;NVN-abJcQ^j=@~l%h@3sM2b=%ES8n^%B7(bD}So;R5OcITcJYBMrH>_kKIF@b< zj*o70dFV|0WI0@RO$05vhs_3^{1Vyb301zVAVF9|3`8E6_8L%c%l1zH#eDDi0*|u7 z9>ZbzjFkipyc}?do$d!*j=)UUd19tE%eJA;SJ{sL^0}Lr=29{~>#`0Y9nshS-qeVR z?u~Y54)cxbm&<&h>bkbyPW;usb%OzQ{J_=_7LbXwZ4fPh?6;V-3C!t#Cq_Irw5j{z zt)4$#D*MLN4U}StjFnbeRDMtB#O+n-`^Z0v7h&WN*cZ&r;M=PS^Yv;$`6Q5RAJ6|Z zgeV2-`GjL$?|T1BZ0leDp!@y^h=&5k7@Fvh>__Y zvIV7H(#4^RhD@Gk`9Yll-B-b#h7f-JXg=Alz~r#&z^>zit5#`lBrW1bPN+=E4j~zL z^7WbB<+m$h#?HqVYlLb!WyxxMG!8iS)rbmxjw6EGO8S6QWED_;uvBvt&4@2Jy&V$l zT+{QV-s1Hx!Z237PfhoayW+q(4zm&16AfV!wj6L+cXd|%?)q!y@Utz>iL=kMia1$v z*X#Yqv8ClbF1+4YXehCGM^4wxZ$hj5YWiu*R3RxbLzdRS^Y+%Y&5jrGfx@7df+{w~ zJ0#Zv+tWC=ltnCi$wFFSz48Vu@T4@2!_3gsiH^k&XIOI|Lc+&#reA1W!aCy%XRGp; z32kVB86bWj#i#qSjw!Rfl&icq=G1kWqiEB>5%}Zh=l`uL;QuoG0{)5%zn$}b#+de8 zB&d~ODsjjox2C0b`&^Q5rB8+;e6hoY(mKqJ59_Q{1cHkL)@w9Z&uFvxw*8`le)rn1>k{%BReh|3!YHj7$9XMU4lx7KA zO=@>=2sv7h{o(R9lq@L-H6aw|g-|l;4GKrXWbnaz9LXrgViNO^Uk*x54kJwt9;*ir zCyvfqFK5E_a8K(IJO%or+ga<5`g-pA`pdvupX^)6;w8GA9qB&6W_IuvE#XMunAFpq zov&b~wG%y+_PH?|E0s-$AC)F*uj{Q+nPei&$u}+on9UDY^zr==R(+0IGR$^)%*j$;GilNVkNZ;-39Ta!h zT@?~rL^t%5V9MrD2op(W{sP)B$mic!7|?ItjJ1REE~&9%u-5tH!A5-!3kb@s11$_k zu3MxVQ>GOz-1~+|(A10i`NpK}0E{=CK}Buq79mF57VP#S3W|bhjz*uI+Efy%#38K7 zqe6SG8bxA)d)}*B_2P0@$#rp8HlxkdU#lFX6dF&)k+y{tJ>ENifBgRLjMVk#@xfRe z%U!9_sAd<7{lqOPm@Y@I@HA5X8{%Q_+vQD@tIBxfqcS{soOLo!Pv}l#`WHg5-_u;> zT6l@k)uQ*q2;&FJi)T?eFV5DeYCn$ZWSBje%2MW+K(cL^v<4mUT62;*fxHA0+4v?B+0X#A;FldOB}y5cvJHUG87aYZCqqN7}S z?w_c`x{ocu{-;C+JwhNj*nKg)lO~5j{Wra4Sw2Q_4T_lQ^ydUdJsd{K@fkw5D$^7Lu={tPJ`?|!2(f8!@?-9v(6#pqmOK7L>{|)uke{@fVL^S_YLhAF z2;tr>1J3V#!-jIp-%Ccx{LVD?*{YLyYl6Q)gZ^0QD4O@lM?NpEbUl&no+JERWF&M& z9GI7DT3APwRF1x+ZzPyAe=~xg@%U>ZnB0|AwsnP}o@arCfPu_2o!QiN4c)46!(i(3 z5y#>miNwt}snfJSJA%97Yb!^mq2*QV+&|l>$v}Pib2akgij8aXe<&?01$g$}7!1w+ z=SQ&icU}$8md@4EnC+RN?#nw-X^6%Y0LJ!-{pKWe!QLsj6*fSY^ z$hhat_?r4dbX|!$ClCBHXWcsJ6?wRfc|*wMfKx(%IA>Xo&FME8PC~kTT8Q!- zj`sOXj(_U&i0;{&nDGl$t5e;)N=@t7FHWJrDF}O0V9pZikY9d3l|~j)fxE;M6;MXA zk<11}`uZgzT{3Cbhr!RBm6lujac3gPifod1-kAMMHBoVIQo6@TIsLl8dEiaf`gH;{ z&P_(D>K3o5%G_ZPmU?K<%rE(J6D(bUs{d_uNl}7R7|$P_pu|=~lE&{|mpPMi>8>cJ zHumTK#|qslz99ZjC;Oi$^f0=ftDMW=%i^QE)7v|c(!T$qKw>xQLxqe%e=sW<9)Qb_ z^>WSrW5yFFg?*W*bEjghHq>w~T;<~YV_I^hR1@d$2lH{qX0O$+<2h}2(gscLWqdh= zWOHOF`0Pz~Bjpx3Exw|QC>E4@m{JM>7Qf=9cETvG(*_#$dbKPSjRM8WvJV;kP6u^E z>(DsTT+{9OTJWA?r%QD98gH|W>ZG(ZdBHlR>fg$y)AM>7p+!3MIb~=&;l@lMQ|B(k zWJD6%cK0Xj%(HT9niJEM1qA7q#=Z*qjReMJqT)8mBY|2uqR(TE+$J_!Y<}+bBRkiV zG18YlG)$P=wzu4BQ~mT1l_~j953H34B>uigP!F4HQ*U0 zepPuR(D+O_9D3E|UjrG^vU%-v zWjd_PpY@jgDk>1Ltf)|W^y|i}!5ZZ(V4PrcF-T#!;8kJAHIr^J1qN(J!6X&=^4KIM zP5q9U6TqqE=@pDg5`4QJVUY^KIwyZP+7{ zUZP#Jf3uL1*&l+8ns2Y8akl+P^HYJ#Zfu!!bCVP*wc3OZS}a34z1@LFB!6zjFJHpH zQFPt&7OGlS&>ORdW5mC@&Ykg38k2&*{;}XXL0fX_G0}bRSjoIsKNtT#tO=Z^6{x`T7bv9%HUW@nGA zOuON#6?DE%u!m0XXd=kw$l3Pm7M{n5`3|Sz0}91_Vqp-xe`eJ(w%OT{LQi+Y3N{NK zWkeyL$znUWIQQ9d0wo@v7)Q6wHZciiD+-di+}zrxp1y+Tq^fa84aMz@h(vSAgI-;4 zA25AC3$}dqM}M#*miWZLDr-|tr5>It@Z@AORZ-U#)u5mRVKLZQQjW?zUhxGR)fl{0 z;+v6c?O}A}D(x)5OSDIBXiKx|KArpUm}_4xl(v(I-nXS?vLVi{#qj5kUem*6t_z#e ztI*+?=zPbxndU=9S-Ka+{baD{&eD#9b`Ya=mjOv!sioOnZUx(OwD2fuc!M`!hx=}# zD0+5@Ve6Y65&yEb_D?2S>eM;|Ci3FQ7Lczzx%02cN-w-IF&hJ2(Udfc6IoO?!kpvl z^hEGUl1uHJkw5i`=aGW=1xJ;LwHrjgHq zcXVu&+zU|TuV#E@c*!fXLN68Sqm)4U( z0PP6KUL$^}@yCXxKJSn=b%ved&XIBdOMAV?d`=iB3KYRxo)5#jK`Soo$lfZuSPdcs zhl*vDO zXmH1@7jXs_IYDEg|OgO5;3=`M2Mk#S~xk6Z|zg_3%yRIQIg4^#{L{ z$EBb5=qpt&!jH`YxjDu?QaI30F7~S*$TIUsNBk^?#I0+0-%`zMY$|>1pm;h7v)A#X zk5jsay}rOYp3zONfwv1h73)}r^hjg=#}}>4*TE(gt>x73p9XqihwIy$*UR03XF=!I z%sr!vkoZhAj&ke0t-24-c2zRX~c)O|xkD3cI zaT4+45h)JVJaQ#pC_cjKq;no#Ce+p5{cjc)|MysbZIV=rw4n?cxZ^{|pIp%Y+ZGBw zUg#pFf8$C@h7}w4muB>xNmg+02>i9?PvxTO{c#}To4uG5|Cnn1_CEiY5l9I4+F;km z%IU)4UBHPC2SvunioVa$8Ue)nt#)}l$1P3z)X*in&E-4Nx5jmBlQ)6h?n3_#l(fsY z@nD-3Dca($YY zS~Q{U8S>JB%f3c*jhfTlv$ONdiO2e}-U%ZD*erE~8no;uB_)GgOHaHNA=_$KZBFL$ z*p*_pp=LA>Axo}oAMdHg*R(J)V{V=K^Ifu^9#g9~Avdi7Z?_G;lqQksfXdhJC1Yud zKWrQ-jH;6>z+4U$;Z=aJIPR&3ek7d8sK@ zB{P*6EKk0+0u8)-L5z-JO?f+0po9k)K7lbDVUF|*WqdoyV4NnFAdsl+Do*2KZEi;Y zD=$Sv;-obzhw71-jwZ~-I_ZXjE+37-<_fdC9h#LKKxbZp%XCiVEJ4qDnLz-IgOX$&vfVlDfWjZ6d4Pn!UDi|5ALL3d*7i;4XPxWF8 znK6be(xQ*%bDs0}HM)E^eZ=J;y}m83M3!CEu-G{3hmN~?*CIK`7xYf_%I92kg2>J) zmUmzTG_@0B`uKbJlnjXRxKG`&h-G$PEteUG+2>!BltKHTUJ2aC+Y<6YvgNp)(wK3XYG;Vl^G;<0gRzFKxgqS((11h-u9OIbO?b8T-SAA6g}9jJ_Its3Kr#Ns|UxFNIy& z?XwGbcde_O@OF_(iA(AboyqItKr6SA2#EXXHp?)g>L2ai9lQhT$Db*9Qva8~>T20I~-U z;rUv_+8Ndv&9^@s34=u;61{?g*H4)g7K}e>c_Jq+sO)Cnre&cB=hhJvgD#SPjEd{n zJ>@C|(s-IxCgypYate4!6A+8f6J8F=^7vTs09q)-Q<)S9wQL{$qn1v;1v!lpr%n?L8MdAh{?C}{PuQcZGnKvlm_ zD{Q4v{yyd&+Ufxme6+?1ocC4_Zd^VADrFj4&KIT@2&6hX7{RIt@()<80KOe1|6g zC{C`HPN|#;VuTPaL1V5>ov34u>%rN;(G6TJ>c{=PDOYIzW~SN+(&L$@r_m@^HsY`6 zYE(FxGi6bP4#0KApxbc%q(a2>61bq9|3$&>{TnT?{+i_)KzvyvN-1aH^{jW{#Pdm= z*xp!&nzeAh$3PA zPEB9!6hSA|iyGe_qv?wAlK-=Rj?zh}%#DP^Mwj?4d6*t^gD?(N&MrMt}IzPW&?C?gxK z*plk-5-EpS0BZ3;jk20@+oUl;O#FvvMznQWzYhV@rmuYIbV$?$aF0e&l*xz1sU@%( z{48utLGK4_FL%-f9Q*~qjfIajr&PwRBXD5TUQu8Ey;0mibZq7DI-`QyJrd?hz0=UY zr_cRPp)Z+s7~PR3^+sj4+L%&_u;I{7i2ED!uA(x7dk47Atrq%tetFxVZZtO9zqsAA zyCV?nb-zQ%@XP;jlm+8PDmEoDRURW@t1gRuH9^ag1f?D6${rr=p>=v_`u+`c_J!OU z#MCb}40K*8@e2OioYsFI@duHL>!@v7PQF#}_6#g>B_=4) zKJVFHn;mbEz~cLTJr0(s0}k3JY$my|v!wlRkRxCLER-9cysP1D@q4F5B;~KPl_0R9 z0=FVan{Cr^Y3QgkPvh=7%o1PUZ=R*Yq2qF#qn8dML0%FiIqV1|yOQXppIoftrg%Rs zVac;KZKU zlZ<}aJbZZryLT@^AgHIJih%I;#h+8aP0P5?({NJ{zD(V!n}z9T1l<=-GNlv&MgE zO+qIhf{wO*=k}Vo!nE;>D!jfb{N%ju6<;dqFXL5EcB1f)KRs6wP6~!p3*~w#)!}u} zMBttgb_Vc+F{^B;%fu^J@^Q$sFZInoKXafbce@WP*&Y#ISe$Qi!yOjsp{u+s{~IW- z|GC08Ge2D%4l*DEa74}P zrE9rrMUFu3$Y|$TK5~yF-ZfR|M5mxPl#9R`I7qE&XPZ-+o|c%NZs9%}^TU9Oj~eQI zn~p5r(DVMf-_A#S3p|GumjS25x~1#8jxo0)Z1-=<@?8**OWeZ|3HA*uweKO+)|YB2 z_SR{+V)Rc|2Eh|S+E zitv%&#-=-?x<|)(XaY=7X>W}R` zoD`yOe*#Zg{6JJcRP28irtXqAq<;np$bM*Lg{zTIQnrsUB1mFi(|DCa?x#k=jbjdna_oX z21LG;VLO<+s3Pm`=rHzPrVp6_`J_-J7$$#8%uarN1ES05XEUwpqY=XVg@944W7mF!6WA{V|sc4jx z`D;}>t_eH%JOhi!m#rw_>YO)b_4lolVKJwEJjqjhQcV*U%8Ae=<;V+%qX3_*E%efE zDlheL0b5ymxsel2fuvSosFM+F!^9NYJ!TWkNkb0W`&7BqX02=DVSH2D{^&$!fdcM8 zG!}r*8m`F9oOAhRAH#a{-y*Dgo_b2a@^~kUgphuAl1Nr#2(2`pPo>wnZ}=`B$JdWPV1r+zIge9i3% zq@gcS1*{%nduX+zSacF2L-WZsug^pykrJA|N3G1BY$+I9#Hv#vBv9?@WEiI`^dv^! zH9RZeGI>T-Zq2uIm)E!1Hb)Sr5PBqLAljnjnJFgFs#}W1G3sgJF z_e}WlzrP=l=vTpleeHC549b4L>KTt$KRhhM^-OhjAY4j-!M|6TfUEQgV8_sluQ3n|lT17u^6hHP~BIGO6t3skrq%4!2!S?m$VeZ6qJif!tq2wAy6cy3K zw{v=m#>)+5=lc%X0Uc6&7k|SW8Fb2fZ8}5EK=<}+)4x-7OtzLLJWOOcxo1HyG{GKN zfu=F`@jLl}aU0l)*6vGbIR#{oEKthgn!b%Ip(8pW`V8GN{<&sqN}O@^L7Vshgjbo_ zy<;P>A;q>Z=aJ{*c=wI#eI1AW@Spmn8B2Bk{jlOu%ux{&MZ3klqtqm&V70Y`^M$7W znuWF!+nfk7+F_TAL9h{OX0jz1ueaXB9rCt;d>wJpKiwYjwc}!1$KEY$GsM|g5!XY+ zFUDpKerXQ9Wca@el6Led?(v*SY>rZM@<++h{jSzkv0rbM;{%gBMgf;bvqrFAJ6)1< zmOW#W+gvTRdtKnM4prf7ci>&S(UVzpzlP)`DZcbM)m6PwW?=tH;TDwhJ8R>zyGPQ* z_vmHT{HY2fs~!rRGM$8TDpF&mDQ=DZv1$g>U+a92t*g-G@)?N{k$<=k~CEBr}!wN=9CG1Z^=`uBm$JBn{TR{3!oQ}68HMOy6_$C5X!Oy;@ASsW z9-afon~c~5-%6L(_Y-`fo<)};ab)d)gYg>E__LW`RUT0K z^ZP&^^(zI9#}z$&Agl9{PgiX(E-y8F%G;IIEz zd5yRSgF@YCA*gz6>X{!0=LYrNGcIa9YiEze1+~Vx!nMwFQU_cy2BW|W1@FOmgB~|; zX$p;8?^Bg9!KRcpwDZHnX;ILPA4h-8EO$4|{ptKs>2bo~q1Lw}5vs%DPQQ&=hq}LY z@%ASLi-LoV-w`QJ51vn!DeK*)h z+xaGe{{t?({sh0`W-qMFDNtDe>rHlEiVAwyzf4;Rz2bT#XnnKVaEE`z_MK@zRV(U6 z%eiB*m!XCPx!93riR5K*#j@?&-_KljRJ7PD7;KnO(k_9(&lHVr%A7sdU&`;V+JoA~ z)hL)!rz(3G%oQYXk#U}>7gr;xAkYz_r@*6Nxdl1oybn*%Y(P{gubXHWl+ zJ2KF%i^&`En%!+=bR4Q@GGmyEZEaF)@C_U6HVCzn&xtU+f^y*Uo%Y$M=EkcsRhBQ8 z@_1%F&~0~Dua7zbX%$e%VU3;BQOBY93BkmI-BGrTltBSI z4S?5^aLq{3Be#TZ?&MYctTPAmxr;ZFQo$;2C46MNhjGRba`(;JxfP#;H{zO_O6Zdn{#L~w^IG}ZfAk< z!%5(r&=cn0;ylUrS|$1<{ZxmHY`FpqpBx@BQo;1U@HwYZzZ&hsYk3rktT2ijuafYl zP9ZtO=3R6tUFRP1(qDJ?{?>{aSJdrbD5~!WcaKaL&fn1lZ1NTXgqivHQrP#iC=%^= z5=|-9q*R1+YQn!J|99cx1@sPwrT-#rU%%zwNo`t?qs7b-?nKUCjka_Mo~No1`#rLN<;yfGqB?~IFa&5e}!#i zeE-hsbRLZr^l4)KQOGu69YVY8GL(A@_13b1$hlg9e}Xz3A3pd+AK==^)exi#NgzB6 z6s`^#Egr)ky}Ai8Z)-{G+ACID_=|F(&0rWVN@pi=DGz_Sb^VdN88Rg!G}pM{)kqYJ zIAJ?&v<%p$nDUt`-m4_|_DF}QtSyDHvadrJhC6odKL$QWSxeTJ{(e-BGa~FZInrfG zjP8iE*fIv15BZm7RWcHYDo^_N|7@v1*CZEMh2GpdG?>&mZzZv{dL;iA(HAG>cnUMr zT7FAR9jdm@RQHlXl0ADjFcCty`y1~Q^E)0}Wz+UN+P74NRt?|vZlpKCVr)b_0OiD= z^l36E@~AT!UjpGfTX0ie+}4arw2>82{Y!EJG40nBP6CBvh!8;L$D}Ye<}78J1QoWi zxZP%bvA#&0{m`O=LR~H>f#r?;YGAOHum^oStH{BTVO*HE>&ePBe1B;hYt|Z z_lfdd0Qy_oGOsUcu-$)L;IrPEt9|0dQFcO^5`NF0RqF1!9RsWNQLvr%@5F+uj2o5eP?oDEF|gn0GnYXgreecSYLBNxyS=c+(MQ8GG>CFo2TM#8rU-v6O7(Et(KGurE zDB+<@{IE!^{ZRgFW>*YJ5Ni)^eVLA$RPc26h;7jwdZTL zJG^@Bl3xk6K|zz{rJOs=rNfJsf*qAUv}#Q(p@vzt3&x+3%);hL<-(u%=U6&RUsnYY z3b<_{-%(Ogl^^Mv`1)5X+a zvvn0?g@}05dX-pjk3=}T{KI^}9lq;bR%-s^8;&%7V7b<||0zxd!{|z*#uIzDu9s6o zeh;_E>Y9rVzP+orNvrkHu`{oQHQ)s{{aN#{C04?JUqC(FflH6xFuCR6f|KQ5^s)Fd ze8?sk`?O~ZJbo_2lGcLq(h{GTbR5z@t&9FRPYAOQv|aGbtzZuW^^o|xPjN|64J|pf z*PPuO9vq!cQaj}mT?LL2JYrwgjhlS+=1*$HCP4p7HR=E7Ha#z&s(0NUkIbi)|GmA* zmHu+pf4oZCT=$Tw>rn3b*BvTa4L=Kh72L8HxoN7=d96I{xq5OPN@n=wL*4ti5om$zr`bKif{?MeQpO_EXxigD=r73@m8rJ=2tf_RAXb)&uS0vTc*X) zX=B;T4^=I%3#UWNk=(7P?593*Ee(w0*e;k?jy_%A%$$!b#lZQxDi{!080&eH*M%+aUs7nn~Z`O72@8bCzvddQK?W6lx zcEYKaRm5|AlBaS4zBcYxFuBb&Gs?KO|8&f8ix~6_Kd(%Qc$%Bj%{}y=o~BSinwVdzGw8 zc!U~zh5^h8ydRG(>3P@Q6?sL*_K(cY&Zxj(Mp1U$1{a~gKx1v;CfTi~u9^VOWCpA} zTJL@tXw}LwDdo!mye0Ri)q3>{)s)6)OUi$fO8kWWIuYG(c)yOm$$Bvn(j5O4NJiPV zr_G4g8#~{oQ{&`He;(+k^=?$#xaV|T}DO1X?Ctb-*e3dBv!QZb^8qie+wLtCsvF0UyE3qFQ z*}?qlnOw64%GyB%8z!InZE^~7q%yo%0AltDs(WeS_6LdTT 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 A3IG5A literal 0 HcmV?d00001 diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/neoforged_icon.png b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/neoforged_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..87190e53d46268658e1a41943651541cfb9bb781 GIT binary patch literal 2400 zcmai0eOOc19S%Y#g`#sRqbLxrwNhh}n=nIjNuZhlVG`scAlMcN$ql(Cxi{wCKtL!K zhtxrQ9(amYfd>lWoTwcJ5VVzAAECPX@MWhP64Y*HtyrXNc8VW6`H&CM?T>qsbKm#( zp5O2NopX{s3Gp#LUjAM*8qFtGB2EIIO8eva3oz<$Rm*5Jk2Xw}Mx{yP5Cx%Q$(2Ml z$};Kn0H@IektV%dp+PBnHkyOsLTLOCtc+OBiMF?&0)dp*>nn1y@YN@=F#zi$hFu{fo#_>HntdbaToesi<6Fgv-=9IXzo|l;!JDb`nYwMuP&4 z%0+Q%mBR_UFGvDrLbYjPOo!qma90R%VE8#S{bi_#&=Q7Zu&q!K<_NNThs5gSIp`Kl zNvUVlW`H;p&zXnVv(p;w?M=ZkP!D%TEo}x9LSbC?tcB8F2u=3_apWf@lpwuOhu{np zD-tCb2o(cC`7 zKwIXugGLLfG**r>b|w1tMh<_ZI=Hc`VPDVkpPGEbSRH<)<+@9H%NO@WcYBAYaI7l@ z-|}(i#GR7vo$vl*>s!$3Q|~PeJ9c#v-&Q63gH2FBDYWGyHhz{*^T%x&VV>W|#e4tf zixZ{y8e49@zA(3z8VF3JjJ&sBZ#-OdUC#MV+MV8VzpMB-w)!w53tJYrMaEnsSS_&Z zOO{%F@#Lz?Eva;YF<^BFX*!^T<^ph-p@!n&9U(0P-)}OX| zL)FTn#@o#WaoO+v>ywIQuUMWIPaW-z+0FPiAmd0ex8hWw`dUfS035N{r-}0Di0S-V zHfdQ1KV%iO9_{-1T$fwU{uJhEcUw*WL_>6&B>m)`cfZ?i5`K6aZp+wJ^v6L`Wd#mKaZyL}=taZ|V=; zd2>AS=+UmxpIcBel6Vtcwm=jNy9R}(=AtE$d}{sl-s#1Y-p8*jt^Br1C|a?qHDDGC z@cqrYW}j`m_>4}&`il31R(LCV_jf-Mp^O88*8+O*xVYc7?l?u1who#yLVbs)^`EWe ze|&V)xW~}s_OYRFvQw;|o*TVzI`#5j%Slzca@@b!R=;k4`+{(f-5aw)rib5b?cQ7` ztU58OiMjIQ4TImXbtJWVvUji#?zpnz$fd6S;i{gPptHG;&sq~bUn~7G!8ue!4_gP* zYRsR#Au;}KZX!*c=E8!Bgw6q`xqPZ8b9XD@P0Gin>IUF_x*ZlX-m`a8vRrHH*ZWPq z{lU7+HM+{pBSn@U2%_wnxMP`$=(3e3D?5k2r}kP4eR~$Ij9AhWSoEamTaXk4CKMPN@gn=PX(E+Ap1EzxsMG`t`?6n+yRs~lY7xT%KK3Ku*E^G1XKp;C` hF1u0RwiTKz3EamTaXdLef^~610;BS-1N90L0ql&7M)NZ_v_DwE>@I8Z>OdfSpH;z?#>ELDJh==E Y(<4|Uw(Ux&18QgRboFyt=akR{07Yyf00000 literal 0 HcmV?d00001 diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/squirrel.png b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/squirrel.png new file mode 100644 index 0000000000000000000000000000000000000000..3d1b2d6469bd7763de1959dd18d9155c496053d2 GIT binary patch literal 43999 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 RESOURCE_COMPARATOR = Comparator.comparing( + resource -> { + try { + return resource.toNativeBuffer().toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, + (o1, o2) -> { + if (o1.length != o2.length) { + return Integer.compareUnsigned(o1.length, o2.length); + } + for (int i = 0; i < o1.length; i++) { + if (o1[i] != o2[i]) { + return Integer.compareUnsigned(o1[i], o2[i]); + } + } + return 0; + }); +} diff --git a/gradle.properties b/gradle.properties index 1cbd9814f..4a3408a2c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,10 +15,10 @@ mixin_version=0.14.0+mixin.0.8.6 terminalconsoleappender_version=1.3.0 nightconfig_version=3.8.0 jetbrains_annotations_version=24.1.0 -slf4j_api_version=1.8.0-beta4 +slf4j_api_version=2.0.16 apache_maven_artifact_version=3.8.5 jarjar_version=0.4.1 -lwjgl_version=3.3.1 +lwjgl_version=3.3.3 jupiter_version=5.10.2 mockito_version=5.11.0 assertj_version=3.26.0 diff --git a/loader/src/main/java/net/neoforged/fml/loading/progress/Message.java b/loader/src/main/java/net/neoforged/fml/loading/progress/Message.java index 144ce3087..061820738 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/progress/Message.java +++ b/loader/src/main/java/net/neoforged/fml/loading/progress/Message.java @@ -32,7 +32,7 @@ public float[] getTypeColour() { return type.colour(); } - enum MessageType { + public enum MessageType { MC(1.0f, 1.0f, 1.0f), ML(0.0f, 0.0f, 0.5f), LOC(0.0f, 0.5f, 0.0f), From a4a4187f1b7d78585da575da484e2f250963f446 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sat, 19 Apr 2025 20:30:25 +0200 Subject: [PATCH 03/22] WIP --- .../fml/earlydisplay/DisplayWindow.java | 115 ++++++++------- .../fml/earlydisplay/package-info.java | 4 + .../fml/earlydisplay/render/GlDebug.java | 1 - .../fml/earlydisplay/render/GlState.java | 1 - .../render/LoadingScreenRenderer.java | 114 +++++++-------- .../render/MaterializedTheme.java | 34 +++-- .../render/MaterializedThemeSprites.java | 8 + .../earlydisplay/render/RenderContext.java | 67 ++++++++- .../render/elements/PerformanceElement.java | 59 +++----- .../render/elements/ProgressBarsElement.java | 68 ++------- .../render/elements/StartupLogElement.java | 13 +- .../render/elements/package-info.java | 4 + .../fml/earlydisplay/render/package-info.java | 4 + .../earlydisplay/theme/ClasspathResource.java | 4 + .../fml/earlydisplay/theme/FileResource.java | 3 +- .../fml/earlydisplay/theme/Theme.java | 67 +++++---- .../fml/earlydisplay/theme/ThemeColor.java | 58 ++------ .../earlydisplay/theme/ThemeColorScheme.java | 20 ++- .../theme/ThemeLoadingScreen.java | 16 ++ .../earlydisplay/theme/ThemeSerializer.java | 137 +++++++++++++----- .../fml/earlydisplay/theme/ThemeSprites.java | 19 +++ .../elements/ThemeDecorativeElement.java | 18 +++ .../theme/elements/ThemeElement.java | 18 ++- .../theme/elements/ThemeImageElement.java | 2 +- .../theme/elements/ThemeLabelElement.java | 2 +- .../elements/ThemePerformanceElement.java | 53 +------ .../elements/ThemeProgressBarsElement.java | 56 +------ .../elements/ThemeStartupLogElement.java | 7 +- .../theme/elements/package-info.java | 4 + .../fml/earlydisplay/theme/package-info.java | 4 + .../fml/earlydisplay/util/package-info.java | 4 + .../fml/earlydisplay/TestEarlyDisplay.java | 6 +- .../theme/ThemeSerializerTest.java | 6 +- 33 files changed, 528 insertions(+), 468 deletions(-) create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 52c77a545..33b2a29af 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -5,6 +5,48 @@ package net.neoforged.fml.earlydisplay; +import joptsimple.OptionParser; +import net.neoforged.fml.earlydisplay.render.LoadingScreenRenderer; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; +import net.neoforged.fml.loading.FMLConfig; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.fml.loading.progress.ProgressMeter; +import net.neoforged.fml.loading.progress.StartupNotificationManager; +import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.PointerBuffer; +import org.lwjgl.glfw.GLFWImage; +import org.lwjgl.glfw.GLFWVidMode; +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.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + import static org.lwjgl.glfw.GLFW.GLFW_CLIENT_API; import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_CREATION_API; import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MAJOR; @@ -46,47 +88,6 @@ import static org.lwjgl.glfw.GLFW.glfwWindowHintString; import static org.lwjgl.opengl.GL32C.GL_TRUE; -import java.awt.Desktop; -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; -import joptsimple.OptionParser; -import net.neoforged.fml.earlydisplay.render.LoadingScreenRenderer; -import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.ThemeColor; -import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; -import net.neoforged.fml.loading.FMLConfig; -import net.neoforged.fml.loading.FMLPaths; -import net.neoforged.fml.loading.progress.ProgressMeter; -import net.neoforged.fml.loading.progress.StartupNotificationManager; -import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.PointerBuffer; -import org.lwjgl.glfw.GLFWImage; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.MemoryUtil; -import org.lwjgl.util.tinyfd.TinyFileDialogs; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * 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. @@ -124,7 +125,8 @@ public class DisplayWindow implements ImmediateWindowProvider { private boolean maximized; private Map fonts; - private Runnable repaintTick = () -> {}; + private Runnable repaintTick = () -> { + }; private ThemeColor background; public DisplayWindow() { @@ -198,26 +200,21 @@ public Runnable initialize(String[] arguments) { } private static Theme loadTheme(boolean darkMode) { - Path themePath = getThemePath(darkMode); + Path themePath = getThemePath(); + var themeId = darkMode ? "darkmode" : "default"; + Theme theme; try { - theme = ThemeSerializer.load(themePath); - } catch (NoSuchFileException ignored) { - LOGGER.info("No theme found at {}", themePath); - theme = Theme.createDefaultTheme(darkMode); - if (Boolean.getBoolean("fml.writeMissingTheme")) { - ThemeSerializer.save(getThemePath(true), Theme.createDefaultTheme(true)); - ThemeSerializer.save(getThemePath(false), Theme.createDefaultTheme(false)); - } + theme = ThemeSerializer.load(themePath, themeId); } catch (Exception e) { LOGGER.error("Failed to load theme {}", themePath, e); - theme = Theme.createDefaultTheme(darkMode); + theme = Theme.createDefaultTheme(); } return theme; } - private static Path getThemePath(boolean darkMode) { - return FMLPaths.CONFIGDIR.get().resolve(darkMode ? "fml/theme_dark.json" : "fml/theme.json"); + private static Path getThemePath() { + return FMLPaths.CONFIGDIR.get().resolve("fml"); } // Called from NeoForge @@ -257,7 +254,8 @@ private void crashElegantly(String errorDetails) { thread.start(); try { thread.join(); - } catch (InterruptedException ignored) {} + } catch (InterruptedException ignored) { + } System.exit(1); } @@ -365,8 +363,8 @@ public void initWindow(@Nullable String mcVersion) { // Attempt setting the icon try (var glfwImgBuffer = GLFWImage.malloc(1); - var glfwImages = GLFWImage.malloc(); - var icon = theme.windowIcon().loadAsImage()) { + var glfwImages = GLFWImage.malloc(); + var icon = theme.windowIcon().loadAsImage()) { glfwImgBuffer.put(glfwImages.set(icon.width(), icon.height(), icon.imageData())); glfwImgBuffer.flip(); glfwSetWindowIcon(window, glfwImgBuffer); @@ -475,7 +473,8 @@ public long takeOverGlfwWindow() { } @Override - public void updateModuleReads(final ModuleLayer layer) {} + public void updateModuleReads(final ModuleLayer layer) { + } // Called from Neo public int getFramebufferTextureId() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java new file mode 100644 index 000000000..8b96a776d --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package net.neoforged.fml.earlydisplay; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java index 91720f7e8..329801a57 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java @@ -16,7 +16,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ApiStatus.Internal public final class GlDebug { private static final Logger LOG = LoggerFactory.getLogger(GlDebug.class); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java index 1d5e86d31..10f6e6312 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java @@ -55,7 +55,6 @@ * This class tracks the current state of various OpenGL state elements and only applies changes * when necessary, reducing overhead from redundant state changes. */ -@ApiStatus.Internal public final class GlState { // Viewport state private static int viewportX; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index a013421f1..2129e1274 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -1,5 +1,27 @@ package net.neoforged.fml.earlydisplay.render; +import net.neoforged.fml.earlydisplay.render.elements.ImageElement; +import net.neoforged.fml.earlydisplay.render.elements.LabelElement; +import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; +import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; +import net.neoforged.fml.earlydisplay.render.elements.RenderElement; +import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import org.lwjgl.opengl.GL32C; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; @@ -16,30 +38,6 @@ import static org.lwjgl.opengl.GL11C.glClear; import static org.lwjgl.opengl.GL11C.glGetString; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import net.neoforged.fml.earlydisplay.render.elements.ImageElement; -import net.neoforged.fml.earlydisplay.render.elements.LabelElement; -import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; -import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; -import net.neoforged.fml.earlydisplay.render.elements.RenderElement; -import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; -import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; -import org.lwjgl.opengl.GL32C; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class LoadingScreenRenderer implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(LoadingScreenRenderer.class); @@ -71,10 +69,10 @@ public class LoadingScreenRenderer implements AutoCloseable { * Nothing fancy, we just want to draw and render text. */ public LoadingScreenRenderer(ScheduledExecutorService scheduler, - long glfwWindow, - Theme theme, - String mcVersion, - String neoForgeVersion) { + long glfwWindow, + Theme theme, + String mcVersion, + String neoForgeVersion) { this.glfwWindow = glfwWindow; this.mcVersion = mcVersion; this.neoForgeVersion = neoForgeVersion; @@ -95,17 +93,8 @@ public LoadingScreenRenderer(ScheduledExecutorService scheduler, // we always render to an 854x480 texture and then fit that to the screen framebuffer = new EarlyFramebuffer(854, 480); - // TODO this.elements = new ArrayList<>(Arrays.asList( - // TODO RenderElement.fox(font), - // TODO RenderElement.logMessageOverlay(font), - // TODO RenderElement.forgeVersionOverlay(font, ), - // TODO RenderElement.performanceBar(font), - // TODO RenderElement.progressBars(font))); - // TODO if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SQUIR) || (date.get(Calendar.MONTH) == Calendar.APRIL && date.get(Calendar.DAY_OF_MONTH) == 1)) - // TODO this.elements.add(0, RenderElement.squir()); - // Set the clear color based on the colour scheme - var background = theme.colorScheme().background(); + var background = theme.colorScheme().screenBackground(); GlState.clearColor(background.r(), background.g(), background.b(), 1f); GL32C.glClear(GL_COLOR_BUFFER_BIT); @@ -113,16 +102,26 @@ public LoadingScreenRenderer(ScheduledExecutorService scheduler, GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glfwMakeContextCurrent(0); this.automaticRendering = scheduler.scheduleWithFixedDelay(this::renderToScreen, 50, 50, TimeUnit.MILLISECONDS); - // TODO this.performanceTick = scheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS); // schedule a 50 ms ticker to try and smooth out the rendering scheduler.scheduleWithFixedDelay(() -> animationFrame++, 1, 50, TimeUnit.MILLISECONDS); } private List loadElements() { - var themeElements = theme.theme().elements(); - var elements = new ArrayList(themeElements.size()); + var elements = new ArrayList(); - for (var element : themeElements) { + var loadingScreen = theme.theme().loadingScreen(); + if (!loadingScreen.performance().visibility()) { + elements.add(new PerformanceElement(theme, loadingScreen.performance())); + } + if (!loadingScreen.startupLog().visibility()) { + elements.add(new StartupLogElement(theme, loadingScreen.startupLog())); + } + if (!loadingScreen.progressBars().visibility()) { + elements.add(new ProgressBarsElement(theme, loadingScreen.progressBars())); + } + + // Add decorative elements + for (var element : theme.theme().decoration()) { elements.add(loadElement(element)); } @@ -131,12 +130,8 @@ private List loadElements() { private RenderElement loadElement(ThemeElement element) { var renderElement = switch (element) { - case ThemeImageElement imageElement -> new ImageElement(imageElement.id(), theme, Texture.create(imageElement.texture())); - - case ThemeStartupLogElement startupLogElement -> new StartupLogElement( - startupLogElement.id(), - theme, - theme.theme().colorScheme().text()); + case ThemeImageElement imageElement -> + new ImageElement(imageElement.id(), theme, Texture.create(imageElement.texture())); case ThemeLabelElement labelElement -> { var version = mcVersion + "-" + neoForgeVersion.split("-")[0]; @@ -146,26 +141,21 @@ yield new LabelElement( labelElement.text().replace("${version}", version)); } - case ThemeProgressBarsElement progressBarsElement -> new ProgressBarsElement( - progressBarsElement.id(), - theme, - progressBarsElement); + default -> + throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); + }; - case ThemePerformanceElement performanceElement -> new PerformanceElement( - performanceElement.id(), - theme, - performanceElement); + applyBaseProperties(element, renderElement); - default -> throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); - }; + return renderElement; + } + private static void applyBaseProperties(ThemeElement element, RenderElement renderElement) { renderElement.setLeft(element.left()); renderElement.setTop(element.top()); renderElement.setRight(element.right()); renderElement.setBottom(element.bottom()); renderElement.setMaintainAspectRatio(element.maintainAspectRatio()); - - return renderElement; } public void stopAutomaticRendering() throws TimeoutException, InterruptedException { @@ -209,7 +199,7 @@ public void renderToScreen() { glfwGetFramebufferSize(glfwWindow, w, h); GlState.viewport(0, 0, w[0], h[0]); - framebuffer.blitToScreen(this.theme.theme().colorScheme().background(), w[0], h[0]); + framebuffer.blitToScreen(this.theme.theme().colorScheme().screenBackground(), w[0], h[0]); // Swap buffers; we're done glfwSwapBuffers(glfwWindow); @@ -232,7 +222,7 @@ public void renderToFramebuffer() { framebuffer.activate(); // Clear the screen to our color - var background = theme.theme().colorScheme().background(); + var background = theme.theme().colorScheme().screenBackground(); GlState.clearColor(background.r(), background.g(), background.b(), 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); GlState.enableBlend(true); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java index b05a80bf0..bb2a9470d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java @@ -1,9 +1,13 @@ package net.neoforged.fml.earlydisplay.render; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeResource; +import net.neoforged.fml.earlydisplay.theme.ThemeShader; +import net.neoforged.fml.earlydisplay.theme.ThemeSprites; + import java.io.IOException; import java.util.HashMap; import java.util.Map; -import net.neoforged.fml.earlydisplay.theme.Theme; /** * A themes resources loaded for rendering at runtime. @@ -11,17 +15,19 @@ public record MaterializedTheme( Theme theme, Map fonts, - Map shaders) implements AutoCloseable { + Map shaders, + MaterializedThemeSprites sprites) implements AutoCloseable { public static MaterializedTheme materialize(Theme theme) { return new MaterializedTheme( theme, - loadFonts(theme), - loadShaders(theme)); + loadFonts(theme.fonts()), + loadShaders(theme.shaders()), + loadSprites(theme.sprites())); } - private static Map loadShaders(Theme theme) { - var shaders = new HashMap(theme.shaders().size()); - for (var entry : theme.shaders().entrySet()) { + private static Map loadShaders(Map themeShaders) { + var shaders = new HashMap(themeShaders.size()); + for (var entry : themeShaders.entrySet()) { var shader = ElementShader.create( entry.getKey(), entry.getValue().vertexShader(), @@ -31,9 +37,9 @@ private static Map loadShaders(Theme theme) { return shaders; } - private static Map loadFonts(Theme theme) { - var fonts = new HashMap(theme.fonts().size()); - for (var entry : theme.fonts().entrySet()) { + private static Map loadFonts(Map themeFonts) { + var fonts = new HashMap(themeFonts.size()); + for (var entry : themeFonts.entrySet()) { try { fonts.put(entry.getKey(), new SimpleFont(entry.getValue(), 1)); } catch (IOException e) { @@ -43,6 +49,14 @@ private static Map loadFonts(Theme theme) { return fonts; } + private static MaterializedThemeSprites loadSprites(ThemeSprites sprites) { + return new MaterializedThemeSprites( + Texture.create(sprites.progressBarBackground()), + Texture.create(sprites.progressBarForeground()), + Texture.create(sprites.progressBarIndeterminate()) + ); + } + public SimpleFont getFont(String fontId) { var font = fonts.getOrDefault(fontId, fonts.get(Theme.FONT_DEFAULT)); if (font == null) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java new file mode 100644 index 000000000..d06f0a6cf --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java @@ -0,0 +1,8 @@ +package net.neoforged.fml.earlydisplay.render; + +public record MaterializedThemeSprites( + Texture progressBarBackground, + Texture progressBarForeground, + Texture progressBarIndeterminate +) { +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java index 0e296b34c..1404a03ac 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -1,11 +1,13 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; - -import java.util.List; import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.util.Bounds; +import java.util.List; + +import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; + public record RenderContext( SimpleBufferBuilder sharedBuffer, MaterializedTheme theme, @@ -62,4 +64,63 @@ public void renderText(float x, float y, SimpleFont font, List 0) { + float f = (animationFrame() % 200) / 100.0f; + if (f > 1) { + f = 1 - (f - 1); + } + barX = (int) (f * availableSpace); + } + blitTexture( + sprites.progressBarIndeterminate(), + backgroundBounds.left() + barX, + backgroundBounds.top(), + barWidth, + backgroundBounds.height()); + } else { + // Indeterminate progress bars are rendered as a 20% piece that's scrolling left-to-right and then resets + var centerPercentage = (animationFrame() % 120) - 10; + var start = Math.clamp((centerPercentage - 10) / 100f, 0f, 1f); + var end = Math.clamp((centerPercentage + 10) / 100f, 0f, 1f); + blitTexture( + sprites.progressBarIndeterminate(), + (int) (backgroundBounds.left() + backgroundBounds.width() * start), + backgroundBounds.top(), + (int) (backgroundBounds.width() * (end - start)), + backgroundBounds.height()); + } + } + + public void renderProgressBar(Bounds barBounds, float fillFactor) { + renderProgressBar(barBounds, fillFactor, ThemeColor.WHITE.toArgb()); + } + + public void renderProgressBar(Bounds barBounds, float fillFactor, int foregroundColor) { + fillFactor = Math.clamp(fillFactor, 0, 1); + + var sprites = theme.sprites(); + + blitTexture(sprites.progressBarBackground(), barBounds); + + GlState.scissorTest(true); + GlState.scissorBox( + (int) barBounds.left(), + (int) barBounds.top(), + (int) (barBounds.width() * fillFactor), + (int) barBounds.height()); + blitTexture(sprites.progressBarForeground(), barBounds, foregroundColor); + GlState.scissorTest(false); + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java index 644439352..900ced711 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java @@ -1,24 +1,23 @@ package net.neoforged.fml.earlydisplay.render.elements; import com.sun.management.OperatingSystemMXBean; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import net.neoforged.fml.earlydisplay.render.GlState; import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.render.Texture; import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; import net.neoforged.fml.earlydisplay.util.Bounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + public class PerformanceElement extends RenderElement { private static final Logger LOG = LoggerFactory.getLogger(PerformanceElement.class); @@ -28,24 +27,13 @@ public class PerformanceElement extends RenderElement { private Future performanceUpdateFuture; private volatile PerformanceInfo currentPerformanceData; - private final Texture barBackground; - private final Texture barForeground; - private final float[] lowColorHsb; - private final float[] highColorHsb; - - public PerformanceElement(String id, MaterializedTheme theme, ThemePerformanceElement element) { - super(id, theme); + public PerformanceElement(MaterializedTheme theme, ThemePerformanceElement settings) { + super(settings.id(), theme); osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); memoryBean = ManagementFactory.getMemoryMXBean(); performanceUpdateFuture = CompletableFuture.runAsync(this::updatePerformanceData); - - this.barBackground = Texture.create(element.barBackground()); - this.barForeground = Texture.create(element.barForeground()); - - this.lowColorHsb = element.lowColor().toHsb(); - this.highColorHsb = element.highColor().toHsb(); } @Override @@ -63,26 +51,20 @@ public void render(RenderContext context) { var areaBounds = resolveBounds(context.availableWidth(), context.availableHeight(), 250, 50); float memoryBarFill = performanceData.memory(); - final int colour = ThemeColor.ofHsb( - lowColorHsb[0] + (highColorHsb[0] - lowColorHsb[0]) * memoryBarFill, - lowColorHsb[1] + (highColorHsb[1] - lowColorHsb[1]) * memoryBarFill, - lowColorHsb[2] + (highColorHsb[2] - lowColorHsb[2]) * memoryBarFill).toArgb(); + // Interpolate between the low/high colors set in the theme based on current memory usage + var color = ThemeColor.lerp( + theme.theme().colorScheme().memoryLowColor(), + theme.theme().colorScheme().memoryHighColor(), + memoryBarFill + ); var barBounds = new Bounds( areaBounds.left(), areaBounds.top(), areaBounds.right(), - areaBounds.top() + barBackground.height()); - context.blitTexture(barBackground, barBounds); - GlState.scissorTest(true); - memoryBarFill = 0.5f; - GlState.scissorBox( - (int) barBounds.left(), - (int) barBounds.top(), - (int) (barBounds.width() * memoryBarFill), - (int) barBounds.height()); - context.blitTexture(barForeground, barBounds, colour); - GlState.scissorTest(false); + areaBounds.top() + theme.sprites().progressBarBackground().height()); + + context.renderProgressBar(barBounds, memoryBarFill, color.toArgb()); // Draw the detailed performance text centered below the progress bar var textMeasurement = font.measureText(performanceData.text()); @@ -121,5 +103,6 @@ private void updatePerformanceData() { } } - private record PerformanceInfo(long createdNanos, float memory, String text) {} + private record PerformanceInfo(long createdNanos, float memory, String text) { + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java index 802a125e8..d5a2ad6f3 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java @@ -1,32 +1,23 @@ package net.neoforged.fml.earlydisplay.render.elements; -import java.util.List; -import net.neoforged.fml.earlydisplay.render.GlState; import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.render.Texture; import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.loading.progress.StartupNotificationManager; +import java.util.List; + public class ProgressBarsElement extends RenderElement { private static final int BAR_AREA_WIDTH = 400; private static final int BAR_AREA_HEIGHT = 200; - private final ThemeProgressBarsElement themeElement; - private final Texture background; - private final Texture foreground; - private final Texture foregroundIndeterminate; + private final ThemeProgressBarsElement settings; - public ProgressBarsElement(String id, - MaterializedTheme theme, - ThemeProgressBarsElement themeElement) { - super(id, theme); - this.background = Texture.create(themeElement.background()); - this.foreground = Texture.create(themeElement.foreground()); - this.foregroundIndeterminate = Texture.create(themeElement.foregroundIndeterminate()); - this.themeElement = themeElement; + public ProgressBarsElement(MaterializedTheme theme, ThemeProgressBarsElement settings) { + super(settings.id(), theme); + this.settings = settings; } @Override @@ -47,58 +38,21 @@ public void render(RenderContext context) { areaBounds.top() + yOffset, font, List.of(new SimpleFont.DisplayText(text, theme.theme().colorScheme().text().toArgb()))); - yOffset += font.lineSpacing() + themeElement.labelGap(); + yOffset += font.lineSpacing() + settings.labelGap(); } var barBounds = new Bounds( areaBounds.left(), areaBounds.top() + yOffset, areaBounds.right(), - areaBounds.top() + yOffset + background.height()); - context.blitTexture(background, barBounds); + areaBounds.top() + yOffset + theme.sprites().progressBarBackground().height()); if (progress.steps() == 0) { - if (themeElement.indeterminateBounce()) { - // Indeterminate progress bars are rendered as a 20% piece that travels back and forth - var barX = 0; - var barWidth = (int) (barBounds.width() * 0.2f); - var availableSpace = (int) (barBounds.width() - barWidth); - if (availableSpace > 0) { - float f = (context.animationFrame() % 200) / 100.0f; - if (f > 1) { - f = 1 - (f - 1); - } - barX = (int) (f * availableSpace); - } - context.blitTexture( - foregroundIndeterminate, - barBounds.left() + barX, - barBounds.top(), - barWidth, - barBounds.height()); - } else { - // Indeterminate progress bars are rendered as a 20% piece that's scrolling left-to-right and then resets - var centerPercentage = (context.animationFrame() % 120) - 10; - var start = Math.clamp((centerPercentage - 10) / 100f, 0f, 1f); - var end = Math.clamp((centerPercentage + 10) / 100f, 0f, 1f); - context.blitTexture( - foregroundIndeterminate, - (int) (barBounds.left() + barBounds.width() * start), - barBounds.top(), - (int) (barBounds.width() * (end - start)), - barBounds.height()); - } + context.renderIndeterminateProgressBar(barBounds); } else { - GlState.scissorTest(true); - GlState.scissorBox( - (int) barBounds.left(), - (int) barBounds.top(), - (int) (barBounds.width() * progress.progress()), - (int) barBounds.height()); - context.blitTexture(foreground, barBounds); - GlState.scissorTest(false); + context.renderProgressBar(barBounds, progress.progress()); } - yOffset += barBounds.height() + themeElement.barGap(); + yOffset += barBounds.height() + settings.barGap(); } } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java index 712f34cde..fe64fd684 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java @@ -1,22 +1,25 @@ package net.neoforged.fml.earlydisplay.render.elements; -import java.util.ArrayList; -import java.util.List; import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.earlydisplay.util.Size; import net.neoforged.fml.loading.progress.Message; import net.neoforged.fml.loading.progress.StartupNotificationManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + public class StartupLogElement extends RenderElement { private ThemeColor textColor; - public StartupLogElement(String id, MaterializedTheme theme, ThemeColor textColor) { - super(id, theme); - this.textColor = textColor; + public StartupLogElement(MaterializedTheme theme, ThemeStartupLogElement settings) { + super(settings.id(), theme); + this.textColor = Objects.requireNonNullElseGet(textColor, () -> theme.theme().colorScheme().text()); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java new file mode 100644 index 000000000..e5692d1e5 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package net.neoforged.fml.earlydisplay.render.elements; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java new file mode 100644 index 000000000..78a382707 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package net.neoforged.fml.earlydisplay.render; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java index 618a64492..a0c08b092 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java @@ -5,6 +5,10 @@ import java.nio.ByteBuffer; import org.lwjgl.system.MemoryUtil; +/** + * A resource loaded from the loading screens classloader. + * @param path Path to a file. This is expected to be an absolute path that doesn't start with a {@code /}. + */ public record ClasspathResource(String path) implements ThemeResource { public NativeBuffer toNativeBuffer() throws IOException { var resource = getClass().getClassLoader().getResource(path); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java index a43bd5268..f9b696843 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java @@ -20,7 +20,8 @@ public NativeBuffer toNativeBuffer() throws IOException { var buffer = ByteBuffer.allocateDirect((int) size).order(ByteOrder.nativeOrder()); channel.read(buffer); buffer.flip(); - return new NativeBuffer(buffer, ignored -> {}); + return new NativeBuffer(buffer, ignored -> { + }); } } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index d86dfd669..fe23f0870 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -1,7 +1,5 @@ package net.neoforged.fml.earlydisplay.theme; -import java.util.List; -import java.util.Map; import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; @@ -10,27 +8,54 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; import net.neoforged.fml.earlydisplay.util.StyleLength; +import java.util.List; +import java.util.Map; + /** * Defines a theme for the early display screen. + * + * @param windowIcon The icon used for the loading screen operating system window until Minecraft takes over. + * @param decoration Additional decoration elements. + * @param fonts Defines font assets. Must contain a font named 'default', which is used if fonts are not overridden + * for individual loading screen elements. + * @param shaders Defines the GLSL shaders used to draw elements of the loading screen. Overriding shaders is an advanced feature. + * @param colorScheme Defines the color scheme used by the loading screen. + * @param sprites Defines various sprites used by the loading screen. */ public record Theme( ThemeResource windowIcon, Map fonts, Map shaders, - List elements, - ThemeColorScheme colorScheme) { + List decoration, + ThemeColorScheme colorScheme, + ThemeSprites sprites, + ThemeLoadingScreen loadingScreen +) { public static final String FONT_DEFAULT = "default"; public static final String SHADER_GUI = "gui"; public static final String SHADER_FONT = "font"; public static final String SHADER_COLOR = "color"; - public static Theme createDefaultTheme(boolean darkMode) { + + public static Theme createDefaultTheme() { + var sprites = new ThemeSprites( + new ThemeTexture( + classpathResource("progress_bar_bg.png"), + new TextureScaling.NineSlice(40, 20, 2, 2, 2, 2, true, true)), + new ThemeTexture( + classpathResource("progress_bar_fg.png"), + new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)), + new ThemeTexture( + classpathResource("progress_bar_fg.png"), + new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)), + false + ); + var squir = new ThemeImageElement(); squir.setId("squir"); squir.setTexture(new ThemeTexture(classpathResource("squirrel.png"), new TextureScaling.Stretch(112, 112))); var startupLog = new ThemeStartupLogElement(); - startupLog.setId("startupLog"); startupLog.setLeft(StyleLength.ofPoints(10)); startupLog.setBottom(StyleLength.ofPoints(10)); @@ -50,21 +75,7 @@ public static Theme createDefaultTheme(boolean darkMode) { forgeVersion.setBottom(StyleLength.ofPoints(10)); forgeVersion.setRight(StyleLength.ofPoints(10)); - var barBackground = new ThemeTexture( - classpathResource("progress_bar_bg.png"), - new TextureScaling.NineSlice(40, 20, 2, 2, 2, 2, true, true)); - var barForeground = new ThemeTexture( - classpathResource("progress_bar_fg.png"), - new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)); - var barIndeterminate = new ThemeTexture( - classpathResource("progress_bar_fg.png"), - new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)); - var progressBars = new ThemeProgressBarsElement(); - progressBars.setId("progressBars"); - progressBars.setBackground(barBackground); - progressBars.setForeground(barForeground); - progressBars.setForegroundIndeterminate(barIndeterminate); progressBars.setLabelGap(4); progressBars.setBarGap(5); progressBars.setLeft(StyleLength.ofPoints(220)); @@ -73,11 +84,6 @@ public static Theme createDefaultTheme(boolean darkMode) { progressBars.setMaintainAspectRatio(false); var performance = new ThemePerformanceElement(); - performance.setId("performance"); - performance.setBarBackground(barBackground); - performance.setBarForeground(barForeground); - performance.setLowColor(ThemeColor.ofBytes(0, 127, 0)); - performance.setHighColor(ThemeColor.ofBytes(255, 127, 0)); performance.setLeft(StyleLength.ofPoints(220)); performance.setRight(StyleLength.ofPoints(220)); performance.setTop(StyleLength.ofPoints(10)); @@ -93,8 +99,15 @@ FONT_DEFAULT, classpathResource("Monocraft.ttf")), ThemeShader.DEFAULT_FONT, SHADER_COLOR, ThemeShader.DEFAULT_COLOR), - List.of(squir, fox, startupLog, forgeVersion, progressBars, performance), - ThemeColorScheme.DEFAULT); + List.of(squir, fox, forgeVersion), + ThemeColorScheme.DEFAULT, + sprites, + new ThemeLoadingScreen( + performance, + progressBars, + startupLog + ) + ); } private static ClasspathResource classpathResource(String name) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java index 870961f2c..0b838376d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java @@ -5,6 +5,7 @@ public record ThemeColor(float r, float g, float b, float a) { public static final ThemeColor WHITE = new ThemeColor(1, 1, 1, 1); + public static ThemeColor ofBytes(int r, int g, int b, int a) { return new ThemeColor(r / 255.f, g / 255.f, b / 255.f, a / 255.f); } @@ -55,54 +56,15 @@ public int aByte() { return (int) (a * 256); } - 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 = Math.clamp((int) (f4 * 255.0F), 0, 255); - int k = Math.clamp((int) (f5 * 255.0F), 0, 255); - int l = Math.clamp((int) (f6 * 255.0F), 0, 255); - return 0xFF << 24 | j << 16 | k << 8 | l; + public static ThemeColor lerp(ThemeColor a, ThemeColor b, float f) { + var hsbA = a.toHsb(); + var hsbB = b.toHsb(); + + return ofHsb( + hsbA[0] + (hsbB[0] - hsbA[0]) * f, + hsbA[1] + (hsbB[1] - hsbA[1]) * f, + hsbA[2] + (hsbB[2] - hsbA[2]) * f + ); } public float[] toHsb() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java index 2df512cce..f3f5f9f2f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java @@ -1,7 +1,23 @@ package net.neoforged.fml.earlydisplay.theme; -public record ThemeColorScheme(ThemeColor background, ThemeColor text) { +/** + * @param screenBackground + * @param text + * @param memoryLowColor The color to use for coloring the bar when resource usage is low. + * The actual color will be interpolated between this and {@code highColor}. + * @param memoryHighColor The color to use for coloring the bar when resource usage is high. + * The actual color will be interpolated between this and {@code highColor}. + */ +public record ThemeColorScheme( + ThemeColor screenBackground, + ThemeColor text, + ThemeColor memoryLowColor, + ThemeColor memoryHighColor +) { public static final ThemeColorScheme DEFAULT = new ThemeColorScheme( ThemeColor.ofBytes(239, 50, 61), - ThemeColor.ofBytes(255, 255, 255)); + ThemeColor.ofBytes(255, 255, 255), + ThemeColor.ofBytes(0, 127, 0), + ThemeColor.ofBytes(255, 127, 0) + ); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java new file mode 100644 index 000000000..3fefc02e8 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java @@ -0,0 +1,16 @@ +package net.neoforged.fml.earlydisplay.theme; + +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; + +/** + * Describes the themable properties of the loading screen. + */ +public record ThemeLoadingScreen( + ThemePerformanceElement performance, + ThemeProgressBarsElement progressBars, + ThemeStartupLogElement startupLog +) { +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java index 17d3de5f0..9ee8927fc 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java @@ -16,36 +16,104 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.util.StyleLength; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.Map; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; -import net.neoforged.fml.earlydisplay.util.StyleLength; -import org.jetbrains.annotations.ApiStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -@ApiStatus.Internal public final class ThemeSerializer { private static final Logger LOG = LoggerFactory.getLogger(ThemeSerializer.class); + private static final int VERSION = 1; + + private ThemeSerializer() { + } + + public static Theme load(Path baseDirectory, String id) throws IOException { + + var themeTree = readThemeTree(baseDirectory, id); + + createGson(baseDirectory) + + } + + private static JsonObject readThemeTree(Path baseDirectory, String id) throws IOException { + String filename = getThemeFilename(id); - private ThemeSerializer() {} + try (var in = Files.newInputStream(baseDirectory.resolve(filename))) { + return readThemeTree(baseDirectory, in); + } catch (NoSuchFileException ignored) { + } + + // Try to load it from the classpath instead + String classpathLocation = "/net/neoforged/fml/earlydisplay/" + filename; + try (var in = ThemeSerializer.class.getResourceAsStream(classpathLocation)) { + if (in == null) { + throw new NoSuchFileException("Failed to find embedded theme resource " + classpathLocation); + } + return readThemeTree(baseDirectory, in); + } + } - public static Theme load(Path path) throws IOException { - try (var in = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { - return createGson(path.toAbsolutePath().getParent()).fromJson(in, Theme.class); + private static JsonObject readThemeTree(Path baseDirectory, InputStream in) { + var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + + var themeRoot = createGson(baseDirectory).fromJson(reader, JsonObject.class); + var themeVersion = takeInt(themeRoot, "version"); + if (themeVersion == null || themeVersion != VERSION) { + throw new JsonParseException("Expected theme version " + VERSION + " but found: " + themeVersion); + } + + var extendsId = takeString(themeRoot, "extends"); + + return null; + } + + private static String getThemeFilename(String id) { + return "theme-" + id + ".json"; + } + + @Nullable + private static Integer takeInt(JsonElement el, String field) { + var primitive = takePrimitive(el, field); + return primitive == null ? null : primitive.getAsInt(); + } + + @Nullable + private static String takeString(JsonElement el, String field) { + var primitive = takePrimitive(el, field); + return primitive == null ? null : primitive.getAsString(); + } + + private static JsonPrimitive takePrimitive(JsonElement el, String field) { + if (!el.isJsonObject()) { + throw new JsonParseException("Expected " + el + " to be an object."); + } + var obj = (JsonObject) el; + var v = obj.remove(field); + if (v == null) { + return null; + } + if (!(v instanceof JsonPrimitive primitive)) { + throw new JsonParseException("Expected " + field + " of " + el + " to be a primitive"); } + return primitive; } public static void save(Path path, Theme theme) { @@ -57,12 +125,12 @@ public static void save(Path path, Theme theme) { } } - private static Gson createGson(Path outputFolder) { + private static Gson createGson(Path baseDirectory) { return new GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(TextureScaling.class, new TextureScalingSerializer()) .registerTypeAdapterFactory(new ThemeElementAdapterFactory()) - .registerTypeHierarchyAdapter(ThemeResource.class, new ThemeResourceAdapter(outputFolder)) + .registerTypeHierarchyAdapter(ThemeResource.class, new ThemeResourceAdapter(baseDirectory)) .registerTypeAdapter(UncompressedImage.class, new UncompressedImageSerializer()) .registerTypeAdapter(StyleLength.class, new StyleLengthAdapter()) .registerTypeAdapter(ThemeColor.class, new ThemeColorAdapter()) @@ -117,10 +185,10 @@ public JsonElement serialize(UncompressedImage value, Type typeOfSrc, JsonSerial } private static class ThemeResourceAdapter extends TypeAdapter { - private final Path themeFolder; + private final Path baseDirectory; - public ThemeResourceAdapter(Path themeFolder) { - this.themeFolder = themeFolder; + public ThemeResourceAdapter(Path baseDirectory) { + this.baseDirectory = baseDirectory; } @Override @@ -131,7 +199,7 @@ public void write(JsonWriter out, ThemeResource value) throws IOException { classpathResource.path().lastIndexOf('/'), classpathResource.path().lastIndexOf('\\')); var filename = classpathResource.path().substring(idx + 1); - var diskPath = themeFolder.resolve(filename); + var diskPath = baseDirectory.resolve(filename); try (var buffer = value.toNativeBuffer()) { Files.write(diskPath, buffer.toByteArray()); } catch (IOException e) { @@ -140,7 +208,7 @@ public void write(JsonWriter out, ThemeResource value) throws IOException { out.value(filename); } case FileResource fileResource -> { - var diskPath = themeFolder.resolve(fileResource.file().getName()); + var diskPath = baseDirectory.resolve(fileResource.file().getName()); Files.copy(fileResource.file().toPath(), diskPath, StandardCopyOption.REPLACE_EXISTING); out.value(fileResource.file().getName()); } @@ -153,7 +221,7 @@ public ThemeResource read(JsonReader in) throws IOException { if (text.startsWith("classpath:")) { return new ClasspathResource(text.substring("classpath:".length())); } - return new FileResource(themeFolder.resolve(text).toFile()); + return new FileResource(baseDirectory.resolve(text).toFile()); } } @@ -215,12 +283,9 @@ public JsonElement serialize(TextureScaling src, Type typeOfSrc, JsonSerializati } private static class ThemeElementAdapterFactory implements TypeAdapterFactory { - private static final Map> TYPE_MAP = Map.of( + private static final Map> TYPE_MAP = Map.of( "image", ThemeImageElement.class, - "label", ThemeLabelElement.class, - "performance", ThemePerformanceElement.class, - "progress", ThemeProgressBarsElement.class, - "startupLog", ThemeStartupLogElement.class); + "label", ThemeLabelElement.class); @SuppressWarnings("unchecked") @Override @@ -228,13 +293,13 @@ public TypeAdapter create(Gson gson, TypeToken type) { if (type == null) { return null; } - if (!ThemeElement.class.isAssignableFrom(type.getRawType())) { + if (!ThemeDecorativeElement.class.isAssignableFrom(type.getRawType())) { return null; } TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - Map> labelToDelegate = new HashMap<>(); - Map, TypeAdapter> subtypeToDelegate = new HashMap<>(); + Map> labelToDelegate = new HashMap<>(); + Map, TypeAdapter> subtypeToDelegate = new HashMap<>(); Map, String> subtypeToLabel = new HashMap<>(); for (var entry : TYPE_MAP.entrySet()) { var delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); @@ -243,9 +308,9 @@ public TypeAdapter create(Gson gson, TypeToken type) { subtypeToLabel.put(entry.getValue(), entry.getKey()); } - return (TypeAdapter) new TypeAdapter() { + return (TypeAdapter) new TypeAdapter() { @Override - public ThemeElement read(JsonReader in) throws IOException { + public ThemeDecorativeElement read(JsonReader in) throws IOException { var jsonElement = jsonElementAdapter.read(in); var labelJsonElement = jsonElement.getAsJsonObject().remove("type"); @@ -263,11 +328,11 @@ public ThemeElement read(JsonReader in) throws IOException { } @Override - public void write(JsonWriter out, ThemeElement value) throws IOException { - Class srcType = value.getClass(); + public void write(JsonWriter out, ThemeDecorativeElement value) throws IOException { + Class srcType = value.getClass(); String label = subtypeToLabel.get(srcType); // The registration in this map guarantees the type bound of the key equals that of the value - var delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + var delegate = (TypeAdapter) subtypeToDelegate.get(srcType); if (delegate == null) { throw new JsonParseException("cannot serialize theme element " + srcType.getName()); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java new file mode 100644 index 000000000..bcded0924 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java @@ -0,0 +1,19 @@ +package net.neoforged.fml.earlydisplay.theme; + +/** + * @param progressBarBackground The background image being rendered as the base for a progress bar. + * @param progressBarForeground The image that will be rendered on top of the background for progress bars that are being filled normally + * from the left. + * @param progressBarIndeterminate The image that will be rendered on top of the background for progress bars that are actively animating + * as an indeterminate progress bar. + * @param progressBarIndeterminateBounces Indicates that indeterminate progress bars use a fixed width sprite that bounces back and forth, + * instead of using a sprite that seems like it scrolls through the background (which causes the + * sprite to squish until it is 0 pixels wide at the edges). + */ +public record ThemeSprites( + ThemeTexture progressBarBackground, + ThemeTexture progressBarForeground, + ThemeTexture progressBarIndeterminate, + boolean progressBarIndeterminateBounces +) { +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java new file mode 100644 index 000000000..e2ada6a2d --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java @@ -0,0 +1,18 @@ +package net.neoforged.fml.earlydisplay.theme.elements; + +/** + * Decorative elements are additional elements that a theme can add to the screen that + * have no specific functionality. + */ +public abstract class ThemeDecorativeElement extends ThemeElement { + private String id; + + @Override + public String id() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java index a6610c55f..8fe887ce5 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -1,23 +1,25 @@ package net.neoforged.fml.earlydisplay.theme.elements; -import java.util.Objects; import net.neoforged.fml.earlydisplay.util.StyleLength; -public abstract class ThemeElement { - private String id; +import java.util.Objects; +public abstract class ThemeElement { + private boolean visibility = false; private boolean maintainAspectRatio = true; private StyleLength left = StyleLength.ofUndefined(); private StyleLength top = StyleLength.ofUndefined(); private StyleLength right = StyleLength.ofUndefined(); private StyleLength bottom = StyleLength.ofUndefined(); - public String id() { - return id; + public abstract String id(); + + public boolean visibility() { + return visibility; } - public void setId(String id) { - this.id = id; + public void setVisibility(boolean visibility) { + this.visibility = visibility; } public StyleLength left() { @@ -62,6 +64,6 @@ public void setMaintainAspectRatio(boolean maintainAspectRatio) { @Override public String toString() { - return id; + return id(); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java index f2495b348..a61f31821 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java @@ -2,7 +2,7 @@ import net.neoforged.fml.earlydisplay.theme.ThemeTexture; -public class ThemeImageElement extends ThemeElement { +public class ThemeImageElement extends ThemeDecorativeElement { private ThemeTexture texture; public ThemeTexture texture() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java index 75e1ade17..3a47b3b24 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java @@ -2,7 +2,7 @@ import java.util.Objects; -public class ThemeLabelElement extends ThemeElement { +public class ThemeLabelElement extends ThemeDecorativeElement { private String text = ""; public String text() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java index 0417206af..72379bc72 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java @@ -4,55 +4,8 @@ import net.neoforged.fml.earlydisplay.theme.ThemeTexture; public class ThemePerformanceElement extends ThemeElement { - /** - * The background image being rendered as the base for a progress bar. - */ - private ThemeTexture barBackground; - /** - * The image that will be rendered on top of the background for progress bars that are being filled normally - * from the left. - */ - private ThemeTexture barForeground; - /** - * The color to use for coloring the bar when resource usage is low. - * The actual color will be interpolated between this and {@code highColor}. - */ - private ThemeColor lowColor; - /** - * The color to use for coloring the bar when resource usage is high. - * The actual color will be interpolated between this and {@code highColor}. - */ - private ThemeColor highColor; - - public ThemeTexture barBackground() { - return barBackground; - } - - public void setBarBackground(ThemeTexture barBackground) { - this.barBackground = barBackground; - } - - public ThemeTexture barForeground() { - return barForeground; - } - - public void setBarForeground(ThemeTexture barForeground) { - this.barForeground = barForeground; - } - - public ThemeColor lowColor() { - return lowColor; - } - - public void setLowColor(ThemeColor lowColor) { - this.lowColor = lowColor; - } - - public ThemeColor highColor() { - return highColor; - } - - public void setHighColor(ThemeColor highColor) { - this.highColor = highColor; + @Override + public String id() { + return "performance"; } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java index 83201d3a0..b4a6af57a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java @@ -1,23 +1,6 @@ package net.neoforged.fml.earlydisplay.theme.elements; -import net.neoforged.fml.earlydisplay.theme.ThemeTexture; - public class ThemeProgressBarsElement extends ThemeElement { - /** - * The background image being rendered as the base for a progress bar. - */ - private ThemeTexture background; - /** - * The image that will be rendered on top of the background for progress bars that are being filled normally - * from the left. - */ - private ThemeTexture foreground; - /** - * The image that will be rendered on top of the background for progress bars that are actively animating - * as an indeterminate progress bar. - */ - private ThemeTexture foregroundIndeterminate; - /** * The gap in virtual pixels between a bars label and the bar itself. */ @@ -28,34 +11,9 @@ public class ThemeProgressBarsElement extends ThemeElement { */ private int barGap; - /** - * Makes the indeterminate progress bars bounce back and forth instead of trying to - * emulate an infinite scroll, which doesn't work that well with more complex progress bars. - */ - private boolean indeterminateBounce; - - public ThemeTexture background() { - return background; - } - - public void setBackground(ThemeTexture background) { - this.background = background; - } - - public ThemeTexture foreground() { - return foreground; - } - - public void setForeground(ThemeTexture foreground) { - this.foreground = foreground; - } - - public ThemeTexture foregroundIndeterminate() { - return foregroundIndeterminate; - } - - public void setForegroundIndeterminate(ThemeTexture foregroundIndeterminate) { - this.foregroundIndeterminate = foregroundIndeterminate; + @Override + public String id() { + return "progressBars"; } public int labelGap() { @@ -73,12 +31,4 @@ public int barGap() { public void setBarGap(int barGap) { this.barGap = barGap; } - - public boolean indeterminateBounce() { - return indeterminateBounce; - } - - public void setIndeterminateBounce(boolean indeterminateBounce) { - this.indeterminateBounce = indeterminateBounce; - } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java index 557db9ec6..8f2770987 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java @@ -1,3 +1,8 @@ package net.neoforged.fml.earlydisplay.theme.elements; -public class ThemeStartupLogElement extends ThemeElement {} +public class ThemeStartupLogElement extends ThemeElement { + @Override + public String id() { + return "startupLog"; + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java new file mode 100644 index 000000000..3a63210f6 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package net.neoforged.fml.earlydisplay.theme.elements; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java new file mode 100644 index 000000000..6dc959d55 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package net.neoforged.fml.earlydisplay.theme; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java new file mode 100644 index 000000000..cc2079f46 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package net.neoforged.fml.earlydisplay.util; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java index 8a4580a90..307d0bc2d 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java @@ -1,12 +1,14 @@ package net.neoforged.fml.earlydisplay; +import net.neoforged.fml.loading.FMLPaths; + import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import net.neoforged.fml.loading.FMLPaths; public class TestEarlyDisplay { public static void main(String[] args) throws Exception { + System.setProperty("java.awt.headless", "true"); System.setProperty("fml.writeMissingTheme", "true"); // Find the project directory by search for build.gradle upwards @@ -14,7 +16,7 @@ public static void main(String[] args) throws Exception { FMLPaths.loadAbsolutePaths(projectRoot); var window = new DisplayWindow(); - var periodicTick = window.initialize(new String[] { + var periodicTick = window.initialize(new String[]{ "--fml.mcVersion", "1.21.5", "--fml.neoForgeVersion", "21.5.123-beta" }); diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java index 205e03a5c..4aae6e1e9 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java @@ -15,11 +15,11 @@ class ThemeSerializerTest { @Test void testDefaultThemeRoundtrip() throws IOException { - var defaultTheme = Theme.createDefaultTheme(false); - Path themePath = tempDir.resolve("theme.json"); + var defaultTheme = Theme.createDefaultTheme(); + Path themePath = tempDir.resolve("theme-default.json"); ThemeSerializer.save(themePath, defaultTheme); - var loadedTheme = ThemeSerializer.load(themePath); + var loadedTheme = ThemeSerializer.load(tempDir, "default"); assertThat(loadedTheme) .usingComparatorForType(RESOURCE_COMPARATOR, ThemeResource.class) .usingRecursiveComparison() From 710695008369f910c16d928707d426432246260d Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 20 Apr 2025 17:22:35 +0200 Subject: [PATCH 04/22] reworked theme loading --- .../fml/earlydisplay/DisplayWindow.java | 2 +- .../render/LoadingScreenRenderer.java | 40 ++--- .../fml/earlydisplay/render/Texture.java | 6 +- .../render/elements/ImageElement.java | 7 +- .../render/elements/LabelElement.java | 12 +- .../render/elements/PerformanceElement.java | 4 +- .../render/elements/ProgressBarsElement.java | 4 +- .../render/elements/RenderElement.java | 22 ++- .../render/elements/StartupLogElement.java | 4 +- .../earlydisplay/theme/TextureScaling.java | 12 +- .../fml/earlydisplay/theme/Theme.java | 13 +- .../fml/earlydisplay/theme/ThemeColor.java | 8 +- .../earlydisplay/theme/ThemeSerializer.java | 140 ++++++++++++++---- .../theme/elements/ThemeElement.java | 10 ++ .../fml/earlydisplay/util/Placeholders.java | 18 +++ .../earlydisplay/theme/theme-darkmode.json | 8 + .../fml/earlydisplay/theme/theme-default.json | 140 ++++++++++++++++++ .../fml/earlydisplay/ExportThemes.java | 15 ++ .../fml/earlydisplay/TestEarlyDisplay.java | 23 +-- .../neoforged/fml/earlydisplay/TestUtil.java | 24 +++ .../theme/ThemeSerializerTest.java | 2 +- 21 files changed, 403 insertions(+), 111 deletions(-) create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-darkmode.json create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 33b2a29af..d07d2ac17 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -157,7 +157,7 @@ public Runnable initialize(String[] arguments) { FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH, winWidth); FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT, winHeight); - if (System.getenv("FML_EARLY_WINDOW_DARK") != null) { + if (Boolean.getBoolean("fml.earlyWindowDarkMode")) { this.darkMode = true; } else { try { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index 2129e1274..7fa91430a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; @@ -111,13 +112,13 @@ private List loadElements() { var loadingScreen = theme.theme().loadingScreen(); if (!loadingScreen.performance().visibility()) { - elements.add(new PerformanceElement(theme, loadingScreen.performance())); + elements.add(new PerformanceElement(loadingScreen.performance(), theme)); } if (!loadingScreen.startupLog().visibility()) { - elements.add(new StartupLogElement(theme, loadingScreen.startupLog())); + elements.add(new StartupLogElement(loadingScreen.startupLog(), theme)); } if (!loadingScreen.progressBars().visibility()) { - elements.add(new ProgressBarsElement(theme, loadingScreen.progressBars())); + elements.add(new ProgressBarsElement(loadingScreen.progressBars(), theme)); } // Add decorative elements @@ -129,33 +130,20 @@ private List loadElements() { } private RenderElement loadElement(ThemeElement element) { - var renderElement = switch (element) { - case ThemeImageElement imageElement -> - new ImageElement(imageElement.id(), theme, Texture.create(imageElement.texture())); - - case ThemeLabelElement labelElement -> { - var version = mcVersion + "-" + neoForgeVersion.split("-")[0]; - yield new LabelElement( - labelElement.id(), - theme, - labelElement.text().replace("${version}", version)); - } + return switch (element) { + case ThemeImageElement imageElement -> new ImageElement(imageElement, theme); + + case ThemeLabelElement labelElement -> new LabelElement( + labelElement, + theme, + Map.of( + "version", mcVersion + "-" + neoForgeVersion.split("-")[0] + ) + ); default -> throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); }; - - applyBaseProperties(element, renderElement); - - return renderElement; - } - - private static void applyBaseProperties(ThemeElement element, RenderElement renderElement) { - renderElement.setLeft(element.left()); - renderElement.setTop(element.top()); - renderElement.setRight(element.right()); - renderElement.setBottom(element.bottom()); - renderElement.setMaintainAspectRatio(element.maintainAspectRatio()); } public void stopAutomaticRendering() throws TimeoutException, InterruptedException { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java index 87f5e293e..05a5d88ea 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -1,6 +1,7 @@ package net.neoforged.fml.earlydisplay.render; import static org.lwjgl.opengl.GL11C.GL_LINEAR; +import static org.lwjgl.opengl.GL11C.GL_NEAREST; import static org.lwjgl.opengl.GL11C.GL_RGBA; import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D; import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MAG_FILTER; @@ -37,8 +38,9 @@ public static Texture create(ThemeTexture themeTexture) { GlState.activeTexture(GL_TEXTURE0); GlState.bindTexture2D(texId); GlDebug.labelTexture(texId, "EarlyDisplay " + themeTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + boolean linear = themeTexture.scaling().linearScaling(); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.imageData()); GlState.activeTexture(GL_TEXTURE0); return new Texture(texId, image.width(), image.height(), themeTexture.scaling(), themeTexture.animation()); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java index 5600bd6c4..27a066c64 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java @@ -3,13 +3,14 @@ import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; public class ImageElement extends RenderElement { private final Texture texture; - public ImageElement(String id, MaterializedTheme theme, Texture texture) { - super(id, theme); - this.texture = texture; + public ImageElement(ThemeImageElement element, MaterializedTheme theme) { + super(element, theme); + this.texture = Texture.create(element.texture()); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java index 829efacb3..31daeccca 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java @@ -1,18 +1,22 @@ package net.neoforged.fml.earlydisplay.render.elements; -import java.util.List; import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; import net.neoforged.fml.earlydisplay.util.Bounds; +import net.neoforged.fml.earlydisplay.util.Placeholders; import net.neoforged.fml.earlydisplay.util.Size; +import java.util.List; +import java.util.Map; + public class LabelElement extends RenderElement { private final String text; - public LabelElement(String id, MaterializedTheme theme, String text) { - super(id, theme); - this.text = text; + public LabelElement(ThemeLabelElement element, MaterializedTheme theme, Map placeholders) { + super(element, theme); + this.text = Placeholders.resolve(element.text(), placeholders); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java index 900ced711..ad7a7c714 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java @@ -27,8 +27,8 @@ public class PerformanceElement extends RenderElement { private Future performanceUpdateFuture; private volatile PerformanceInfo currentPerformanceData; - public PerformanceElement(MaterializedTheme theme, ThemePerformanceElement settings) { - super(settings.id(), theme); + public PerformanceElement(ThemePerformanceElement settings, MaterializedTheme theme) { + super(settings, theme); osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); memoryBean = ManagementFactory.getMemoryMXBean(); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java index d5a2ad6f3..e7a4f2663 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java @@ -15,8 +15,8 @@ public class ProgressBarsElement extends RenderElement { private final ThemeProgressBarsElement settings; - public ProgressBarsElement(MaterializedTheme theme, ThemeProgressBarsElement settings) { - super(settings.id(), theme); + public ProgressBarsElement(ThemeProgressBarsElement settings, MaterializedTheme theme) { + super(settings, theme); this.settings = settings; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java index 66626e771..b4731d628 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java @@ -9,6 +9,7 @@ import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.SimpleFont; import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.earlydisplay.util.StyleLength; @@ -16,17 +17,22 @@ public abstract class RenderElement implements AutoCloseable { private final String id; protected final MaterializedTheme theme; - private boolean maintainAspectRatio = true; - private StyleLength left = StyleLength.ofUndefined(); - private StyleLength top = StyleLength.ofUndefined(); - private StyleLength right = StyleLength.ofUndefined(); - private StyleLength bottom = StyleLength.ofUndefined(); + private boolean maintainAspectRatio; + private StyleLength left; + private StyleLength top; + private StyleLength right; + private StyleLength bottom; protected SimpleFont font; - public RenderElement(String id, MaterializedTheme theme) { - this.id = id; + public RenderElement(ThemeElement element, MaterializedTheme theme) { this.theme = theme; - this.font = theme.fonts().get(Theme.FONT_DEFAULT); + this.id = element.id(); + this.font = theme.getFont(element.font()); + this.left = element.left(); + this.top = element.top(); + this.right = element.right(); + this.bottom = element.bottom(); + this.maintainAspectRatio = element.maintainAspectRatio(); } public String id() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java index fe64fd684..5f340e0d1 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java @@ -17,8 +17,8 @@ public class StartupLogElement extends RenderElement { private ThemeColor textColor; - public StartupLogElement(MaterializedTheme theme, ThemeStartupLogElement settings) { - super(settings.id(), theme); + public StartupLogElement(ThemeStartupLogElement settings, MaterializedTheme theme) { + super(settings, theme); this.textColor = Objects.requireNonNullElseGet(textColor, () -> theme.theme().colorScheme().text()); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java index 166a06a33..4bd3be805 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java @@ -13,9 +13,15 @@ public sealed interface TextureScaling { */ int height(); - record Stretch(int width, int height) implements TextureScaling {} + boolean linearScaling(); - record Tile(int width, int height) implements TextureScaling {} + record Stretch(int width, int height, boolean linearScaling) implements TextureScaling { + } - record NineSlice(int width, int height, int left, int top, int right, int bottom, boolean stretchHorizontalFill, boolean stretchVerticalFill) implements TextureScaling {} + record Tile(int width, int height, boolean linearScaling) implements TextureScaling { + } + + record NineSlice(int width, int height, int left, int top, int right, int bottom, boolean stretchHorizontalFill, + boolean stretchVerticalFill, boolean linearScaling) implements TextureScaling { + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index fe23f0870..15eb4cf22 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -1,5 +1,6 @@ package net.neoforged.fml.earlydisplay.theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; @@ -26,7 +27,7 @@ public record Theme( ThemeResource windowIcon, Map fonts, Map shaders, - List decoration, + List decoration, ThemeColorScheme colorScheme, ThemeSprites sprites, ThemeLoadingScreen loadingScreen @@ -41,19 +42,19 @@ public static Theme createDefaultTheme() { var sprites = new ThemeSprites( new ThemeTexture( classpathResource("progress_bar_bg.png"), - new TextureScaling.NineSlice(40, 20, 2, 2, 2, 2, true, true)), + new TextureScaling.NineSlice(40, 20, 2, 2, 2, 2, true, true, false)), new ThemeTexture( classpathResource("progress_bar_fg.png"), - new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)), + new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true, false)), new ThemeTexture( classpathResource("progress_bar_fg.png"), - new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true)), + new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true, false)), false ); var squir = new ThemeImageElement(); squir.setId("squir"); - squir.setTexture(new ThemeTexture(classpathResource("squirrel.png"), new TextureScaling.Stretch(112, 112))); + squir.setTexture(new ThemeTexture(classpathResource("squirrel.png"), new TextureScaling.Stretch(112, 112, true))); var startupLog = new ThemeStartupLogElement(); startupLog.setLeft(StyleLength.ofPoints(10)); @@ -64,7 +65,7 @@ public static Theme createDefaultTheme() { fox.setTexture( new ThemeTexture( classpathResource("fox_running.png"), - new TextureScaling.Stretch(151, 128), + new TextureScaling.Stretch(151, 128, false), new AnimationMetadata(28))); fox.setRight(StyleLength.ofPoints(10)); fox.setBottom(StyleLength.ofREM(1)); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java index 0b838376d..aa5569bed 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java @@ -41,19 +41,19 @@ public int toArgb() { } public int rByte() { - return (int) (r * 256); + return (int) (r * 255); } public int gByte() { - return (int) (g * 256); + return (int) (g * 255); } public int bByte() { - return (int) (b * 256); + return (int) (b * 255); } public int aByte() { - return (int) (a * 256); + return (int) (a * 255); } public static ThemeColor lerp(ThemeColor a, ThemeColor b, float f) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java index 9ee8927fc..82e73a47a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java @@ -36,7 +36,9 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; public final class ThemeSerializer { private static final Logger LOG = LoggerFactory.getLogger(ThemeSerializer.class); @@ -47,42 +49,102 @@ private ThemeSerializer() { public static Theme load(Path baseDirectory, String id) throws IOException { - var themeTree = readThemeTree(baseDirectory, id); + var sources = new LinkedHashSet(); + var themeTree = readThemeTree(baseDirectory, id, sources); - createGson(baseDirectory) + try { + return createGson(baseDirectory, false).fromJson(themeTree, Theme.class); + } catch (Exception e) { + throw new IOException("Failed to load theme '" + id + "' from JSON structure.", e); + } } - private static JsonObject readThemeTree(Path baseDirectory, String id) throws IOException { + private static JsonObject readThemeTree(Path baseDirectory, String id, Set sources) throws IOException { + if (!sources.add(id)) { + throw new IllegalStateException("Detected recursion in theme extends clause: " + sources + " -> " + id); + } + String filename = getThemeFilename(id); - try (var in = Files.newInputStream(baseDirectory.resolve(filename))) { - return readThemeTree(baseDirectory, in); + Path themePath = baseDirectory.resolve(filename); + try (var in = Files.newInputStream(themePath)) { + LOG.debug("Loading theme from {}", themePath); + return readThemeTree(baseDirectory, in, sources); } catch (NoSuchFileException ignored) { } // Try to load it from the classpath instead - String classpathLocation = "/net/neoforged/fml/earlydisplay/" + filename; + String classpathLocation = "/net/neoforged/fml/earlydisplay/theme/" + filename; try (var in = ThemeSerializer.class.getResourceAsStream(classpathLocation)) { + LOG.debug("Loading built-in theme {}", id); if (in == null) { throw new NoSuchFileException("Failed to find embedded theme resource " + classpathLocation); } - return readThemeTree(baseDirectory, in); + return readThemeTree(baseDirectory, in, sources); } } - private static JsonObject readThemeTree(Path baseDirectory, InputStream in) { + private static JsonObject readThemeTree(Path baseDirectory, InputStream in, Set sources) throws IOException { var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); - var themeRoot = createGson(baseDirectory).fromJson(reader, JsonObject.class); + var themeRoot = createGson(baseDirectory, false).fromJson(reader, JsonObject.class); var themeVersion = takeInt(themeRoot, "version"); if (themeVersion == null || themeVersion != VERSION) { throw new JsonParseException("Expected theme version " + VERSION + " but found: " + themeVersion); } var extendsId = takeString(themeRoot, "extends"); + if (extendsId != null) { + var baseThemeRoot = readThemeTree(baseDirectory, extendsId, sources); + themeRoot = mergeThemeRoot(baseThemeRoot, themeRoot); + } + + return themeRoot; + } - return null; + private static JsonObject mergeThemeRoot(JsonObject baseThemeRoot, JsonObject themeRoot) { + return mergeObject(baseThemeRoot, themeRoot, (property, baseValue, value) -> switch (property) { + case "fonts", "shaders", "colorScheme", "sprites" -> mergeObject(baseValue, value); + case "loadingScreen" -> mergeObject(baseValue, value, ThemeSerializer::mergeLoadingScreenProperty); + default -> value; + }); + } + + private static JsonElement mergeLoadingScreenProperty(String property, JsonElement baseValue, JsonElement value) { + // Just recursively merge every property of the loading screen object + return mergeObject(baseValue, value); + } + + private static JsonObject mergeObject(JsonElement baseObject, JsonElement object) { + return mergeObject(baseObject, object, (property, baseValue, value) -> value); + } + + /** + * Simple merge function that copies all entries from object into baseObject, overwriting + * existing entries. + */ + private static JsonObject mergeObject(JsonElement baseObject, + JsonElement object, + PropertyMerger propertyMerger) { + var objectObj = object.getAsJsonObject(); + var baseObjectObj = baseObject.getAsJsonObject(); + + for (var entry : objectObj.entrySet()) { + var baseValue = baseObjectObj.get(entry.getKey()); + if (baseValue == null) { + baseObjectObj.add(entry.getKey(), entry.getValue()); + } else { + baseObjectObj.add(entry.getKey(), propertyMerger.map(entry.getKey(), baseValue, entry.getValue())); + } + } + + return baseObjectObj; + } + + @FunctionalInterface + private interface PropertyMerger { + JsonElement map(String property, JsonElement baseValue, JsonElement value); } private static String getThemeFilename(String id) { @@ -116,21 +178,30 @@ private static JsonPrimitive takePrimitive(JsonElement el, String field) { return primitive; } - public static void save(Path path, Theme theme) { + public static void save(Path path, Theme theme, boolean exportResources) { LOG.info("Saving theme to {}", path); + + Gson gson = createGson(path.toAbsolutePath().getParent(), exportResources); + var themeTree = (JsonObject) gson.toJsonTree(theme); + var merged = new JsonObject(); + merged.addProperty("version", VERSION); + for (var entry : themeTree.entrySet()) { + merged.add(entry.getKey(), entry.getValue()); + } + try (var out = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { - createGson(path.toAbsolutePath().getParent()).toJson(theme, out); + gson.toJson(merged, out); } catch (IOException e) { LOG.error("Failed to save theme to {}", path, e); } } - private static Gson createGson(Path baseDirectory) { + private static Gson createGson(Path baseDirectory, boolean exportResources) { return new GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(TextureScaling.class, new TextureScalingSerializer()) .registerTypeAdapterFactory(new ThemeElementAdapterFactory()) - .registerTypeHierarchyAdapter(ThemeResource.class, new ThemeResourceAdapter(baseDirectory)) + .registerTypeHierarchyAdapter(ThemeResource.class, new ThemeResourceAdapter(baseDirectory, exportResources)) .registerTypeAdapter(UncompressedImage.class, new UncompressedImageSerializer()) .registerTypeAdapter(StyleLength.class, new StyleLengthAdapter()) .registerTypeAdapter(ThemeColor.class, new ThemeColorAdapter()) @@ -186,26 +257,32 @@ public JsonElement serialize(UncompressedImage value, Type typeOfSrc, JsonSerial private static class ThemeResourceAdapter extends TypeAdapter { private final Path baseDirectory; + private final boolean exportResources; - public ThemeResourceAdapter(Path baseDirectory) { + public ThemeResourceAdapter(Path baseDirectory, boolean exportResources) { this.baseDirectory = baseDirectory; + this.exportResources = exportResources; } @Override public void write(JsonWriter out, ThemeResource value) throws IOException { switch (value) { case ClasspathResource classpathResource -> { - var idx = Math.max( - classpathResource.path().lastIndexOf('/'), - classpathResource.path().lastIndexOf('\\')); - var filename = classpathResource.path().substring(idx + 1); - var diskPath = baseDirectory.resolve(filename); - try (var buffer = value.toNativeBuffer()) { - Files.write(diskPath, buffer.toByteArray()); - } catch (IOException e) { - throw new UncheckedIOException(e); + if (exportResources) { + var idx = Math.max( + classpathResource.path().lastIndexOf('/'), + classpathResource.path().lastIndexOf('\\')); + var filename = classpathResource.path().substring(idx + 1); + var diskPath = baseDirectory.resolve(filename); + try (var buffer = value.toNativeBuffer()) { + Files.write(diskPath, buffer.toByteArray()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + out.value(filename); + } else { + out.value("classpath:" + classpathResource.path()); } - out.value(filename); } case FileResource fileResource -> { var diskPath = baseDirectory.resolve(fileResource.file().getName()); @@ -231,8 +308,14 @@ public void write(JsonWriter out, ThemeColor value) throws IOException { if (value == null) { out.nullValue(); } else { - var hexColor = Integer.toHexString(value.toArgb()); - hexColor = "#" + "0".repeat(Math.max(0, 8 - hexColor.length())) + hexColor; + String hexColor; + if (value.a() == 1) { + hexColor = Integer.toHexString(value.toArgb() & 0xFFFFFF); + hexColor = "#" + "0".repeat(Math.max(0, 6 - hexColor.length())) + hexColor; + } else { + hexColor = Integer.toHexString(value.toArgb()); + hexColor = "#" + "0".repeat(Math.max(0, 8 - hexColor.length())) + hexColor; + } out.value(hexColor); } } @@ -285,7 +368,8 @@ public JsonElement serialize(TextureScaling src, Type typeOfSrc, JsonSerializati private static class ThemeElementAdapterFactory implements TypeAdapterFactory { private static final Map> TYPE_MAP = Map.of( "image", ThemeImageElement.class, - "label", ThemeLabelElement.class); + "label", ThemeLabelElement.class + ); @SuppressWarnings("unchecked") @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java index 8fe887ce5..fcafec1d4 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -1,5 +1,6 @@ package net.neoforged.fml.earlydisplay.theme.elements; +import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.util.StyleLength; import java.util.Objects; @@ -11,6 +12,7 @@ public abstract class ThemeElement { private StyleLength top = StyleLength.ofUndefined(); private StyleLength right = StyleLength.ofUndefined(); private StyleLength bottom = StyleLength.ofUndefined(); + private String font = Theme.FONT_DEFAULT; public abstract String id(); @@ -62,6 +64,14 @@ public void setMaintainAspectRatio(boolean maintainAspectRatio) { this.maintainAspectRatio = maintainAspectRatio; } + public String font() { + return font; + } + + public void setFont(String font) { + this.font = font; + } + @Override public String toString() { return id(); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java new file mode 100644 index 000000000..223a2243b --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java @@ -0,0 +1,18 @@ +package net.neoforged.fml.earlydisplay.util; + +import java.util.Map; +import java.util.regex.Pattern; + +public final class Placeholders { + private static final Pattern PATTERN = Pattern.compile("\\$\\{(\\w+)}"); + + private Placeholders() { + } + + public static String resolve(String text, Map placeholders) { + return PATTERN.matcher(text).replaceAll(matchResult -> { + var placeholder = matchResult.group(1); + return placeholders.getOrDefault(placeholder, "${" + placeholder + "}"); + }); + } +} diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-darkmode.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-darkmode.json new file mode 100644 index 000000000..3e67cc868 --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-darkmode.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "extends": "default", + "colorScheme": { + "screenBackground": "#000000", + "text": "#ffffff" + } +} \ No newline at end of file diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json new file mode 100644 index 000000000..d55b7d362 --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json @@ -0,0 +1,140 @@ +{ + "version": 1, + "windowIcon": "classpath:net/neoforged/fml/earlydisplay/theme/neoforged_icon.png", + "fonts": { + "default": "classpath:net/neoforged/fml/earlydisplay/theme/Monocraft.ttf" + }, + "shaders": { + "font": { + "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", + "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui_font.frag" + }, + "color": { + "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", + "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui_color.frag" + }, + "gui": { + "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", + "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui.frag" + } + }, + "decoration": [ + { + "type": "image", + "texture": { + "resource": "classpath:net/neoforged/fml/earlydisplay/theme/squirrel.png", + "scaling": { + "type": "stretch", + "width": 112, + "height": 112 + } + }, + "id": "squir", + "visibility": false, + "maintainAspectRatio": true + }, + { + "type": "image", + "texture": { + "resource": "classpath:net/neoforged/fml/earlydisplay/theme/fox_running.png", + "scaling": { + "type": "stretch", + "width": 151, + "height": 128 + }, + "animation": { + "frameCount": 28 + } + }, + "id": "fox", + "visibility": false, + "maintainAspectRatio": true, + "right": 10.0, + "bottom": "1.0rem" + }, + { + "type": "label", + "text": "${version}", + "id": "version", + "visibility": false, + "maintainAspectRatio": true, + "right": 10.0, + "bottom": 10.0 + } + ], + "colorScheme": { + "screenBackground": "#ef323d", + "text": "#ffffff", + "memoryLowColor": "#007f00", + "memoryHighColor": "#ff7f00" + }, + "sprites": { + "progressBarBackground": { + "resource": "classpath:net/neoforged/fml/earlydisplay/theme/progress_bar_bg.png", + "scaling": { + "type": "nine_slice", + "width": 40, + "height": 20, + "left": 2, + "top": 2, + "right": 2, + "bottom": 2, + "stretchHorizontalFill": true, + "stretchVerticalFill": true + } + }, + "progressBarForeground": { + "resource": "classpath:net/neoforged/fml/earlydisplay/theme/progress_bar_fg.png", + "scaling": { + "type": "nine_slice", + "width": 40, + "height": 20, + "left": 4, + "top": 4, + "right": 4, + "bottom": 4, + "stretchHorizontalFill": true, + "stretchVerticalFill": true + } + }, + "progressBarIndeterminate": { + "resource": "classpath:net/neoforged/fml/earlydisplay/theme/progress_bar_fg.png", + "scaling": { + "type": "nine_slice", + "width": 40, + "height": 20, + "left": 4, + "top": 4, + "right": 4, + "bottom": 4, + "stretchHorizontalFill": true, + "stretchVerticalFill": true + } + }, + "progressBarIndeterminateBounces": false + }, + "loadingScreen": { + "performance": { + "visibility": false, + "maintainAspectRatio": true, + "left": 220.0, + "top": 10.0, + "right": 220.0 + }, + "progressBars": { + "labelGap": 4, + "barGap": 5, + "visibility": false, + "maintainAspectRatio": false, + "left": 220.0, + "top": 250.0, + "right": 220.0 + }, + "startupLog": { + "visibility": false, + "maintainAspectRatio": true, + "left": 10.0, + "bottom": 10.0 + } + } +} \ No newline at end of file diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java new file mode 100644 index 000000000..c7a32b873 --- /dev/null +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java @@ -0,0 +1,15 @@ +package net.neoforged.fml.earlydisplay; + +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; + +public class ExportThemes { + public static void main(String[] args) throws Exception { + var projectRoot = TestUtil.findProjectRoot(); + + var defaultTheme = Theme.createDefaultTheme(); + var builtInThemePath = projectRoot.resolve("src/main/resources/net/neoforged/fml/earlydisplay/theme"); + ThemeSerializer.save(builtInThemePath.resolve("theme-default.json"), defaultTheme, false); + + } +} diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java index 307d0bc2d..97ad5bafb 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java @@ -2,18 +2,14 @@ import net.neoforged.fml.loading.FMLPaths; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; public class TestEarlyDisplay { public static void main(String[] args) throws Exception { System.setProperty("java.awt.headless", "true"); - System.setProperty("fml.writeMissingTheme", "true"); + System.setProperty("fml.earlyWindowDarkMode", "true"); - // Find the project directory by search for build.gradle upwards - var projectRoot = findProjectRoot(Paths.get(TestEarlyDisplay.class.getProtectionDomain().getCodeSource().getLocation().toURI())); - FMLPaths.loadAbsolutePaths(projectRoot); + FMLPaths.loadAbsolutePaths(TestUtil.findProjectRoot()); var window = new DisplayWindow(); var periodicTick = window.initialize(new String[]{ @@ -26,20 +22,9 @@ public static void main(String[] args) throws Exception { periodicTick.run(); Thread.sleep(100L); } catch (InterruptedException e) { - throw new RuntimeException(e); + Thread.currentThread().interrupt(); + break; } } } - - private static Path findProjectRoot(Path path) { - Path current = path; - while (current != null) { - if (Files.exists(current.resolve("build.gradle"))) { - return current; - } - current = current.getParent(); - } - - throw new IllegalArgumentException("Couldn't find buid.gradle in any parent directory of " + path); - } } diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java new file mode 100644 index 000000000..904bc453f --- /dev/null +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java @@ -0,0 +1,24 @@ +package net.neoforged.fml.earlydisplay; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class TestUtil { + static Path findProjectRoot() throws Exception { + // Find the project directory by search for build.gradle upwards + return findProjectRoot(Paths.get(TestUtil.class.getProtectionDomain().getCodeSource().getLocation().toURI())); + } + + static Path findProjectRoot(Path path) { + Path current = path; + while (current != null) { + if (Files.exists(current.resolve("build.gradle"))) { + return current; + } + current = current.getParent(); + } + + throw new IllegalArgumentException("Couldn't find buid.gradle in any parent directory of " + path); + } +} diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java index 4aae6e1e9..c0a8e3c87 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java @@ -17,7 +17,7 @@ class ThemeSerializerTest { void testDefaultThemeRoundtrip() throws IOException { var defaultTheme = Theme.createDefaultTheme(); Path themePath = tempDir.resolve("theme-default.json"); - ThemeSerializer.save(themePath, defaultTheme); + ThemeSerializer.save(themePath, defaultTheme, false); var loadedTheme = ThemeSerializer.load(tempDir, "default"); assertThat(loadedTheme) From 8fefd4b3b0a5569cf1b1dc1661894a34bf7eb37b Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Mon, 21 Apr 2025 01:56:03 +0200 Subject: [PATCH 05/22] Formatting --- .../fml/earlydisplay/DisplayWindow.java | 96 +++++++++---------- .../fml/earlydisplay/package-info.java | 2 +- .../fml/earlydisplay/render/GlDebug.java | 1 - .../fml/earlydisplay/render/GlState.java | 2 - .../render/LoadingScreenRenderer.java | 60 ++++++------ .../render/MaterializedTheme.java | 10 +- .../render/MaterializedThemeSprites.java | 4 +- .../earlydisplay/render/RenderContext.java | 8 +- .../render/elements/LabelElement.java | 5 +- .../render/elements/PerformanceElement.java | 21 ++-- .../render/elements/ProgressBarsElement.java | 3 +- .../render/elements/RenderElement.java | 1 - .../render/elements/StartupLogElement.java | 7 +- .../render/elements/package-info.java | 2 +- .../fml/earlydisplay/render/package-info.java | 2 +- .../earlydisplay/theme/ClasspathResource.java | 1 + .../fml/earlydisplay/theme/FileResource.java | 3 +- .../earlydisplay/theme/TextureScaling.java | 9 +- .../fml/earlydisplay/theme/Theme.java | 17 +--- .../fml/earlydisplay/theme/ThemeColor.java | 4 +- .../earlydisplay/theme/ThemeColorScheme.java | 14 ++- .../theme/ThemeLoadingScreen.java | 5 +- .../earlydisplay/theme/ThemeSerializer.java | 30 +++--- .../fml/earlydisplay/theme/ThemeSprites.java | 4 +- .../theme/elements/ThemeElement.java | 3 +- .../elements/ThemePerformanceElement.java | 3 - .../theme/elements/package-info.java | 2 +- .../fml/earlydisplay/theme/package-info.java | 2 +- .../fml/earlydisplay/util/Placeholders.java | 3 +- .../fml/earlydisplay/util/package-info.java | 2 +- .../fml/earlydisplay/ExportThemes.java | 1 - .../fml/earlydisplay/TestEarlyDisplay.java | 4 +- 32 files changed, 137 insertions(+), 194 deletions(-) diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index d07d2ac17..40229b22b 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -5,48 +5,6 @@ package net.neoforged.fml.earlydisplay; -import joptsimple.OptionParser; -import net.neoforged.fml.earlydisplay.render.LoadingScreenRenderer; -import net.neoforged.fml.earlydisplay.render.SimpleFont; -import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.ThemeColor; -import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; -import net.neoforged.fml.loading.FMLConfig; -import net.neoforged.fml.loading.FMLPaths; -import net.neoforged.fml.loading.progress.ProgressMeter; -import net.neoforged.fml.loading.progress.StartupNotificationManager; -import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.PointerBuffer; -import org.lwjgl.glfw.GLFWImage; -import org.lwjgl.glfw.GLFWVidMode; -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.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; - import static org.lwjgl.glfw.GLFW.GLFW_CLIENT_API; import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_CREATION_API; import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MAJOR; @@ -88,6 +46,47 @@ import static org.lwjgl.glfw.GLFW.glfwWindowHintString; import static org.lwjgl.opengl.GL32C.GL_TRUE; +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import joptsimple.OptionParser; +import net.neoforged.fml.earlydisplay.render.LoadingScreenRenderer; +import net.neoforged.fml.earlydisplay.render.SimpleFont; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.ThemeColor; +import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; +import net.neoforged.fml.loading.FMLConfig; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.fml.loading.progress.ProgressMeter; +import net.neoforged.fml.loading.progress.StartupNotificationManager; +import net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.PointerBuffer; +import org.lwjgl.glfw.GLFWImage; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.util.tinyfd.TinyFileDialogs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * 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. @@ -125,8 +124,7 @@ public class DisplayWindow implements ImmediateWindowProvider { private boolean maximized; private Map fonts; - private Runnable repaintTick = () -> { - }; + private Runnable repaintTick = () -> {}; private ThemeColor background; public DisplayWindow() { @@ -254,8 +252,7 @@ private void crashElegantly(String errorDetails) { thread.start(); try { thread.join(); - } catch (InterruptedException ignored) { - } + } catch (InterruptedException ignored) {} System.exit(1); } @@ -363,8 +360,8 @@ public void initWindow(@Nullable String mcVersion) { // Attempt setting the icon try (var glfwImgBuffer = GLFWImage.malloc(1); - var glfwImages = GLFWImage.malloc(); - var icon = theme.windowIcon().loadAsImage()) { + var glfwImages = GLFWImage.malloc(); + var icon = theme.windowIcon().loadAsImage()) { glfwImgBuffer.put(glfwImages.set(icon.width(), icon.height(), icon.imageData())); glfwImgBuffer.flip(); glfwSetWindowIcon(window, glfwImgBuffer); @@ -473,8 +470,7 @@ public long takeOverGlfwWindow() { } @Override - public void updateModuleReads(final ModuleLayer layer) { - } + public void updateModuleReads(final ModuleLayer layer) {} // Called from Neo public int getFramebufferTextureId() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java index 8b96a776d..cad638944 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java @@ -1,4 +1,4 @@ @ApiStatus.Internal package net.neoforged.fml.earlydisplay; -import org.jetbrains.annotations.ApiStatus; \ No newline at end of file +import org.jetbrains.annotations.ApiStatus; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java index 329801a57..010cdf6f0 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlDebug.java @@ -6,7 +6,6 @@ package net.neoforged.fml.earlydisplay.render; import net.neoforged.fml.loading.FMLConfig; -import org.jetbrains.annotations.ApiStatus; import org.lwjgl.opengl.EXTDebugLabel; import org.lwjgl.opengl.EXTDebugMarker; import org.lwjgl.opengl.GL32C; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java index 10f6e6312..043d4ff90 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/GlState.java @@ -47,8 +47,6 @@ import static org.lwjgl.opengl.GL32C.glUseProgram; import static org.lwjgl.opengl.GL32C.glViewport; -import org.jetbrains.annotations.ApiStatus; - /** * A static state manager for a subset of OpenGL states to minimize redundant state changes. *

diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index 7fa91430a..2a0d01664 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -1,28 +1,5 @@ package net.neoforged.fml.earlydisplay.render; -import net.neoforged.fml.earlydisplay.render.elements.ImageElement; -import net.neoforged.fml.earlydisplay.render.elements.LabelElement; -import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; -import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; -import net.neoforged.fml.earlydisplay.render.elements.RenderElement; -import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; -import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; -import org.lwjgl.opengl.GL32C; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; @@ -39,6 +16,28 @@ import static org.lwjgl.opengl.GL11C.glClear; import static org.lwjgl.opengl.GL11C.glGetString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import net.neoforged.fml.earlydisplay.render.elements.ImageElement; +import net.neoforged.fml.earlydisplay.render.elements.LabelElement; +import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; +import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; +import net.neoforged.fml.earlydisplay.render.elements.RenderElement; +import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import org.lwjgl.opengl.GL32C; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class LoadingScreenRenderer implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(LoadingScreenRenderer.class); @@ -70,10 +69,10 @@ public class LoadingScreenRenderer implements AutoCloseable { * Nothing fancy, we just want to draw and render text. */ public LoadingScreenRenderer(ScheduledExecutorService scheduler, - long glfwWindow, - Theme theme, - String mcVersion, - String neoForgeVersion) { + long glfwWindow, + Theme theme, + String mcVersion, + String neoForgeVersion) { this.glfwWindow = glfwWindow; this.mcVersion = mcVersion; this.neoForgeVersion = neoForgeVersion; @@ -137,12 +136,9 @@ private RenderElement loadElement(ThemeElement element) { labelElement, theme, Map.of( - "version", mcVersion + "-" + neoForgeVersion.split("-")[0] - ) - ); + "version", mcVersion + "-" + neoForgeVersion.split("-")[0])); - default -> - throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); + default -> throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); }; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java index bb2a9470d..eb86125ef 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java @@ -1,14 +1,13 @@ package net.neoforged.fml.earlydisplay.render; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeResource; import net.neoforged.fml.earlydisplay.theme.ThemeShader; import net.neoforged.fml.earlydisplay.theme.ThemeSprites; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - /** * A themes resources loaded for rendering at runtime. */ @@ -53,8 +52,7 @@ private static MaterializedThemeSprites loadSprites(ThemeSprites sprites) { return new MaterializedThemeSprites( Texture.create(sprites.progressBarBackground()), Texture.create(sprites.progressBarForeground()), - Texture.create(sprites.progressBarIndeterminate()) - ); + Texture.create(sprites.progressBarIndeterminate())); } public SimpleFont getFont(String fontId) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java index d06f0a6cf..a6e44b2db 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java @@ -3,6 +3,4 @@ public record MaterializedThemeSprites( Texture progressBarBackground, Texture progressBarForeground, - Texture progressBarIndeterminate -) { -} + Texture progressBarIndeterminate) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java index 1404a03ac..120e56729 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -1,13 +1,12 @@ package net.neoforged.fml.earlydisplay.render; +import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; + +import java.util.List; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.util.Bounds; -import java.util.List; - -import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; - public record RenderContext( SimpleBufferBuilder sharedBuffer, MaterializedTheme theme, @@ -66,7 +65,6 @@ public void renderText(float x, float y, SimpleFont font, List { - }); + return new NativeBuffer(buffer, ignored -> {}); } } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java index 4bd3be805..c6a749736 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java @@ -15,13 +15,10 @@ public sealed interface TextureScaling { boolean linearScaling(); - record Stretch(int width, int height, boolean linearScaling) implements TextureScaling { - } + record Stretch(int width, int height, boolean linearScaling) implements TextureScaling {} - record Tile(int width, int height, boolean linearScaling) implements TextureScaling { - } + record Tile(int width, int height, boolean linearScaling) implements TextureScaling {} record NineSlice(int width, int height, int left, int top, int right, int bottom, boolean stretchHorizontalFill, - boolean stretchVerticalFill, boolean linearScaling) implements TextureScaling { - } + boolean stretchVerticalFill, boolean linearScaling) implements TextureScaling {} } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index 15eb4cf22..181930b60 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -1,7 +1,8 @@ package net.neoforged.fml.earlydisplay.theme; +import java.util.List; +import java.util.Map; import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; @@ -9,9 +10,6 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; import net.neoforged.fml.earlydisplay.util.StyleLength; -import java.util.List; -import java.util.Map; - /** * Defines a theme for the early display screen. * @@ -30,14 +28,12 @@ public record Theme( List decoration, ThemeColorScheme colorScheme, ThemeSprites sprites, - ThemeLoadingScreen loadingScreen -) { + ThemeLoadingScreen loadingScreen) { public static final String FONT_DEFAULT = "default"; public static final String SHADER_GUI = "gui"; public static final String SHADER_FONT = "font"; public static final String SHADER_COLOR = "color"; - public static Theme createDefaultTheme() { var sprites = new ThemeSprites( new ThemeTexture( @@ -49,8 +45,7 @@ public static Theme createDefaultTheme() { new ThemeTexture( classpathResource("progress_bar_fg.png"), new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true, false)), - false - ); + false); var squir = new ThemeImageElement(); squir.setId("squir"); @@ -106,9 +101,7 @@ FONT_DEFAULT, classpathResource("Monocraft.ttf")), new ThemeLoadingScreen( performance, progressBars, - startupLog - ) - ); + startupLog)); } private static ClasspathResource classpathResource(String name) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java index aa5569bed..a8be093f3 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java @@ -5,7 +5,6 @@ public record ThemeColor(float r, float g, float b, float a) { public static final ThemeColor WHITE = new ThemeColor(1, 1, 1, 1); - public static ThemeColor ofBytes(int r, int g, int b, int a) { return new ThemeColor(r / 255.f, g / 255.f, b / 255.f, a / 255.f); } @@ -63,8 +62,7 @@ public static ThemeColor lerp(ThemeColor a, ThemeColor b, float f) { return ofHsb( hsbA[0] + (hsbB[0] - hsbA[0]) * f, hsbA[1] + (hsbB[1] - hsbA[1]) * f, - hsbA[2] + (hsbB[2] - hsbA[2]) * f - ); + hsbA[2] + (hsbB[2] - hsbA[2]) * f); } public float[] toHsb() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java index f3f5f9f2f..b43f6cb11 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java @@ -3,21 +3,19 @@ /** * @param screenBackground * @param text - * @param memoryLowColor The color to use for coloring the bar when resource usage is low. - * The actual color will be interpolated between this and {@code highColor}. - * @param memoryHighColor The color to use for coloring the bar when resource usage is high. - * The actual color will be interpolated between this and {@code highColor}. + * @param memoryLowColor The color to use for coloring the bar when resource usage is low. + * The actual color will be interpolated between this and {@code highColor}. + * @param memoryHighColor The color to use for coloring the bar when resource usage is high. + * The actual color will be interpolated between this and {@code highColor}. */ public record ThemeColorScheme( ThemeColor screenBackground, ThemeColor text, ThemeColor memoryLowColor, - ThemeColor memoryHighColor -) { + ThemeColor memoryHighColor) { public static final ThemeColorScheme DEFAULT = new ThemeColorScheme( ThemeColor.ofBytes(239, 50, 61), ThemeColor.ofBytes(255, 255, 255), ThemeColor.ofBytes(0, 127, 0), - ThemeColor.ofBytes(255, 127, 0) - ); + ThemeColor.ofBytes(255, 127, 0)); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java index 3fefc02e8..1565417c7 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java @@ -1,6 +1,5 @@ package net.neoforged.fml.earlydisplay.theme; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; @@ -11,6 +10,4 @@ public record ThemeLoadingScreen( ThemePerformanceElement performance, ThemeProgressBarsElement progressBars, - ThemeStartupLogElement startupLog -) { -} + ThemeStartupLogElement startupLog) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java index 82e73a47a..d1b3e8b04 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java @@ -16,14 +16,6 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; -import net.neoforged.fml.earlydisplay.util.StyleLength; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -39,16 +31,21 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.util.StyleLength; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public final class ThemeSerializer { private static final Logger LOG = LoggerFactory.getLogger(ThemeSerializer.class); private static final int VERSION = 1; - private ThemeSerializer() { - } + private ThemeSerializer() {} public static Theme load(Path baseDirectory, String id) throws IOException { - var sources = new LinkedHashSet(); var themeTree = readThemeTree(baseDirectory, id, sources); @@ -57,7 +54,6 @@ public static Theme load(Path baseDirectory, String id) throws IOException { } catch (Exception e) { throw new IOException("Failed to load theme '" + id + "' from JSON structure.", e); } - } private static JsonObject readThemeTree(Path baseDirectory, String id, Set sources) throws IOException { @@ -71,8 +67,7 @@ private static JsonObject readThemeTree(Path baseDirectory, String id, Set> TYPE_MAP = Map.of( "image", ThemeImageElement.class, - "label", ThemeLabelElement.class - ); + "label", ThemeLabelElement.class); @SuppressWarnings("unchecked") @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java index bcded0924..b5dd7f511 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java @@ -14,6 +14,4 @@ public record ThemeSprites( ThemeTexture progressBarBackground, ThemeTexture progressBarForeground, ThemeTexture progressBarIndeterminate, - boolean progressBarIndeterminateBounces -) { -} + boolean progressBarIndeterminateBounces) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java index fcafec1d4..7b1f16bfa 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -1,10 +1,9 @@ package net.neoforged.fml.earlydisplay.theme.elements; +import java.util.Objects; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.util.StyleLength; -import java.util.Objects; - public abstract class ThemeElement { private boolean visibility = false; private boolean maintainAspectRatio = true; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java index 72379bc72..c1312e889 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java @@ -1,8 +1,5 @@ package net.neoforged.fml.earlydisplay.theme.elements; -import net.neoforged.fml.earlydisplay.theme.ThemeColor; -import net.neoforged.fml.earlydisplay.theme.ThemeTexture; - public class ThemePerformanceElement extends ThemeElement { @Override public String id() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java index 3a63210f6..c3a18efa9 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java @@ -1,4 +1,4 @@ @ApiStatus.Internal package net.neoforged.fml.earlydisplay.theme.elements; -import org.jetbrains.annotations.ApiStatus; \ No newline at end of file +import org.jetbrains.annotations.ApiStatus; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java index 6dc959d55..cb9e8009f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java @@ -1,4 +1,4 @@ @ApiStatus.Internal package net.neoforged.fml.earlydisplay.theme; -import org.jetbrains.annotations.ApiStatus; \ No newline at end of file +import org.jetbrains.annotations.ApiStatus; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java index 223a2243b..eae6fd105 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java @@ -6,8 +6,7 @@ public final class Placeholders { private static final Pattern PATTERN = Pattern.compile("\\$\\{(\\w+)}"); - private Placeholders() { - } + private Placeholders() {} public static String resolve(String text, Map placeholders) { return PATTERN.matcher(text).replaceAll(matchResult -> { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java index cc2079f46..1838d9454 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java @@ -1,4 +1,4 @@ @ApiStatus.Internal package net.neoforged.fml.earlydisplay.util; -import org.jetbrains.annotations.ApiStatus; \ No newline at end of file +import org.jetbrains.annotations.ApiStatus; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java index c7a32b873..e563ba1db 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java @@ -10,6 +10,5 @@ public static void main(String[] args) throws Exception { var defaultTheme = Theme.createDefaultTheme(); var builtInThemePath = projectRoot.resolve("src/main/resources/net/neoforged/fml/earlydisplay/theme"); ThemeSerializer.save(builtInThemePath.resolve("theme-default.json"), defaultTheme, false); - } } diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java index 97ad5bafb..b549678c1 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java @@ -2,8 +2,6 @@ import net.neoforged.fml.loading.FMLPaths; -import java.nio.file.Paths; - public class TestEarlyDisplay { public static void main(String[] args) throws Exception { System.setProperty("java.awt.headless", "true"); @@ -12,7 +10,7 @@ public static void main(String[] args) throws Exception { FMLPaths.loadAbsolutePaths(TestUtil.findProjectRoot()); var window = new DisplayWindow(); - var periodicTick = window.initialize(new String[]{ + var periodicTick = window.initialize(new String[] { "--fml.mcVersion", "1.21.5", "--fml.neoForgeVersion", "21.5.123-beta" }); From 0310be0ed1218af58a78b36ecc5132d74c589a96 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Mon, 21 Apr 2025 10:11:03 +0200 Subject: [PATCH 06/22] Fix docs typo. --- .../net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java index b43f6cb11..ac46076eb 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java @@ -6,7 +6,7 @@ * @param memoryLowColor The color to use for coloring the bar when resource usage is low. * The actual color will be interpolated between this and {@code highColor}. * @param memoryHighColor The color to use for coloring the bar when resource usage is high. - * The actual color will be interpolated between this and {@code highColor}. + * The actual color will be interpolated between this and {@code lowColor}. */ public record ThemeColorScheme( ThemeColor screenBackground, From d86912191a4a94c4a0974585c460fb196c3e2a3a Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Mon, 21 Apr 2025 10:11:35 +0200 Subject: [PATCH 07/22] Fix licenses. --- .../java/net/neoforged/fml/earlydisplay/RenderContext.java | 5 +++++ .../java/net/neoforged/fml/earlydisplay/package-info.java | 5 +++++ .../neoforged/fml/earlydisplay/render/ElementRenderer.java | 5 +++++ .../fml/earlydisplay/render/LoadingScreenRenderer.java | 5 +++++ .../neoforged/fml/earlydisplay/render/MaterializedTheme.java | 5 +++++ .../fml/earlydisplay/render/MaterializedThemeSprites.java | 5 +++++ .../net/neoforged/fml/earlydisplay/render/RenderContext.java | 5 +++++ .../java/net/neoforged/fml/earlydisplay/render/Texture.java | 5 +++++ .../fml/earlydisplay/render/elements/ImageElement.java | 5 +++++ .../fml/earlydisplay/render/elements/LabelElement.java | 5 +++++ .../fml/earlydisplay/render/elements/PerformanceElement.java | 5 +++++ .../earlydisplay/render/elements/ProgressBarsElement.java | 5 +++++ .../fml/earlydisplay/render/elements/StartupLogElement.java | 5 +++++ .../fml/earlydisplay/render/elements/package-info.java | 5 +++++ .../net/neoforged/fml/earlydisplay/render/package-info.java | 5 +++++ .../neoforged/fml/earlydisplay/theme/AnimationMetadata.java | 5 +++++ .../neoforged/fml/earlydisplay/theme/ClasspathResource.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/FileResource.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/ImageLoader.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/NativeBuffer.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/TextureScaling.java | 5 +++++ .../java/net/neoforged/fml/earlydisplay/theme/Theme.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/ThemeColor.java | 5 +++++ .../neoforged/fml/earlydisplay/theme/ThemeColorScheme.java | 5 +++++ .../neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/ThemeResource.java | 5 +++++ .../neoforged/fml/earlydisplay/theme/ThemeSerializer.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/ThemeShader.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/ThemeSprites.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/ThemeTexture.java | 5 +++++ .../neoforged/fml/earlydisplay/theme/UncompressedImage.java | 5 +++++ .../earlydisplay/theme/elements/ThemeDecorativeElement.java | 5 +++++ .../fml/earlydisplay/theme/elements/ThemeElement.java | 5 +++++ .../fml/earlydisplay/theme/elements/ThemeImageElement.java | 5 +++++ .../fml/earlydisplay/theme/elements/ThemeLabelElement.java | 5 +++++ .../earlydisplay/theme/elements/ThemePerformanceElement.java | 5 +++++ .../theme/elements/ThemeProgressBarsElement.java | 5 +++++ .../earlydisplay/theme/elements/ThemeStartupLogElement.java | 5 +++++ .../fml/earlydisplay/theme/elements/package-info.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/package-info.java | 5 +++++ .../java/net/neoforged/fml/earlydisplay/util/Bounds.java | 5 +++++ .../net/neoforged/fml/earlydisplay/util/Placeholders.java | 5 +++++ .../main/java/net/neoforged/fml/earlydisplay/util/Size.java | 5 +++++ .../net/neoforged/fml/earlydisplay/util/StyleLength.java | 5 +++++ .../net/neoforged/fml/earlydisplay/util/package-info.java | 5 +++++ .../java/net/neoforged/fml/earlydisplay/ExportThemes.java | 5 +++++ .../net/neoforged/fml/earlydisplay/TestEarlyDisplay.java | 5 +++++ .../test/java/net/neoforged/fml/earlydisplay/TestUtil.java | 5 +++++ .../neoforged/fml/earlydisplay/render/SimpleFontTest.java | 5 +++++ .../fml/earlydisplay/render/WithOffScreenGLSurface.java | 5 +++++ .../net/neoforged/fml/earlydisplay/theme/ThemeColorTest.java | 5 +++++ .../fml/earlydisplay/theme/ThemeSerializerTest.java | 5 +++++ 52 files changed, 260 insertions(+) diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java index 1426f4e34..b4f7fe36c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay; public record RenderContext(float availableWidth, float availableHeight) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java index cad638944..75c18e1b2 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/package-info.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + @ApiStatus.Internal package net.neoforged.fml.earlydisplay; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java index 9c3f38a0f..b05ff5231 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementRenderer.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; public abstract class ElementRenderer { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index 2a0d01664..cb9a7faac 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java index eb86125ef..9d0cb63c9 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; import java.io.IOException; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java index a6e44b2db..a84848be7 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedThemeSprites.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; public record MaterializedThemeSprites( diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java index 120e56729..18d8a47bd 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java index 05a5d88ea..a5afa129c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; import static org.lwjgl.opengl.GL11C.GL_LINEAR; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java index 27a066c64..821b389c7 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render.elements; import net.neoforged.fml.earlydisplay.render.MaterializedTheme; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java index 1050c2b5f..da707eba3 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/LabelElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render.elements; import java.util.List; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java index edf90a366..35519ab25 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render.elements; import com.sun.management.OperatingSystemMXBean; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java index 8012daf17..c681815c6 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ProgressBarsElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render.elements; import java.util.List; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java index 6d3ec6bfc..eccb6c406 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/StartupLogElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render.elements; import java.util.ArrayList; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java index 903f6ac5b..965ecd3ee 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/package-info.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + @ApiStatus.Internal package net.neoforged.fml.earlydisplay.render.elements; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java index 39eed4243..eb1bdd7ef 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/package-info.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + @ApiStatus.Internal package net.neoforged.fml.earlydisplay.render; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java index 6c8a115a8..06f503a2c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/AnimationMetadata.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; /** diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java index 2d7908d94..32f41b896 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import java.io.FileNotFoundException; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java index a43bd5268..0ada53d84 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import java.io.File; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java index db141f566..45eeb7888 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import org.lwjgl.stb.STBImage; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java index fdb17b69a..e09454a5d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import java.nio.ByteBuffer; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java index c6a749736..c71778cdb 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/TextureScaling.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; public sealed interface TextureScaling { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index 181930b60..cdecb61e2 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import java.util.List; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java index a8be093f3..ec8347340 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColor.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import java.awt.Color; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java index ac46076eb..57c4d9260 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeColorScheme.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; /** diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java index 1565417c7..f3728f0f5 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java index 57a67d1ed..8639b5686 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import java.io.IOException; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java index d1b3e8b04..148b4e5c2 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import com.google.gson.Gson; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java index 84e8941bc..93ea40db1 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; public record ThemeShader( diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java index b5dd7f511..9cbf48c07 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSprites.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; /** diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java index 3b6016387..92da64772 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeTexture.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import org.jetbrains.annotations.Nullable; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java index 87186675e..01ea9905f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/UncompressedImage.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import java.nio.ByteBuffer; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java index e2ada6a2d..dc6595bbf 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme.elements; /** diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java index 7b1f16bfa..c7dc528b9 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme.elements; import java.util.Objects; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java index a61f31821..bec5823d7 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeImageElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme.elements; import net.neoforged.fml.earlydisplay.theme.ThemeTexture; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java index 3a47b3b24..fc5bc8b0d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeLabelElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme.elements; import java.util.Objects; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java index c1312e889..681024d78 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme.elements; public class ThemePerformanceElement extends ThemeElement { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java index b4a6af57a..743049b34 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme.elements; public class ThemeProgressBarsElement extends ThemeElement { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java index 8f2770987..a8076b58b 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme.elements; public class ThemeStartupLogElement extends ThemeElement { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java index c3a18efa9..767a8fd24 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/package-info.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + @ApiStatus.Internal package net.neoforged.fml.earlydisplay.theme.elements; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java index cb9e8009f..7f1d17620 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/package-info.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + @ApiStatus.Internal package net.neoforged.fml.earlydisplay.theme; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java index 030d683a1..a269bf100 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Bounds.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.util; public record Bounds(float left, float top, float right, float bottom) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java index eae6fd105..d1f4255c7 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Placeholders.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.util; import java.util.Map; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java index 43d6387a8..a930837c6 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/Size.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.util; public record Size(float width, float height) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java index 15b27d5ef..6ca6d2e78 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/StyleLength.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.util; /** diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java index 1838d9454..9855e66ab 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/util/package-info.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + @ApiStatus.Internal package net.neoforged.fml.earlydisplay.util; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java index e563ba1db..c2590c378 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay; import net.neoforged.fml.earlydisplay.theme.Theme; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java index b549678c1..f1e4c107d 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay; import net.neoforged.fml.loading.FMLPaths; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java index 904bc453f..b2b75fb2f 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestUtil.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay; import java.nio.file.Files; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java index af4b750eb..4d105f99c 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/WithOffScreenGLSurface.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/WithOffScreenGLSurface.java index 9e558d3f4..0651c9fdf 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/WithOffScreenGLSurface.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/WithOffScreenGLSurface.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.render; import static org.lwjgl.glfw.GLFW.GLFW_CLIENT_API; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeColorTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeColorTest.java index ce4b648b2..a39f62096 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeColorTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeColorTest.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import static org.junit.jupiter.api.Assertions.*; diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java index c0a8e3c87..8b1b3ea3c 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.fml.earlydisplay.theme; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; From 0106a56476943b0cdb1a4b99274811e0b65db21c Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Mon, 21 Apr 2025 10:26:07 +0200 Subject: [PATCH 08/22] Try enabling xvfb for running tests --- .github/workflows/build-prs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml index c383d133c..3902118cd 100644 --- a/.github/workflows/build-prs.yml +++ b/.github/workflows/build-prs.yml @@ -17,9 +17,10 @@ on: jobs: build: - uses: neoforged/actions/.github/workflows/build-prs.yml@main + uses: neoforged/actions/.github/workflows/build-prs.yml@non-headless with: java: 21 # --info allows seeing STDOUT of tests gradle_tasks: check --info jar_compatibility: true + headless: false From 39dc9e338df2787fe1d42e8551872b3399ca22c2 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Mon, 21 Apr 2025 10:38:01 +0200 Subject: [PATCH 09/22] Removed duplicate resources --- earlydisplay/src/main/resources/Monocraft.ttf | Bin 202764 -> 0 bytes .../src/main/resources/neoforged_icon.png | Bin 2400 -> 0 bytes earlydisplay/src/main/resources/squirrel.png | Bin 43999 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 earlydisplay/src/main/resources/Monocraft.ttf delete mode 100644 earlydisplay/src/main/resources/neoforged_icon.png delete mode 100644 earlydisplay/src/main/resources/squirrel.png diff --git a/earlydisplay/src/main/resources/Monocraft.ttf b/earlydisplay/src/main/resources/Monocraft.ttf deleted file mode 100644 index 4066b0a9889c2505d31b953487ac1d48fafaf9e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 202764 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

aqJ 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 H`;q?xJ@PKt diff --git a/earlydisplay/src/main/resources/neoforged_icon.png b/earlydisplay/src/main/resources/neoforged_icon.png deleted file mode 100644 index 87190e53d46268658e1a41943651541cfb9bb781..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2400 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 Date: Mon, 21 Apr 2025 20:28:57 +0200 Subject: [PATCH 10/22] Test workflow without headless set --- .github/workflows/build-prs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml index 3902118cd..e8c781348 100644 --- a/.github/workflows/build-prs.yml +++ b/.github/workflows/build-prs.yml @@ -23,4 +23,3 @@ jobs: # --info allows seeing STDOUT of tests gradle_tasks: check --info jar_compatibility: true - headless: false From 69223af90e3f9924b9fea42ee3520f6b0282b704 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Mon, 21 Apr 2025 21:45:08 +0200 Subject: [PATCH 11/22] Enable graphics_environment --- .github/workflows/build-prs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml index e8c781348..2262097c6 100644 --- a/.github/workflows/build-prs.yml +++ b/.github/workflows/build-prs.yml @@ -23,3 +23,4 @@ jobs: # --info allows seeing STDOUT of tests gradle_tasks: check --info jar_compatibility: true + graphics_environment: true From 68171318d284e29a92cbae1a7ea7edda17bc0ebe Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Tue, 22 Apr 2025 00:11:28 +0200 Subject: [PATCH 12/22] Switch back to main workflow --- .github/workflows/build-prs.yml | 2 +- .../fml/earlydisplay/render/elements/ImageElement.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml index 2262097c6..881436c74 100644 --- a/.github/workflows/build-prs.yml +++ b/.github/workflows/build-prs.yml @@ -17,7 +17,7 @@ on: jobs: build: - uses: neoforged/actions/.github/workflows/build-prs.yml@non-headless + uses: neoforged/actions/.github/workflows/build-prs.yml@main with: java: 21 # --info allows seeing STDOUT of tests diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java index 821b389c7..d1748dcb7 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java @@ -21,6 +21,7 @@ public ImageElement(ThemeImageElement element, MaterializedTheme theme) { @Override public void render(RenderContext context) { int color = -1; + // April-fools handling if ("squir".equals(id())) { int fade = (int) (Math.cos(context.animationFrame() * Math.PI / 16) * 16) + 16; color = (fade & 0xff) << 24 | 0xffffff; From 1c041b42367ea7fa3754b8b63d0554e6e37f848d Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 4 May 2025 02:37:42 +0200 Subject: [PATCH 13/22] Implement Mojang logo. --- .../fml/earlydisplay/DisplayWindow.java | 2 - .../fml/earlydisplay/RenderContext.java | 8 - .../earlydisplay/render/EarlyFramebuffer.java | 13 +- .../render/LoadingScreenRenderer.java | 98 +++++---- .../fml/earlydisplay/render/QuadHelper.java | 64 +++--- .../earlydisplay/render/RenderContext.java | 28 ++- .../render/SimpleBufferBuilder.java | 2 +- .../fml/earlydisplay/render/SimpleFont.java | 5 +- .../fml/earlydisplay/render/Texture.java | 51 +++-- .../render/elements/MojangLogoElement.java | 63 ++++++ .../render/elements/PerformanceElement.java | 1 + .../render/elements/RenderElement.java | 189 +++--------------- .../fml/earlydisplay/theme/ImageLoader.java | 35 +++- .../fml/earlydisplay/theme/Theme.java | 24 ++- .../theme/ThemeLoadingScreen.java | 8 +- .../theme/ThemeMojangLogoElement.java | 11 + .../fml/earlydisplay/theme/ThemeResource.java | 14 ++ .../elements/ThemeDecorativeElement.java | 10 - .../theme/elements/ThemeElement.java | 34 +++- .../elements/ThemePerformanceElement.java | 4 - .../elements/ThemeProgressBarsElement.java | 5 - .../elements/ThemeStartupLogElement.java | 4 - .../fml/earlydisplay/theme/theme-default.json | 142 +++++++------ tests/build.gradle | 9 +- .../src/test/java}/TestEarlyDisplay.java | 26 ++- 25 files changed, 474 insertions(+), 376 deletions(-) delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java rename {earlydisplay/src/test/java/net/neoforged/fml/earlydisplay => tests/src/test/java}/TestEarlyDisplay.java (50%) diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 40229b22b..8eaa8c002 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -500,8 +500,6 @@ public void completeProgress() { } public void addMojangTexture(final int textureId) { -// TODO this.elements.add(0, RenderElement.mojang(textureId, framecount)); -// this.elements.get(0).retire(framecount + 1); } public void close() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java deleted file mode 100644 index b4f7fe36c..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/RenderContext.java +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay; - -public record RenderContext(float availableWidth, float availableHeight) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java index 7f82f1053..558fdd39b 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/EarlyFramebuffer.java @@ -14,8 +14,8 @@ public class EarlyFramebuffer { private final int framebuffer; private final int texture; - private final int width; - private final int height; + private int width; + private int height; EarlyFramebuffer(int width, int height) { this.width = width; @@ -74,6 +74,15 @@ int getTexture() { return this.texture; } + public void resize(int width, int height) { + if (this.width != width || this.height != height) { + GlState.bindFramebuffer(framebuffer); + this.width = width; + this.height = height; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer) null); + } + } + public void close() { glDeleteTextures(this.texture); glDeleteFramebuffers(this.framebuffer); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index cb9a7faac..2e0e8a515 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -5,6 +5,30 @@ package net.neoforged.fml.earlydisplay.render; +import net.neoforged.fml.earlydisplay.render.elements.ImageElement; +import net.neoforged.fml.earlydisplay.render.elements.LabelElement; +import net.neoforged.fml.earlydisplay.render.elements.MojangLogoElement; +import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; +import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; +import net.neoforged.fml.earlydisplay.render.elements.RenderElement; +import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import org.lwjgl.opengl.GL32C; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; @@ -21,30 +45,10 @@ import static org.lwjgl.opengl.GL11C.glClear; import static org.lwjgl.opengl.GL11C.glGetString; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import net.neoforged.fml.earlydisplay.render.elements.ImageElement; -import net.neoforged.fml.earlydisplay.render.elements.LabelElement; -import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; -import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; -import net.neoforged.fml.earlydisplay.render.elements.RenderElement; -import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; -import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; -import org.lwjgl.opengl.GL32C; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class LoadingScreenRenderer implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(LoadingScreenRenderer.class); + public static final int LAYOUT_WIDTH = 854; + public static final int LAYOUT_HEIGHT = 480; private final long glfwWindow; private final MaterializedTheme theme; @@ -74,10 +78,10 @@ public class LoadingScreenRenderer implements AutoCloseable { * Nothing fancy, we just want to draw and render text. */ public LoadingScreenRenderer(ScheduledExecutorService scheduler, - long glfwWindow, - Theme theme, - String mcVersion, - String neoForgeVersion) { + long glfwWindow, + Theme theme, + String mcVersion, + String neoForgeVersion) { this.glfwWindow = glfwWindow; this.mcVersion = mcVersion; this.neoForgeVersion = neoForgeVersion; @@ -96,7 +100,7 @@ public LoadingScreenRenderer(ScheduledExecutorService scheduler, this.elements = loadElements(); // we always render to an 854x480 texture and then fit that to the screen - framebuffer = new EarlyFramebuffer(854, 480); + framebuffer = new EarlyFramebuffer(LAYOUT_WIDTH, LAYOUT_HEIGHT); // Set the clear color based on the colour scheme var background = theme.colorScheme().screenBackground(); @@ -115,26 +119,33 @@ private List loadElements() { var elements = new ArrayList(); var loadingScreen = theme.theme().loadingScreen(); - if (!loadingScreen.performance().visibility()) { + if (loadingScreen.performance().visible()) { elements.add(new PerformanceElement(loadingScreen.performance(), theme)); } - if (!loadingScreen.startupLog().visibility()) { + if (loadingScreen.startupLog().visible()) { elements.add(new StartupLogElement(loadingScreen.startupLog(), theme)); } - if (!loadingScreen.progressBars().visibility()) { + if (loadingScreen.progressBars().visible()) { elements.add(new ProgressBarsElement(loadingScreen.progressBars(), theme)); } + if (loadingScreen.mojangLogo().visible()) { + elements.add(new MojangLogoElement(loadingScreen.mojangLogo(), theme)); + } // Add decorative elements - for (var element : theme.theme().decoration()) { - elements.add(loadElement(element)); + for (var entry : loadingScreen.decoration().entrySet()) { + var element = entry.getValue(); + if (!element.visible()) { + continue; // Likely reconfigured in an extended theme + } + elements.add(loadElement(entry.getKey(), element)); } return elements; } - private RenderElement loadElement(ThemeElement element) { - return switch (element) { + private RenderElement loadElement(String id, ThemeElement element) { + var renderElement = switch (element) { case ThemeImageElement imageElement -> new ImageElement(imageElement, theme); case ThemeLabelElement labelElement -> new LabelElement( @@ -143,8 +154,11 @@ private RenderElement loadElement(ThemeElement element) { Map.of( "version", mcVersion + "-" + neoForgeVersion.split("-")[0])); - default -> throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); + default -> + throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); }; + renderElement.setId(id); + return renderElement; } public void stopAutomaticRendering() throws TimeoutException, InterruptedException { @@ -178,14 +192,12 @@ public void renderToScreen() { GlState.readFromOpenGL(); var backup = GlState.createSnapshot(); - framebuffer.activate(); - GlState.viewport(0, 0, framebuffer.width(), framebuffer.height()); - renderToFramebuffer(); - framebuffer.deactivate(); - int[] w = new int[1]; int[] h = new int[1]; glfwGetFramebufferSize(glfwWindow, w, h); + framebuffer.resize(w[0], h[0]); + + renderToFramebuffer(); GlState.viewport(0, 0, w[0], h[0]); framebuffer.blitToScreen(this.theme.theme().colorScheme().screenBackground(), w[0], h[0]); @@ -207,8 +219,8 @@ public void renderToFramebuffer() { GlState.readFromOpenGL(); var backup = GlState.createSnapshot(); - GlState.viewport(0, 0, framebuffer.width(), framebuffer.height()); framebuffer.activate(); + GlState.viewport(0, 0, framebuffer.width(), framebuffer.height()); // Clear the screen to our color var background = theme.theme().colorScheme().screenBackground(); @@ -220,11 +232,11 @@ public void renderToFramebuffer() { for (var shader : theme.shaders().values()) { shader.activate(); if (shader.hasUniform(ElementShader.UNIFORM_SCREEN_SIZE)) { - shader.setUniform2f(ElementShader.UNIFORM_SCREEN_SIZE, framebuffer.width(), framebuffer.height()); + shader.setUniform2f(ElementShader.UNIFORM_SCREEN_SIZE, LAYOUT_WIDTH, LAYOUT_HEIGHT); } } - var context = new RenderContext(buffer, theme, framebuffer.width(), framebuffer.height(), animationFrame); + var context = new RenderContext(buffer, theme, LAYOUT_WIDTH, LAYOUT_HEIGHT, animationFrame); for (var element : this.elements) { element.render(context); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java index caa6f26bb..3be57fea3 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java @@ -8,17 +8,22 @@ import net.neoforged.fml.earlydisplay.theme.TextureScaling; import net.neoforged.fml.earlydisplay.util.Bounds; -public class QuadHelper { +class QuadHelper { public static void fillSprite(SimpleBufferBuilder buffer, - Texture texture, - float x, - float y, - float z, - float width, - float height, - int color, - SpriteFillDirection fillDirection, - int animationFrame) { + Texture texture, + float x, + float y, + float z, + float width, + float height, + int color, + SpriteFillDirection fillDirection, + int animationFrame, + float srcU0, + float srcU1, + float srcV0, + float srcV1 + ) { // Too large values for width / height cause immediate crashes of the VM due to graphics driver bugs< // These maximum values are picked without too much thought. width = Math.min(65535, width); @@ -33,6 +38,14 @@ public static void fillSprite(SimpleBufferBuilder buffer, v1 = (animationFrame % frameCount + 1) * vUnit; } + // Apply a source region of the texture if requested + var w = (u1 - u0); + u1 = u0 + w * srcU1; + u0 = u0 + w * srcU0; + var h = (v1 - v0); + v1 = v0 + h * srcV1; + v0 = v0 + h * srcV0; + switch (texture.scaling()) { case TextureScaling.Tile tiled -> { fillTiled(buffer, x, y, z, width, height, color, tiled.width(), tiled.height(), u0, u1, v0, v1, fillDirection); @@ -43,22 +56,23 @@ public static void fillSprite(SimpleBufferBuilder buffer, case TextureScaling.NineSlice nineSlice -> { addTiledNineSlice(buffer, x, y, z, width, height, color, nineSlice, u0, u1, v0, v1); } - default -> {} + default -> { + } } } private static void addTiledNineSlice(SimpleBufferBuilder buffer, - float x, - float y, - float z, - float width, - float height, - int color, - TextureScaling.NineSlice nineSlice, - float u0, - float u1, - float v0, - float v1) { + float x, + float y, + float z, + float width, + float height, + int color, + TextureScaling.NineSlice nineSlice, + float u0, + float u1, + float v0, + float v1) { var leftWidth = Math.min(nineSlice.left(), width / 2); var rightWidth = Math.min(nineSlice.right(), width / 2); var topHeight = Math.min(nineSlice.top(), height / 2); @@ -123,13 +137,13 @@ private static void addTiledNineSlice(SimpleBufferBuilder buffer, } private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float destTileWidth, - float destTileHeight, float u0, float u1, float v0, float v1) { + float destTileHeight, float u0, float u1, float v0, float v1) { fillTiled(buffer, x, y, z, width, height, color, destTileWidth, destTileHeight, u0, u1, v0, v1, SpriteFillDirection.TOP_TO_BOTTOM); } private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float destTileWidth, - float destTileHeight, float u0, float u1, float v0, float v1, SpriteFillDirection fillDirection) { + float destTileHeight, float u0, float u1, float v0, float v1, SpriteFillDirection fillDirection) { if (destTileWidth <= 0 || destTileHeight <= 0) { return; } @@ -169,7 +183,7 @@ private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, floa } public static void addQuad(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float minU, float maxU, - float minV, float maxV) { + float minV, float maxV) { if (width < 0 || height < 0) { return; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java index 18d8a47bd..0dc314c34 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -5,13 +5,14 @@ package net.neoforged.fml.earlydisplay.render; -import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; - -import java.util.List; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.util.Bounds; +import java.util.List; + +import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; + public record RenderContext( SimpleBufferBuilder sharedBuffer, MaterializedTheme theme, @@ -37,6 +38,20 @@ public void blitTexture(Texture texture, float x, float y, float width, float he } public void blitTexture(Texture texture, float x, float y, float width, float height, int color) { + blitTextureRegion(texture, x, y, width, height, color, 0, 1, 0, 1); + } + + public void blitTextureRegion(Texture texture, + float x, + float y, + float width, + float height, + int color, + float u0, + float u1, + float v0, + float v1 + ) { GlState.activeTexture(GL_TEXTURE0); GlState.bindTexture2D(texture.textureId()); @@ -55,7 +70,12 @@ public void blitTexture(Texture texture, float x, float y, float width, float he height, color, QuadHelper.SpriteFillDirection.TOP_TO_BOTTOM, - animationFrame); + animationFrame, + u0, + u1, + v0, + v1 + ); sharedBuffer.draw(); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java index 5950bd74c..a96d324e5 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleBufferBuilder.java @@ -35,7 +35,7 @@ * * @author covers1624 */ -public class SimpleBufferBuilder implements Closeable { +class SimpleBufferBuilder implements Closeable { private static final MemoryUtil.MemoryAllocator ALLOCATOR = MemoryUtil.getAllocator(false); private static final int[] VERTEX_ARRAYS = new int[Format.values().length]; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java index 275989060..0f0c26162 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java @@ -5,6 +5,7 @@ package net.neoforged.fml.earlydisplay.render; +import static org.lwjgl.opengl.GL11C.GL_NEAREST; import static org.lwjgl.opengl.GL32C.GL_CLAMP_TO_EDGE; import static org.lwjgl.opengl.GL32C.GL_LINEAR; import static org.lwjgl.opengl.GL32C.GL_RED; @@ -143,8 +144,8 @@ public SimpleFont(ThemeResource resource, int scale) throws IOException { 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); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } } try (var q = STBTTAlignedQuad.malloc()) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java index a5afa129c..80a7dbc6a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -5,27 +5,31 @@ package net.neoforged.fml.earlydisplay.render; +import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; +import net.neoforged.fml.earlydisplay.theme.TextureScaling; +import net.neoforged.fml.earlydisplay.theme.ThemeTexture; +import net.neoforged.fml.earlydisplay.theme.UncompressedImage; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL32C; + import static org.lwjgl.opengl.GL11C.GL_LINEAR; import static org.lwjgl.opengl.GL11C.GL_NEAREST; import static org.lwjgl.opengl.GL11C.GL_RGBA; import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D; import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MAG_FILTER; import static org.lwjgl.opengl.GL11C.GL_TEXTURE_MIN_FILTER; +import static org.lwjgl.opengl.GL11C.GL_TEXTURE_WRAP_S; +import static org.lwjgl.opengl.GL11C.GL_TEXTURE_WRAP_T; import static org.lwjgl.opengl.GL11C.GL_UNSIGNED_BYTE; import static org.lwjgl.opengl.GL11C.glGenTextures; import static org.lwjgl.opengl.GL11C.glTexImage2D; import static org.lwjgl.opengl.GL11C.glTexParameteri; +import static org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE; import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; -import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; -import net.neoforged.fml.earlydisplay.theme.TextureScaling; -import net.neoforged.fml.earlydisplay.theme.ThemeTexture; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.opengl.GL32C; - public record Texture(int textureId, int physicalWidth, int physicalHeight, - TextureScaling scaling, - @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { + TextureScaling scaling, + @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { public int width() { return scaling.width(); } @@ -39,19 +43,30 @@ public int height() { */ public static Texture create(ThemeTexture themeTexture) { try (var image = themeTexture.resource().loadAsImage()) { - var texId = glGenTextures(); - GlState.activeTexture(GL_TEXTURE0); - GlState.bindTexture2D(texId); - GlDebug.labelTexture(texId, "EarlyDisplay " + themeTexture); - boolean linear = themeTexture.scaling().linearScaling(); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.imageData()); - GlState.activeTexture(GL_TEXTURE0); - return new Texture(texId, image.width(), image.height(), themeTexture.scaling(), themeTexture.animation()); + return create(image, "EarlyDisplay " + themeTexture, themeTexture.scaling(), themeTexture.animation()); } } + public static Texture create( + UncompressedImage image, + String debugName, + TextureScaling scaling, + @Nullable AnimationMetadata animation + ) { + var texId = glGenTextures(); + GlState.activeTexture(GL_TEXTURE0); + GlState.bindTexture2D(texId); + GlDebug.labelTexture(texId, debugName); + boolean linear = scaling.linearScaling(); + 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, linear ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.imageData()); + GlState.activeTexture(GL_TEXTURE0); + return new Texture(texId, image.width(), image.height(), scaling, animation); + } + @Override public void close() { GL32C.glDeleteTextures(textureId); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java new file mode 100644 index 000000000..63c6837ca --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.render.elements; + +import net.neoforged.fml.earlydisplay.render.MaterializedTheme; +import net.neoforged.fml.earlydisplay.render.RenderContext; +import net.neoforged.fml.earlydisplay.render.Texture; +import net.neoforged.fml.earlydisplay.theme.ClasspathResource; +import net.neoforged.fml.earlydisplay.theme.TextureScaling; +import net.neoforged.fml.earlydisplay.theme.ThemeMojangLogoElement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MojangLogoElement extends RenderElement { + private static final Logger LOGGER = LoggerFactory.getLogger(MojangLogoElement.class); + + /** + * Potential paths on the classpath for the Mojang logo. + */ + private static final String[] LOGO_PATHS = { + "assets/minecraft/textures/gui/title/mojangstudios.png" + }; + + private final Texture mojangLogo; + + public MojangLogoElement(ThemeMojangLogoElement element, MaterializedTheme theme) { + super(element, theme); + + Texture mojangLogo = null; + for (var logoPath : LOGO_PATHS) { + try (var image = new ClasspathResource(logoPath).tryLoadAsImage()) { + if (image == null) { + LOGGER.debug("Failed to load Mojang logo from {}", logoPath); + continue; + } + + mojangLogo = Texture.create(image, "mojang logo", new TextureScaling.Stretch(512, 128, true), null); + } + } + this.mojangLogo = mojangLogo; + } + + @Override + public void render(RenderContext context) { + var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), 512, 128); + + float x0 = bounds.left(); + float x1 = (bounds.left() + bounds.right()) / 2; + float x2 = bounds.right(); + context.blitTextureRegion(this.mojangLogo, x0, bounds.top(), x1 - x0, bounds.height(), -1, 0, 1, 0, 0.5f); + context.blitTextureRegion(this.mojangLogo, x1, bounds.top(), x2 - x1, bounds.height(), -1, 0, 1, 0.5f, 1f); + } + + @Override + public void close() { + if (mojangLogo != null) { + mojangLogo.close(); + } + } +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java index 35519ab25..451266fc4 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/PerformanceElement.java @@ -33,6 +33,7 @@ public class PerformanceElement extends RenderElement { public PerformanceElement(ThemePerformanceElement settings, MaterializedTheme theme) { super(settings, theme); + setId("performance"); osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); memoryBean = ManagementFactory.getMemoryMXBean(); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java index 7a88cfd83..a632cc02f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java @@ -11,40 +11,38 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.util.Bounds; import net.neoforged.fml.earlydisplay.util.StyleLength; +import org.jetbrains.annotations.Nullable; public abstract class RenderElement implements AutoCloseable { - private final String id; protected final MaterializedTheme theme; - private boolean maintainAspectRatio; - private StyleLength left; - private StyleLength top; - private StyleLength right; - private StyleLength bottom; + @Nullable + private String id; protected SimpleFont font; + private final ThemeElement element; public RenderElement(ThemeElement element, MaterializedTheme theme) { this.theme = theme; - this.id = element.id(); this.font = theme.getFont(element.font()); - this.left = element.left(); - this.top = element.top(); - this.right = element.right(); - this.bottom = element.bottom(); - this.maintainAspectRatio = element.maintainAspectRatio(); + this.element = element; } + @Nullable public String id() { return this.id; } + public void setId(@Nullable String id) { + this.id = id; + } + public abstract void render(RenderContext context); public Bounds resolveBounds(float availableWidth, float availableHeight, float intrinsicWidth, float intrinsicHeight) { - var left = resolve(this.left, availableWidth); - var right = availableWidth - resolve(this.right, availableWidth); - var top = resolve(this.top, availableHeight); - var bottom = availableHeight - resolve(this.bottom, availableHeight); + var left = resolve(element.left(), availableWidth); + var right = availableWidth - resolve(element.right(), availableWidth); + var top = resolve(element.top(), availableHeight); + var bottom = availableHeight - resolve(element.bottom(), availableHeight); var width = right - left; var height = bottom - top; @@ -54,7 +52,7 @@ public Bounds resolveBounds(float availableWidth, float availableHeight, float i // Handle aspect ratio if (widthDefined != heightDefined) { - if (maintainAspectRatio) { + if (element.maintainAspectRatio()) { float ar = intrinsicWidth / intrinsicHeight; if (widthDefined) { height = width / ar; @@ -89,6 +87,19 @@ public Bounds resolveBounds(float availableWidth, float availableHeight, float i top = bottom - height; } + // Apply centering with the assumption any offset on the corresponding edge is to be interpreted as an + // offset from the center instead + if (element.centerHorizontally()) { + width = right - left; + left = availableWidth / 2 - width / 2 + left; + right = left + width; + } + if (element.centerVertically()) { + height = bottom - top; + top = availableHeight / 2 - height / 2 + top; + bottom = top + height; + } + return new Bounds(left, top, right, bottom); } @@ -101,147 +112,6 @@ private float resolve(StyleLength length, float availableSpace) { }; } - public StyleLength left() { - return left; - } - - public void setLeft(StyleLength left) { - this.left = left; - } - - public StyleLength top() { - return top; - } - - public void setTop(StyleLength top) { - this.top = top; - } - - public StyleLength right() { - return right; - } - - public void setRight(StyleLength right) { - this.right = right; - } - - public StyleLength bottom() { - return bottom; - } - - public void setBottom(StyleLength bottom) { - this.bottom = bottom; - } - - public boolean maintainAspectRatio() { - return maintainAspectRatio; - } - - public void setMaintainAspectRatio(boolean maintainAspectRatio) { - this.maintainAspectRatio = maintainAspectRatio; - } - - // -// public void retire(final int frame) { -// this.retireCount = frame; -// } -// -// public static RenderElement mojang(final int textureId, final int frameStart) { -// return new RenderElement("mojang logo", () -> (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); -// GlState.bindTexture2D(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, ctx.colourScheme.foreground().packedint(fade)); -// QuadHelper.loadQuad(bb, x0 + size, x0 + 2 * size, y0, y0 + size / 2f, 0f, 1f, 0.5f, 1f, ctx.colourScheme.foreground().packedint(fade)); -// bb.draw(); -// GlState.bindTexture2D(0); -// }); -// } -// -// public static RenderElement forgeVersionOverlay(SimpleFont font, String version) { -// return new RenderElement("version overlay", 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("squir", RenderElement.initializeTexture(SQUIR, (bb, context, size, frame) -> { -// -// })); -// } -// -// public static RenderElement fox(SimpleFont font) { -// return new RenderElement("fox", RenderElement.initializeTexture(FOX_RUNNING, (bb, context, size, frame) -> { -// int framecount = 28; -// float aspect = size[0] * (float) framecount / size[1]; -// int outsize = size[0]; -// int offset = outsize / 6; -// var x0 = context.scaledWidth() - outsize * context.scale() + offset; -// var x1 = context.scaledWidth() + offset; -// var y0 = context.scaledHeight() - outsize * context.scale() / aspect - font.descent() - font.lineSpacing(); -// var y1 = context.scaledHeight() - font.descent() - font.lineSpacing(); -// int frameidx = frame % framecount; -// float framesize = 1 / (float) framecount; -// float framepos = frameidx * framesize; -// QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, framepos, framepos + framesize, globalAlpha << 24 | 0xFFFFFF); -// })); -// } -// -// public static RenderElement progressBars(SimpleFont font) { -// return new RenderElement("progress bars", () -> (bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame)); -// } -// -// public static RenderElement performanceBar(SimpleFont font) { -// return new RenderElement("performance bar", () -> (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); -// } -// if (acc != null) -// acc.accept(buffer, context, frameNumber); -// } -// -// 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[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, BAR_WIDTH * ctx.scale()}, 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); -// } -// -// 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) { -// GlState.activeTexture(GL_TEXTURE0); -// GlState.bindTexture2D(font.textureId()); -// 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)); -// } - public static float clamp(float num, float min, float max) { if (num < min) { return min; @@ -259,7 +129,8 @@ public static int clamp(int num, int min, int max) { } @Override - public void close() {} + public void close() { + } @Override public String toString() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java index 45eeb7888..9c196eb6e 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -5,6 +5,7 @@ package net.neoforged.fml.earlydisplay.theme; +import org.jetbrains.annotations.Nullable; import org.lwjgl.stb.STBImage; import org.lwjgl.system.MemoryUtil; import org.slf4j.Logger; @@ -16,7 +17,10 @@ * Theme images will refer to a resource their content is loaded from, this is expected to be a PNG image. */ final class ImageLoader { + private static final String BROKEN_TEXTURE_NAME = "broken texture"; + private static final int BROKEN_TEXTURE_DIMENSIONS = 16; + static final Logger LOGGER = LoggerFactory.getLogger(ImageLoader.class); /** @@ -24,23 +28,41 @@ final class ImageLoader { * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ static UncompressedImage loadImage(ThemeResource resource) { + return switch (tryLoadImage(resource)) { + case ImageLoadResult.Success(UncompressedImage image) -> image; + case ImageLoadResult.Error(Exception exception) -> { + LOGGER.error("Failed to load theme image {}", resource, exception); + yield createBrokenImage(); + } + }; + } + + /** + * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. + * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. + */ + static ImageLoadResult tryLoadImage(ThemeResource resource) { try (var buffer = resource.toNativeBuffer()) { var width = new int[1]; var height = new int[1]; var channels = new int[1]; var decodedImage = STBImage.stbi_load_from_memory(buffer.buffer(), width, height, channels, 4); // TODO: Handle image decoding error - return new UncompressedImage(resource.toString(), + return new ImageLoadResult.Success(new UncompressedImage(resource.toString(), resource, new NativeBuffer(decodedImage, STBImage::stbi_image_free), width[0], - height[0]); + height[0])); } catch (Exception e) { - LOGGER.error("Failed to load theme image {}", resource, e); - return createBrokenImage(); + return new ImageLoadResult.Error(e); } } + public sealed interface ImageLoadResult { + record Success(UncompressedImage image) implements ImageLoadResult {} + record Error(Exception exception) implements ImageLoadResult {} + } + private static UncompressedImage createBrokenImage() { var pixelData = MemoryUtil.memAlloc(BROKEN_TEXTURE_DIMENSIONS * BROKEN_TEXTURE_DIMENSIONS * 4); var pixelBuffer = pixelData.asIntBuffer(); // ABGR format @@ -57,12 +79,13 @@ private static UncompressedImage createBrokenImage() { var nativeBuffer = new NativeBuffer(pixelData, MemoryUtil::memFree); return new UncompressedImage( - "broken texture", + BROKEN_TEXTURE_NAME, null, nativeBuffer, BROKEN_TEXTURE_DIMENSIONS, BROKEN_TEXTURE_DIMENSIONS); } - private ImageLoader() {} + private ImageLoader() { + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index cdecb61e2..a9bf2acd9 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -5,9 +5,6 @@ package net.neoforged.fml.earlydisplay.theme; -import java.util.List; -import java.util.Map; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; @@ -15,11 +12,12 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; import net.neoforged.fml.earlydisplay.util.StyleLength; +import java.util.Map; + /** * Defines a theme for the early display screen. * * @param windowIcon The icon used for the loading screen operating system window until Minecraft takes over. - * @param decoration Additional decoration elements. * @param fonts Defines font assets. Must contain a font named 'default', which is used if fonts are not overridden * for individual loading screen elements. * @param shaders Defines the GLSL shaders used to draw elements of the loading screen. Overriding shaders is an advanced feature. @@ -30,7 +28,6 @@ public record Theme( ThemeResource windowIcon, Map fonts, Map shaders, - List decoration, ThemeColorScheme colorScheme, ThemeSprites sprites, ThemeLoadingScreen loadingScreen) { @@ -39,6 +36,7 @@ public record Theme( public static final String SHADER_GUI = "gui"; public static final String SHADER_FONT = "font"; public static final String SHADER_COLOR = "color"; + public static Theme createDefaultTheme() { var sprites = new ThemeSprites( new ThemeTexture( @@ -53,7 +51,6 @@ public static Theme createDefaultTheme() { false); var squir = new ThemeImageElement(); - squir.setId("squir"); squir.setTexture(new ThemeTexture(classpathResource("squirrel.png"), new TextureScaling.Stretch(112, 112, true))); var startupLog = new ThemeStartupLogElement(); @@ -61,7 +58,6 @@ public static Theme createDefaultTheme() { startupLog.setBottom(StyleLength.ofPoints(10)); var fox = new ThemeImageElement(); - fox.setId("fox"); fox.setTexture( new ThemeTexture( classpathResource("fox_running.png"), @@ -71,7 +67,6 @@ public static Theme createDefaultTheme() { fox.setBottom(StyleLength.ofREM(1)); var forgeVersion = new ThemeLabelElement(); - forgeVersion.setId("version"); forgeVersion.setText("${version}"); forgeVersion.setBottom(StyleLength.ofPoints(10)); forgeVersion.setRight(StyleLength.ofPoints(10)); @@ -89,6 +84,10 @@ public static Theme createDefaultTheme() { performance.setRight(StyleLength.ofPoints(220)); performance.setTop(StyleLength.ofPoints(10)); + var mojangLogo = new ThemeMojangLogoElement(); + mojangLogo.setCenterHorizontally(true); + mojangLogo.setTop(StyleLength.ofPoints(96)); + return new Theme( classpathResource("neoforged_icon.png"), Map.of( @@ -100,13 +99,18 @@ FONT_DEFAULT, classpathResource("Monocraft.ttf")), ThemeShader.DEFAULT_FONT, SHADER_COLOR, ThemeShader.DEFAULT_COLOR), - List.of(squir, fox, forgeVersion), ThemeColorScheme.DEFAULT, sprites, new ThemeLoadingScreen( performance, progressBars, - startupLog)); + startupLog, + mojangLogo, + Map.of( + "squir", squir, + "fox", fox, + "version", forgeVersion + ))); } private static ClasspathResource classpathResource(String name) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java index f3728f0f5..83f6849d3 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java @@ -5,14 +5,20 @@ package net.neoforged.fml.earlydisplay.theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; +import java.util.Map; + /** * Describes the themable properties of the loading screen. */ public record ThemeLoadingScreen( ThemePerformanceElement performance, ThemeProgressBarsElement progressBars, - ThemeStartupLogElement startupLog) {} + ThemeStartupLogElement startupLog, + ThemeMojangLogoElement mojangLogo, + Map decoration) { +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java new file mode 100644 index 000000000..84659e06f --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.theme; + +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; + +public class ThemeMojangLogoElement extends ThemeElement { +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java index 8639b5686..b1bcd9a8f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java @@ -5,6 +5,8 @@ package net.neoforged.fml.earlydisplay.theme; +import org.jetbrains.annotations.Nullable; + import java.io.IOException; public sealed interface ThemeResource permits ClasspathResource, FileResource { @@ -22,4 +24,16 @@ public sealed interface ThemeResource permits ClasspathResource, FileResource { default UncompressedImage loadAsImage() { return ImageLoader.loadImage(this); } + + /** + * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. + * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. + */ + @Nullable + default UncompressedImage tryLoadAsImage() { + return switch(ImageLoader.tryLoadImage(this)) { + case ImageLoader.ImageLoadResult.Error error -> null; + case ImageLoader.ImageLoadResult.Success(UncompressedImage image) -> image; + }; + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java index dc6595bbf..2510174f4 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java @@ -10,14 +10,4 @@ * have no specific functionality. */ public abstract class ThemeDecorativeElement extends ThemeElement { - private String id; - - @Override - public String id() { - return id; - } - - public void setId(String id) { - this.id = id; - } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java index c7dc528b9..e54f316d4 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -5,27 +5,28 @@ package net.neoforged.fml.earlydisplay.theme.elements; -import java.util.Objects; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.util.StyleLength; +import java.util.Objects; + public abstract class ThemeElement { - private boolean visibility = false; + private boolean visible = true; private boolean maintainAspectRatio = true; private StyleLength left = StyleLength.ofUndefined(); private StyleLength top = StyleLength.ofUndefined(); private StyleLength right = StyleLength.ofUndefined(); private StyleLength bottom = StyleLength.ofUndefined(); + private boolean centerHorizontally; + private boolean centerVertically; private String font = Theme.FONT_DEFAULT; - public abstract String id(); - - public boolean visibility() { - return visibility; + public boolean visible() { + return visible; } - public void setVisibility(boolean visibility) { - this.visibility = visibility; + public void setVisible(boolean visible) { + this.visible = visible; } public StyleLength left() { @@ -76,8 +77,19 @@ public void setFont(String font) { this.font = font; } - @Override - public String toString() { - return id(); + public boolean centerHorizontally() { + return centerHorizontally; + } + + public void setCenterHorizontally(boolean centerHorizontally) { + this.centerHorizontally = centerHorizontally; + } + + public boolean centerVertically() { + return centerVertically; + } + + public void setCenterVertically(boolean centerVertically) { + this.centerVertically = centerVertically; } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java index 681024d78..eee6e9ccb 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java @@ -6,8 +6,4 @@ package net.neoforged.fml.earlydisplay.theme.elements; public class ThemePerformanceElement extends ThemeElement { - @Override - public String id() { - return "performance"; - } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java index 743049b34..dd82743f0 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeProgressBarsElement.java @@ -16,11 +16,6 @@ public class ThemeProgressBarsElement extends ThemeElement { */ private int barGap; - @Override - public String id() { - return "progressBars"; - } - public int labelGap() { return labelGap; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java index a8076b58b..726bb4b07 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java @@ -6,8 +6,4 @@ package net.neoforged.fml.earlydisplay.theme.elements; public class ThemeStartupLogElement extends ThemeElement { - @Override - public String id() { - return "startupLog"; - } } diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json index d55b7d362..08a57b358 100644 --- a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json @@ -5,63 +5,19 @@ "default": "classpath:net/neoforged/fml/earlydisplay/theme/Monocraft.ttf" }, "shaders": { - "font": { - "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", - "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui_font.frag" - }, "color": { "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui_color.frag" }, + "font": { + "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", + "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui_font.frag" + }, "gui": { "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui.frag" } }, - "decoration": [ - { - "type": "image", - "texture": { - "resource": "classpath:net/neoforged/fml/earlydisplay/theme/squirrel.png", - "scaling": { - "type": "stretch", - "width": 112, - "height": 112 - } - }, - "id": "squir", - "visibility": false, - "maintainAspectRatio": true - }, - { - "type": "image", - "texture": { - "resource": "classpath:net/neoforged/fml/earlydisplay/theme/fox_running.png", - "scaling": { - "type": "stretch", - "width": 151, - "height": 128 - }, - "animation": { - "frameCount": 28 - } - }, - "id": "fox", - "visibility": false, - "maintainAspectRatio": true, - "right": 10.0, - "bottom": "1.0rem" - }, - { - "type": "label", - "text": "${version}", - "id": "version", - "visibility": false, - "maintainAspectRatio": true, - "right": 10.0, - "bottom": 10.0 - } - ], "colorScheme": { "screenBackground": "#ef323d", "text": "#ffffff", @@ -80,7 +36,8 @@ "right": 2, "bottom": 2, "stretchHorizontalFill": true, - "stretchVerticalFill": true + "stretchVerticalFill": true, + "linearScaling": false } }, "progressBarForeground": { @@ -94,7 +51,8 @@ "right": 4, "bottom": 4, "stretchHorizontalFill": true, - "stretchVerticalFill": true + "stretchVerticalFill": true, + "linearScaling": false } }, "progressBarIndeterminate": { @@ -108,33 +66,103 @@ "right": 4, "bottom": 4, "stretchHorizontalFill": true, - "stretchVerticalFill": true + "stretchVerticalFill": true, + "linearScaling": false } }, "progressBarIndeterminateBounces": false }, "loadingScreen": { "performance": { - "visibility": false, + "visible": true, "maintainAspectRatio": true, "left": 220.0, "top": 10.0, - "right": 220.0 + "right": 220.0, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" }, "progressBars": { "labelGap": 4, "barGap": 5, - "visibility": false, + "visible": true, "maintainAspectRatio": false, "left": 220.0, "top": 250.0, - "right": 220.0 + "right": 220.0, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" }, "startupLog": { - "visibility": false, + "visible": true, "maintainAspectRatio": true, "left": 10.0, - "bottom": 10.0 + "bottom": 10.0, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" + }, + "mojangLogo": { + "visible": true, + "maintainAspectRatio": true, + "top": 96.0, + "centerHorizontally": true, + "centerVertically": false, + "font": "default" + }, + "decoration": { + "fox": { + "type": "image", + "texture": { + "resource": "classpath:net/neoforged/fml/earlydisplay/theme/fox_running.png", + "scaling": { + "type": "stretch", + "width": 151, + "height": 128, + "linearScaling": false + }, + "animation": { + "frameCount": 28 + } + }, + "visible": true, + "maintainAspectRatio": true, + "right": 10.0, + "bottom": "1.0rem", + "centerHorizontally": false, + "centerVertically": false, + "font": "default" + }, + "version": { + "type": "label", + "text": "${version}", + "visible": true, + "maintainAspectRatio": true, + "right": 10.0, + "bottom": 10.0, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" + }, + "squir": { + "type": "image", + "texture": { + "resource": "classpath:net/neoforged/fml/earlydisplay/theme/squirrel.png", + "scaling": { + "type": "stretch", + "width": 112, + "height": 112, + "linearScaling": true + } + }, + "visible": true, + "maintainAspectRatio": true, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" + } } } } \ No newline at end of file diff --git a/tests/build.gradle b/tests/build.gradle index 28e43a10d..e47dcaebc 100644 --- a/tests/build.gradle +++ b/tests/build.gradle @@ -3,7 +3,10 @@ plugins { } neoForge { - version = test_neoforge_version + enable { + version = test_neoforge_version + enabledSourceSets = [sourceSets.test] + } runs { client { client() @@ -86,3 +89,7 @@ configurations.configureEach { substitute module("net.neoforged.fancymodloader:earlydisplay") using project(":earlydisplay") } } + +dependencies { + testImplementation project(":earlydisplay") +} diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java b/tests/src/test/java/TestEarlyDisplay.java similarity index 50% rename from earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java rename to tests/src/test/java/TestEarlyDisplay.java index f1e4c107d..4a090768d 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/TestEarlyDisplay.java +++ b/tests/src/test/java/TestEarlyDisplay.java @@ -3,16 +3,19 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.earlydisplay; - +import net.neoforged.fml.earlydisplay.DisplayWindow; import net.neoforged.fml.loading.FMLPaths; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + public class TestEarlyDisplay { public static void main(String[] args) throws Exception { System.setProperty("java.awt.headless", "true"); System.setProperty("fml.earlyWindowDarkMode", "true"); - FMLPaths.loadAbsolutePaths(TestUtil.findProjectRoot()); + FMLPaths.loadAbsolutePaths(findProjectRoot()); var window = new DisplayWindow(); var periodicTick = window.initialize(new String[] { @@ -30,4 +33,21 @@ public static void main(String[] args) throws Exception { } } } + + static Path findProjectRoot() throws Exception { + // Find the project directory by search for build.gradle upwards + return findProjectRoot(Paths.get(TestEarlyDisplay.class.getProtectionDomain().getCodeSource().getLocation().toURI())); + } + + static Path findProjectRoot(Path path) { + Path current = path; + while (current != null) { + if (Files.exists(current.resolve("build.gradle"))) { + return current; + } + current = current.getParent(); + } + + throw new IllegalArgumentException("Couldn't find buid.gradle in any parent directory of " + path); + } } From 0069708fbf5be4050f6f146106b46b1fb9d60a12 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 4 May 2025 10:50:32 +0200 Subject: [PATCH 14/22] Formatting --- .../fml/earlydisplay/DisplayWindow.java | 3 +- .../render/LoadingScreenRenderer.java | 58 +++++++++---------- .../fml/earlydisplay/render/QuadHelper.java | 58 +++++++++---------- .../earlydisplay/render/RenderContext.java | 29 +++++----- .../fml/earlydisplay/render/SimpleFont.java | 1 - .../fml/earlydisplay/render/Texture.java | 21 ++++--- .../render/elements/RenderElement.java | 3 +- .../fml/earlydisplay/theme/ImageLoader.java | 5 +- .../fml/earlydisplay/theme/Theme.java | 7 +-- .../theme/ThemeLoadingScreen.java | 6 +- .../theme/ThemeMojangLogoElement.java | 3 +- .../fml/earlydisplay/theme/ThemeResource.java | 5 +- .../elements/ThemeDecorativeElement.java | 3 +- .../theme/elements/ThemeElement.java | 3 +- .../elements/ThemePerformanceElement.java | 3 +- .../elements/ThemeStartupLogElement.java | 3 +- tests/src/test/java/TestEarlyDisplay.java | 5 +- 17 files changed, 96 insertions(+), 120 deletions(-) diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 8eaa8c002..897684746 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -499,8 +499,7 @@ public void completeProgress() { mainProgress.complete(); } - public void addMojangTexture(final int textureId) { - } + public void addMojangTexture(final int textureId) {} public void close() { // Close the Render Scheduler thread diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index 2e0e8a515..ca66e7b44 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -5,30 +5,6 @@ package net.neoforged.fml.earlydisplay.render; -import net.neoforged.fml.earlydisplay.render.elements.ImageElement; -import net.neoforged.fml.earlydisplay.render.elements.LabelElement; -import net.neoforged.fml.earlydisplay.render.elements.MojangLogoElement; -import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; -import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; -import net.neoforged.fml.earlydisplay.render.elements.RenderElement; -import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; -import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; -import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; -import org.lwjgl.opengl.GL32C; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - import static org.lwjgl.glfw.GLFW.glfwGetFramebufferSize; import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; @@ -45,6 +21,29 @@ import static org.lwjgl.opengl.GL11C.glClear; import static org.lwjgl.opengl.GL11C.glGetString; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import net.neoforged.fml.earlydisplay.render.elements.ImageElement; +import net.neoforged.fml.earlydisplay.render.elements.LabelElement; +import net.neoforged.fml.earlydisplay.render.elements.MojangLogoElement; +import net.neoforged.fml.earlydisplay.render.elements.PerformanceElement; +import net.neoforged.fml.earlydisplay.render.elements.ProgressBarsElement; +import net.neoforged.fml.earlydisplay.render.elements.RenderElement; +import net.neoforged.fml.earlydisplay.render.elements.StartupLogElement; +import net.neoforged.fml.earlydisplay.theme.Theme; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; +import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import org.lwjgl.opengl.GL32C; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class LoadingScreenRenderer implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(LoadingScreenRenderer.class); public static final int LAYOUT_WIDTH = 854; @@ -78,10 +77,10 @@ public class LoadingScreenRenderer implements AutoCloseable { * Nothing fancy, we just want to draw and render text. */ public LoadingScreenRenderer(ScheduledExecutorService scheduler, - long glfwWindow, - Theme theme, - String mcVersion, - String neoForgeVersion) { + long glfwWindow, + Theme theme, + String mcVersion, + String neoForgeVersion) { this.glfwWindow = glfwWindow; this.mcVersion = mcVersion; this.neoForgeVersion = neoForgeVersion; @@ -154,8 +153,7 @@ private RenderElement loadElement(String id, ThemeElement element) { Map.of( "version", mcVersion + "-" + neoForgeVersion.split("-")[0])); - default -> - throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); + default -> throw new IllegalStateException("Unexpected theme element " + element + " of type " + element.getClass()); }; renderElement.setId(id); return renderElement; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java index 3be57fea3..3d451d901 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/QuadHelper.java @@ -10,20 +10,19 @@ class QuadHelper { public static void fillSprite(SimpleBufferBuilder buffer, - Texture texture, - float x, - float y, - float z, - float width, - float height, - int color, - SpriteFillDirection fillDirection, - int animationFrame, - float srcU0, - float srcU1, - float srcV0, - float srcV1 - ) { + Texture texture, + float x, + float y, + float z, + float width, + float height, + int color, + SpriteFillDirection fillDirection, + int animationFrame, + float srcU0, + float srcU1, + float srcV0, + float srcV1) { // Too large values for width / height cause immediate crashes of the VM due to graphics driver bugs< // These maximum values are picked without too much thought. width = Math.min(65535, width); @@ -56,23 +55,22 @@ public static void fillSprite(SimpleBufferBuilder buffer, case TextureScaling.NineSlice nineSlice -> { addTiledNineSlice(buffer, x, y, z, width, height, color, nineSlice, u0, u1, v0, v1); } - default -> { - } + default -> {} } } private static void addTiledNineSlice(SimpleBufferBuilder buffer, - float x, - float y, - float z, - float width, - float height, - int color, - TextureScaling.NineSlice nineSlice, - float u0, - float u1, - float v0, - float v1) { + float x, + float y, + float z, + float width, + float height, + int color, + TextureScaling.NineSlice nineSlice, + float u0, + float u1, + float v0, + float v1) { var leftWidth = Math.min(nineSlice.left(), width / 2); var rightWidth = Math.min(nineSlice.right(), width / 2); var topHeight = Math.min(nineSlice.top(), height / 2); @@ -137,13 +135,13 @@ private static void addTiledNineSlice(SimpleBufferBuilder buffer, } private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float destTileWidth, - float destTileHeight, float u0, float u1, float v0, float v1) { + float destTileHeight, float u0, float u1, float v0, float v1) { fillTiled(buffer, x, y, z, width, height, color, destTileWidth, destTileHeight, u0, u1, v0, v1, SpriteFillDirection.TOP_TO_BOTTOM); } private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float destTileWidth, - float destTileHeight, float u0, float u1, float v0, float v1, SpriteFillDirection fillDirection) { + float destTileHeight, float u0, float u1, float v0, float v1, SpriteFillDirection fillDirection) { if (destTileWidth <= 0 || destTileHeight <= 0) { return; } @@ -183,7 +181,7 @@ private static void fillTiled(SimpleBufferBuilder buffer, float x, float y, floa } public static void addQuad(SimpleBufferBuilder buffer, float x, float y, float z, float width, float height, int color, float minU, float maxU, - float minV, float maxV) { + float minV, float maxV) { if (width < 0 || height < 0) { return; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java index 0dc314c34..9f53e1a50 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/RenderContext.java @@ -5,14 +5,13 @@ package net.neoforged.fml.earlydisplay.render; +import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; + +import java.util.List; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeColor; import net.neoforged.fml.earlydisplay.util.Bounds; -import java.util.List; - -import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; - public record RenderContext( SimpleBufferBuilder sharedBuffer, MaterializedTheme theme, @@ -42,16 +41,15 @@ public void blitTexture(Texture texture, float x, float y, float width, float he } public void blitTextureRegion(Texture texture, - float x, - float y, - float width, - float height, - int color, - float u0, - float u1, - float v0, - float v1 - ) { + float x, + float y, + float width, + float height, + int color, + float u0, + float u1, + float v0, + float v1) { GlState.activeTexture(GL_TEXTURE0); GlState.bindTexture2D(texture.textureId()); @@ -74,8 +72,7 @@ public void blitTextureRegion(Texture texture, u0, u1, v0, - v1 - ); + v1); sharedBuffer.draw(); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java index 0f0c26162..50dd5e198 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java @@ -7,7 +7,6 @@ import static org.lwjgl.opengl.GL11C.GL_NEAREST; import static org.lwjgl.opengl.GL32C.GL_CLAMP_TO_EDGE; -import static org.lwjgl.opengl.GL32C.GL_LINEAR; import static org.lwjgl.opengl.GL32C.GL_RED; import static org.lwjgl.opengl.GL32C.GL_TEXTURE0; import static org.lwjgl.opengl.GL32C.GL_TEXTURE_2D; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java index 80a7dbc6a..e1a34e7ba 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -5,13 +5,6 @@ package net.neoforged.fml.earlydisplay.render; -import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; -import net.neoforged.fml.earlydisplay.theme.TextureScaling; -import net.neoforged.fml.earlydisplay.theme.ThemeTexture; -import net.neoforged.fml.earlydisplay.theme.UncompressedImage; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.opengl.GL32C; - import static org.lwjgl.opengl.GL11C.GL_LINEAR; import static org.lwjgl.opengl.GL11C.GL_NEAREST; import static org.lwjgl.opengl.GL11C.GL_RGBA; @@ -27,9 +20,16 @@ import static org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE; import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; +import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; +import net.neoforged.fml.earlydisplay.theme.TextureScaling; +import net.neoforged.fml.earlydisplay.theme.ThemeTexture; +import net.neoforged.fml.earlydisplay.theme.UncompressedImage; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL32C; + public record Texture(int textureId, int physicalWidth, int physicalHeight, - TextureScaling scaling, - @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { + TextureScaling scaling, + @Nullable AnimationMetadata animationMetadata) implements AutoCloseable { public int width() { return scaling.width(); } @@ -51,8 +51,7 @@ public static Texture create( UncompressedImage image, String debugName, TextureScaling scaling, - @Nullable AnimationMetadata animation - ) { + @Nullable AnimationMetadata animation) { var texId = glGenTextures(); GlState.activeTexture(GL_TEXTURE0); GlState.bindTexture2D(texId); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java index a632cc02f..67f063952 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/RenderElement.java @@ -129,8 +129,7 @@ public static int clamp(int num, int min, int max) { } @Override - public void close() { - } + public void close() {} @Override public String toString() { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java index 9c196eb6e..aecb01c35 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -5,7 +5,6 @@ package net.neoforged.fml.earlydisplay.theme; -import org.jetbrains.annotations.Nullable; import org.lwjgl.stb.STBImage; import org.lwjgl.system.MemoryUtil; import org.slf4j.Logger; @@ -60,6 +59,7 @@ static ImageLoadResult tryLoadImage(ThemeResource resource) { public sealed interface ImageLoadResult { record Success(UncompressedImage image) implements ImageLoadResult {} + record Error(Exception exception) implements ImageLoadResult {} } @@ -86,6 +86,5 @@ private static UncompressedImage createBrokenImage() { BROKEN_TEXTURE_DIMENSIONS); } - private ImageLoader() { - } + private ImageLoader() {} } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index a9bf2acd9..f7c0df2c3 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -5,6 +5,7 @@ package net.neoforged.fml.earlydisplay.theme; +import java.util.Map; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; @@ -12,8 +13,6 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; import net.neoforged.fml.earlydisplay.util.StyleLength; -import java.util.Map; - /** * Defines a theme for the early display screen. * @@ -36,7 +35,6 @@ public record Theme( public static final String SHADER_GUI = "gui"; public static final String SHADER_FONT = "font"; public static final String SHADER_COLOR = "color"; - public static Theme createDefaultTheme() { var sprites = new ThemeSprites( new ThemeTexture( @@ -109,8 +107,7 @@ FONT_DEFAULT, classpathResource("Monocraft.ttf")), Map.of( "squir", squir, "fox", fox, - "version", forgeVersion - ))); + "version", forgeVersion))); } private static ClasspathResource classpathResource(String name) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java index 83f6849d3..fcbb6ae70 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoadingScreen.java @@ -5,13 +5,12 @@ package net.neoforged.fml.earlydisplay.theme; +import java.util.Map; import net.neoforged.fml.earlydisplay.theme.elements.ThemeDecorativeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemePerformanceElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeProgressBarsElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeStartupLogElement; -import java.util.Map; - /** * Describes the themable properties of the loading screen. */ @@ -20,5 +19,4 @@ public record ThemeLoadingScreen( ThemeProgressBarsElement progressBars, ThemeStartupLogElement startupLog, ThemeMojangLogoElement mojangLogo, - Map decoration) { -} + Map decoration) {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java index 84659e06f..cc396249c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeMojangLogoElement.java @@ -7,5 +7,4 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; -public class ThemeMojangLogoElement extends ThemeElement { -} +public class ThemeMojangLogoElement extends ThemeElement {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java index b1bcd9a8f..c49848f87 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java @@ -5,9 +5,8 @@ package net.neoforged.fml.earlydisplay.theme; -import org.jetbrains.annotations.Nullable; - import java.io.IOException; +import org.jetbrains.annotations.Nullable; public sealed interface ThemeResource permits ClasspathResource, FileResource { /** @@ -31,7 +30,7 @@ default UncompressedImage loadAsImage() { */ @Nullable default UncompressedImage tryLoadAsImage() { - return switch(ImageLoader.tryLoadImage(this)) { + return switch (ImageLoader.tryLoadImage(this)) { case ImageLoader.ImageLoadResult.Error error -> null; case ImageLoader.ImageLoadResult.Success(UncompressedImage image) -> image; }; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java index 2510174f4..35289a993 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeDecorativeElement.java @@ -9,5 +9,4 @@ * Decorative elements are additional elements that a theme can add to the screen that * have no specific functionality. */ -public abstract class ThemeDecorativeElement extends ThemeElement { -} +public abstract class ThemeDecorativeElement extends ThemeElement {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java index e54f316d4..99a444f81 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeElement.java @@ -5,11 +5,10 @@ package net.neoforged.fml.earlydisplay.theme.elements; +import java.util.Objects; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.util.StyleLength; -import java.util.Objects; - public abstract class ThemeElement { private boolean visible = true; private boolean maintainAspectRatio = true; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java index eee6e9ccb..bf27cbb24 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemePerformanceElement.java @@ -5,5 +5,4 @@ package net.neoforged.fml.earlydisplay.theme.elements; -public class ThemePerformanceElement extends ThemeElement { -} +public class ThemePerformanceElement extends ThemeElement {} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java index 726bb4b07..022629515 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/elements/ThemeStartupLogElement.java @@ -5,5 +5,4 @@ package net.neoforged.fml.earlydisplay.theme.elements; -public class ThemeStartupLogElement extends ThemeElement { -} +public class ThemeStartupLogElement extends ThemeElement {} diff --git a/tests/src/test/java/TestEarlyDisplay.java b/tests/src/test/java/TestEarlyDisplay.java index 4a090768d..545de95c2 100644 --- a/tests/src/test/java/TestEarlyDisplay.java +++ b/tests/src/test/java/TestEarlyDisplay.java @@ -3,12 +3,11 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -import net.neoforged.fml.earlydisplay.DisplayWindow; -import net.neoforged.fml.loading.FMLPaths; - import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import net.neoforged.fml.earlydisplay.DisplayWindow; +import net.neoforged.fml.loading.FMLPaths; public class TestEarlyDisplay { public static void main(String[] args) throws Exception { From 4bcf12a4fc9f365cbfb6a5f2dd40e3c35e47867a Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 4 May 2025 11:07:46 +0200 Subject: [PATCH 15/22] Fix Gradle trying to run tests --- tests/build.gradle | 9 ++++++++- .../src/{test => moddevTest}/java/TestEarlyDisplay.java | 0 2 files changed, 8 insertions(+), 1 deletion(-) rename tests/src/{test => moddevTest}/java/TestEarlyDisplay.java (100%) diff --git a/tests/build.gradle b/tests/build.gradle index e47dcaebc..2b2c4d0c8 100644 --- a/tests/build.gradle +++ b/tests/build.gradle @@ -2,23 +2,30 @@ plugins { id 'net.neoforged.moddev' } +sourceSets { + moddevTest +} + neoForge { enable { version = test_neoforge_version - enabledSourceSets = [sourceSets.test] + enabledSourceSets = [sourceSets.moddevTest] } runs { client { client() gameDirectory = file("build/runs/client") + sourceSet = sourceSets.moddevTest } server { server() gameDirectory = file("build/runs/server") + sourceSet = sourceSets.moddevTest } clientData { clientData() gameDirectory = file("build/runs/data") + sourceSet = sourceSets.moddevTest } } } diff --git a/tests/src/test/java/TestEarlyDisplay.java b/tests/src/moddevTest/java/TestEarlyDisplay.java similarity index 100% rename from tests/src/test/java/TestEarlyDisplay.java rename to tests/src/moddevTest/java/TestEarlyDisplay.java From ac7359aed06a96c8c288ff41da534c177c0698a7 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 4 May 2025 22:03:26 +0200 Subject: [PATCH 16/22] Formatting --- .../fml/earlydisplay/DisplayWindow.java | 17 ++- .../fml/earlydisplay/theme/ThemeIds.java | 19 +++ ...{ThemeSerializer.java => ThemeLoader.java} | 54 ++++++--- .../fml/earlydisplay/ExportThemes.java | 4 +- .../earlydisplay/theme/ThemeLoaderTest.java | 112 ++++++++++++++++++ .../theme/ThemeSerializerTest.java | 53 --------- 6 files changed, 186 insertions(+), 73 deletions(-) create mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java rename earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/{ThemeSerializer.java => ThemeLoader.java} (92%) create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java delete mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 897684746..48cdd486d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -53,6 +53,8 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.Month; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -71,7 +73,8 @@ import net.neoforged.fml.earlydisplay.render.SimpleFont; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeColor; -import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; +import net.neoforged.fml.earlydisplay.theme.ThemeIds; +import net.neoforged.fml.earlydisplay.theme.ThemeLoader; import net.neoforged.fml.loading.FMLConfig; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.fml.loading.progress.ProgressMeter; @@ -199,13 +202,19 @@ public Runnable initialize(String[] arguments) { private static Theme loadTheme(boolean darkMode) { Path themePath = getThemePath(); - var themeId = darkMode ? "darkmode" : "default"; + var themeId = darkMode ? ThemeIds.DARK_MODE : ThemeIds.DEFAULT; + + // Specials + var today = LocalDate.now(); + if (today.getMonth() == Month.APRIL && today.getDayOfMonth() == 1) { + themeId = ThemeIds.APRIL_FOOLS; + } Theme theme; try { - theme = ThemeSerializer.load(themePath, themeId); + theme = ThemeLoader.load(themePath, themeId); } catch (Exception e) { - LOGGER.error("Failed to load theme {}", themePath, e); + LOGGER.error("Failed to load theme {} from {}", themeId, themePath, e); theme = Theme.createDefaultTheme(); } return theme; diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java new file mode 100644 index 000000000..5abd83d64 --- /dev/null +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.theme; + +/** + * Ids of themes that the early loading screen references. + * + * @see ThemeLoader#load + */ +public final class ThemeIds { + public static final String DEFAULT = "default"; + public static final String DARK_MODE = "darkmode"; + public static final String APRIL_FOOLS = "april-fools"; + + private ThemeIds() {} +} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java similarity index 92% rename from earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java rename to earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java index 148b4e5c2..38a5b4ede 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java @@ -44,11 +44,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class ThemeSerializer { - private static final Logger LOG = LoggerFactory.getLogger(ThemeSerializer.class); +public final class ThemeLoader { + private static final Logger LOG = LoggerFactory.getLogger(ThemeLoader.class); private static final int VERSION = 1; + private static final String BUILTIN_PREFIX = "builtin:"; - private ThemeSerializer() {} + private ThemeLoader() {} public static Theme load(Path baseDirectory, String id) throws IOException { var sources = new LinkedHashSet(); @@ -62,21 +63,35 @@ public static Theme load(Path baseDirectory, String id) throws IOException { } private static JsonObject readThemeTree(Path baseDirectory, String id, Set sources) throws IOException { - if (!sources.add(id)) { - throw new IllegalStateException("Detected recursion in theme extends clause: " + sources + " -> " + id); + if (id.startsWith(BUILTIN_PREFIX)) { + id = id.substring(BUILTIN_PREFIX.length()); + return readBuiltinThemeTree(baseDirectory, id, sources); } String filename = getThemeFilename(id); - Path themePath = baseDirectory.resolve(filename); - try (var in = Files.newInputStream(themePath)) { - LOG.debug("Loading theme from {}", themePath); - return readThemeTree(baseDirectory, in, sources); - } catch (NoSuchFileException ignored) {} + try (var in = openIfExists(themePath)) { + if (in != null) { + if (!sources.add(id)) { + throw new IllegalStateException("Detected recursion in theme extends clause: " + sources + " -> " + id); + } + + LOG.debug("Loading theme from {}", themePath); + return readThemeTree(baseDirectory, in, sources); + } + } + + return readBuiltinThemeTree(baseDirectory, id, sources); + } + + private static JsonObject readBuiltinThemeTree(Path baseDirectory, String id, Set sources) throws IOException { + if (!sources.add(BUILTIN_PREFIX + id)) { + throw new IllegalStateException("Detected recursion in theme extends clause: " + sources + " -> " + BUILTIN_PREFIX + id); + } // Try to load it from the classpath instead - String classpathLocation = "/net/neoforged/fml/earlydisplay/theme/" + filename; - try (var in = ThemeSerializer.class.getResourceAsStream(classpathLocation)) { + String classpathLocation = "/net/neoforged/fml/earlydisplay/theme/" + getThemeFilename(id); + try (var in = ThemeLoader.class.getResourceAsStream(classpathLocation)) { LOG.debug("Loading built-in theme {}", id); if (in == null) { throw new NoSuchFileException("Failed to find embedded theme resource " + classpathLocation); @@ -85,7 +100,18 @@ private static JsonObject readThemeTree(Path baseDirectory, String id, Set sources) throws IOException { + @Nullable + private static InputStream openIfExists(Path themePath) throws IOException { + try { + return Files.newInputStream(themePath); + } catch (NoSuchFileException ignored) { + return null; + } + } + + private static JsonObject readThemeTree(Path baseDirectory, + InputStream in, + Set sources) throws IOException { var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); var themeRoot = createGson(baseDirectory, false).fromJson(reader, JsonObject.class); @@ -106,7 +132,7 @@ private static JsonObject readThemeTree(Path baseDirectory, InputStream in, Set< private static JsonObject mergeThemeRoot(JsonObject baseThemeRoot, JsonObject themeRoot) { return mergeObject(baseThemeRoot, themeRoot, (property, baseValue, value) -> switch (property) { case "fonts", "shaders", "colorScheme", "sprites" -> mergeObject(baseValue, value); - case "loadingScreen" -> mergeObject(baseValue, value, ThemeSerializer::mergeLoadingScreenProperty); + case "loadingScreen" -> mergeObject(baseValue, value, ThemeLoader::mergeLoadingScreenProperty); default -> value; }); } diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java index c2590c378..ecc644ea3 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java @@ -6,7 +6,7 @@ package net.neoforged.fml.earlydisplay; import net.neoforged.fml.earlydisplay.theme.Theme; -import net.neoforged.fml.earlydisplay.theme.ThemeSerializer; +import net.neoforged.fml.earlydisplay.theme.ThemeLoader; public class ExportThemes { public static void main(String[] args) throws Exception { @@ -14,6 +14,6 @@ public static void main(String[] args) throws Exception { var defaultTheme = Theme.createDefaultTheme(); var builtInThemePath = projectRoot.resolve("src/main/resources/net/neoforged/fml/earlydisplay/theme"); - ThemeSerializer.save(builtInThemePath.resolve("theme-default.json"), defaultTheme, false); + ThemeLoader.save(builtInThemePath.resolve("theme-default.json"), defaultTheme, false); } } diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java new file mode 100644 index 000000000..80f2d21cf --- /dev/null +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.theme; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.Gson; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ThemeLoaderTest { + @TempDir + Path tempDir; + + @Test + void testDefaultThemeRoundtrip() throws IOException { + var defaultTheme = Theme.createDefaultTheme(); + Path themePath = tempDir.resolve("theme-default.json"); + ThemeLoader.save(themePath, defaultTheme, false); + + var loadedTheme = ThemeLoader.load(tempDir, "default"); + assertThat(loadedTheme) + .usingComparatorForType(RESOURCE_COMPARATOR, ThemeResource.class) + .usingRecursiveComparison() + .isEqualTo(defaultTheme); + } + + @Test + void testExtendingBuiltInTheme() throws Exception { + // Write our theme that just overrides the background color + var overriddenTheme = Map.of( + "version", 1, + "extends", "builtin:default", + "colorScheme", Map.of( + "screenBackground", "#000000")); + + try (var writer = Files.newBufferedWriter(tempDir.resolve("theme-default.json"))) { + new Gson().toJson(overriddenTheme, writer); + } + + var defaultTheme = Theme.createDefaultTheme(); + var loadedTheme = ThemeLoader.load(tempDir, ThemeIds.DEFAULT); + assertThat(loadedTheme) + .usingComparatorForType(RESOURCE_COMPARATOR, ThemeResource.class) + .usingRecursiveComparison() + .ignoringFields("colorScheme.screenBackground") + .isEqualTo(defaultTheme); + assertEquals(ThemeColor.ofBytes(0, 0, 0), loadedTheme.colorScheme().screenBackground()); + } + + @Test + void testOverridingBuiltInTheme() throws Exception { + // Write our theme that just overrides the background color + var overriddenTheme = Map.of( + "version", 1, + "colorScheme", Map.of( + "screenBackground", "#000000")); + + try (var writer = Files.newBufferedWriter(tempDir.resolve("theme-default.json"))) { + new Gson().toJson(overriddenTheme, writer); + } + + var loadedTheme = ThemeLoader.load(tempDir, ThemeIds.DEFAULT); + assertThat(loadedTheme) + .usingComparatorForType(RESOURCE_COMPARATOR, ThemeResource.class) + .usingRecursiveComparison() + .ignoringFields("colorScheme.screenBackground") + .isEqualTo(new Theme(null, null, null, new ThemeColorScheme( + ThemeColor.ofBytes(0, 0, 0), + null, + null, + null), null, null)); + } + + private static final Comparator RESOURCE_COMPARATOR = Comparator.comparing( + resource -> { + if (resource == null) { + return null; + } + try { + return resource.toNativeBuffer().toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, + (o1, o2) -> { + if (o1 == null) { + return o2 == null ? 0 : -1; + } else if (o2 == null) { + return 1; + } + if (o1.length != o2.length) { + return Integer.compareUnsigned(o1.length, o2.length); + } + for (int i = 0; i < o1.length; i++) { + if (o1[i] != o2[i]) { + return Integer.compareUnsigned(o1[i], o2[i]); + } + } + return 0; + }); +} diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java deleted file mode 100644 index 8b1b3ea3c..000000000 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeSerializerTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay.theme; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Path; -import java.util.Comparator; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class ThemeSerializerTest { - @TempDir - Path tempDir; - - @Test - void testDefaultThemeRoundtrip() throws IOException { - var defaultTheme = Theme.createDefaultTheme(); - Path themePath = tempDir.resolve("theme-default.json"); - ThemeSerializer.save(themePath, defaultTheme, false); - - var loadedTheme = ThemeSerializer.load(tempDir, "default"); - assertThat(loadedTheme) - .usingComparatorForType(RESOURCE_COMPARATOR, ThemeResource.class) - .usingRecursiveComparison() - .isEqualTo(defaultTheme); - } - - private static final Comparator RESOURCE_COMPARATOR = Comparator.comparing( - resource -> { - try { - return resource.toNativeBuffer().toByteArray(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }, - (o1, o2) -> { - if (o1.length != o2.length) { - return Integer.compareUnsigned(o1.length, o2.length); - } - for (int i = 0; i < o1.length; i++) { - if (o1[i] != o2[i]) { - return Integer.compareUnsigned(o1[i], o2[i]); - } - } - return 0; - }); -} From 2b3bbbfb3944d5cd7b73122a43016dc942334bba Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Thu, 8 May 2025 23:18:13 +0200 Subject: [PATCH 17/22] Docs --- earlydisplay/THEMING.md | 166 ++++++++++++++++++ .../render/LoadingScreenRenderer.java | 5 + 2 files changed, 171 insertions(+) create mode 100644 earlydisplay/THEMING.md diff --git a/earlydisplay/THEMING.md b/earlydisplay/THEMING.md new file mode 100644 index 000000000..da44cbadc --- /dev/null +++ b/earlydisplay/THEMING.md @@ -0,0 +1,166 @@ +# Early Loading Screen Themes + +This document describes how to customize the theme for the FML early loading screen. + +## Themes + +FML will try to load one of the following themes. + +| Theme Id | Description | Filename | +|---------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| +| `default` | Loaded if no other criteria are met. | [theme-default.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json) | +| `darkmode` | Loaded if the player configured dark-mode in options.txt or via environment variable. | [theme-darkmode.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-darkmode.json) | +| `april-fools` | Loaded on April 1st. Takes precedence over the other themes. | [theme-aprils-fools.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json) | + +## Customization + +FML will first try to load resources or themes from the `config/fml/` subfolder, relative to the game directory. + +If resources aren't found in that folder, it will load them from +the [resources bundled with FML](./src/main/resources/net/neoforged/fml/earlydisplay/theme/) instead. + +## Layout + +### Loading Screen + +The loading screen is laid out using a 854 by 480 layout pixel sized rectangle. This rectangle is centered to fill +the available screen size. + +## Theme Structure + +The theme JSON files have the following keys: + +| Property | Type | Description | +|-----------------|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `version` | number | Must be `1`. | +| `extends` | string | Allows you to base your theme on another existing theme. Most commonly your themes will use `builtin:default` as the base theme. | +| `windowIcon` | [resource](#resource) | Sets the icon of the operating system window while the game is loading. | +| `fonts` | dictionary of [resource](#resource) | Fonts. Must contain a font named `default`. | +| `shaders` | dictionary of [resource](#resource) | Shaders. Must contain a `gui` shader (for drawing textured rectangles), `font` shader for drawing text and `color` for drawing untextured rectangles. | +| `colorScheme` | [color scheme](#color-scheme) | The color scheme. | +| `sprites` | [sprites](#sprites) | Defines various sprites for use by widgets in the loading screen. | +| `loadingScreen` | [loading screen](#loading-screen) | Configures elements of the loading screen. | + +### resource + +References to resources are encoded as strings. To load a sprite from `test.png`, you'd specify `test.png` in your +theme. +As explained above, FML will first try to find `test.png` relative to your theme JSON, and if it can't find it there, +it'll try to use it from the bundled resources. This mechanism can also be used to simply override a bundled resource +by placing the override in the theme folder, without actually overriding the bundled theme itself. + +### color scheme + +The color type defines the following properties: + +| Property | Type | Description | +|--------------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `screenBackground` | [color](#color) | Used to fill the screen background. | +| `text` | [color](#color) | Default text color. | +| `memoryLowColor` | [color](#color) | The color to use for coloring the bar when resource usage is low. The actual color will be interpolated between this and `highColor`. | +| `memoryHighColor` | [color](#color) | The color to use for coloring the bar when resource usage is high. The actual color will be interpolated between this and `lowColor`. | + +### color + +Colors are specified in a HTML-like format: `#AARRGGBB` or `#RRGGBB`. + +### sprites + +The sprites type defines the following properties: + +| Property | Type | Description | +|-----------------------------------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `progressBarBackground` | [texture](#texture) | This sprite is drawn to fill the background of progress bars. | +| `progressBarForeground` | [texture](#texture) | The sprite to draw as the foreground of progress bars using coverage corresponding to the progress. | +| `progressBarIndeterminate` | [texture](#texture) | The sprite to draw as the foreground of indeterminate progress bars. | +| `progressBarIndeterminateBounces` | boolean | If true, an indeterminate progress bar will bounce back and forth within the bounds instead of disappearing to the right and reappearing on the left. | + +### texture + +The texture type defines the following properties: + +| Property | Type | Description | +|-------------|-------------------------------------------|------------------------------------------------------------------------------------------------| +| `resource` | [resource](#resource) | The bitmap used for this texture. Currently only png files are supported. | +| `scaling` | [texture scaling](#texture-scaling) | Defines properties about how the bitmap is drawn to the screen given some arbitrary rectangle. | +| `animation` | [animation metadata](#animation-metadata) | Optional. Used to set up an animated texture. | + +### texture scaling + +The texture scaling can be configured as one of three types using the `type` field. + +| Property | Type | Description | +|-----------------|-----------|-----------------------------------------------------------------------------------------------------| +| `type` | `stretch` | The bitmap will simply be stretched to fill the rectangle. | +| `width` | number | The preferred width of the bitmap in layout coordinates. | +| `height` | number | The preferred height of the bitmap in layout coordinates. | +| `linearScaling` | boolean | If true, the bitmap will be scaled using linear filtering, otherwise nearest neighbor will be used. | + +| Property | Type | Description | +|-----------------|---------|-----------------------------------------------------------------------------------------------------| +| `type` | `tile` | The bitmap will be tiled to fill the rectangle. | +| `width` | number | The preferred width of the bitmap in layout coordinates. | +| `height` | number | The preferred height of the bitmap in layout coordinates. | +| `linearScaling` | boolean | If true, the bitmap will be scaled using linear filtering, otherwise nearest neighbor will be used. | + +| Property | Type | Description | +|-------------------------|-------------|--------------------------------------------------------------------------------------------------------------------| +| `type` | `nineSlice` | The bitmap will is cut into nine slices to avoid stretching the border in the wrong directions. | +| `width` | number | The preferred width of the bitmap in layout coordinates. | +| `height` | number | The preferred height of the bitmap in layout coordinates. | +| `left` | number | The width of the left border in the bitmap in pixels. | +| `top` | number | The height of the top border in the bitmap in pixels. | +| `right` | number | The width of the right border in the bitmap in pixels. | +| `bottom` | number | The height of the bottom border in the bitmap in pixels. | +| `stretchHorizontalFill` | boolean | If true, areas of the bitmap that need to be horizontally fit to the rectangle will be stretched instead of tiled. | +| `stretchVerticalFill` | boolean | If true, areas of the bitmap that need to be vertically fit to the rectangle will be stretched instead of tiled. | +| `linearScaling` | boolean | If true, the bitmap will be scaled using linear filtering, otherwise nearest neighbor will be used. | + +### animation metadata + +| Property | Type | Description | +|--------------|--------|------------------------------------------------------------------------------------------------------------| +| `frameCount` | number | The number of frames in the bitmap. The frames are expected to be stacked vertically in the source bitmap. | + +### loading screen + +| Property | Type | Description | +|----------------|-------------------------------------------------|--------------------------------------------------------------| +| `performance` | [element](#element) | Configures the memory/cpu usage indicators. | +| `progressBars` | [progress bars element](#progress-bars-element) | Configures the current loading progress bars. | +| `startupLog` | [element](#element) | Configures how the most recent log messages are shown. | +| `mojangLogo` | [element](#element) | Configures the large Mojang logo. | +| `decoration` | dictionary of [element](#element) | Allows custom decorative elements to be added to the screen. | + +### progress bars element + +Inherits all properties of [element](#element), and adds the following: + +| Property | Type | Description | +|------------|--------|--------------------------------------------------------------------| +| `labelGap` | number | The gap in virtual pixels between a bars label and the bar itself. | +| `barGap` | number | The gap in virtual pixels between a bar and the next label or bar. | + +### element + +| Property | Type | Description | +|-----------------------|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `visible` | boolean | Defaults to true. Can be used to hide pre-defined elements. | +| `maintainAspectRatio` | boolean | Defaults to true. If only one dimension of the element is fully defined, the other dimension will scaled from the implicit element size while maintaining its original aspect ratio. | +| `left` | [style length](#style-length) | Defaults to undefined. Positions the left edge of the element relative to the layout box. | +| `top` | [style length](#style-length) | Defaults to undefined. Positions the top edge of the element relative to the layout box. | +| `right` | [style length](#style-length) | Defaults to undefined. Positions the right edge of the element relative to the layout box. | +| `bottom` | [style length](#style-length) | Defaults to undefined. Positions the bottom edge of the element relative to the layout box. | +| `centerHorizontally` | boolean | Defaults to false. If true, the element will be centered horizontally and then offset from that using its `left` position. | +| `centerVertically` | boolean | Defaults to false. If true, the element will be centered vertically and then offset from that using its `top` position. | +| `font` | string | Defaults to `default`. Can be used to override the font used by an element. | + +### style length + +Lengths of elements can be specified as follows: + +- `null`, which can be used to override a value set in a base theme to undefined. +- A simple numeric value, which is interpreted as layout pixels. +- A string containing a number followed by the `rem` suffix to indicate a size relative to the font size. +- A string containing a number followed by `%` to indicate a value relative to the available width (or height, depending + on context). diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index ca66e7b44..f57851b7a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -40,6 +40,7 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; +import net.neoforged.fml.earlydisplay.util.Bounds; import org.lwjgl.opengl.GL32C; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -218,6 +219,10 @@ public void renderToFramebuffer() { var backup = GlState.createSnapshot(); framebuffer.activate(); + + // Fit the layout rectangle into the screen while maintaining aspect ratio + var b = new Bounds(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT); + GlState.viewport(0, 0, framebuffer.width(), framebuffer.height()); // Clear the screen to our color From 7071c234a496dc00a0f7e3c18eda0438d3bb5432 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Thu, 8 May 2025 23:38:40 +0200 Subject: [PATCH 18/22] Move squirrel to april fools theme. --- .../fml/earlydisplay/theme/ImageLoader.java | 30 +++++++++++++++++-- .../fml/earlydisplay/theme/Theme.java | 4 --- .../earlydisplay/theme/theme-april-fools.json | 24 +++++++++++++++ .../fml/earlydisplay/theme/theme-default.json | 17 ----------- .../src/moddevTest/java/TestEarlyDisplay.java | 2 +- 5 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java index aecb01c35..131c18a58 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -10,6 +10,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + /** * Represents an image referenced from a theme. *

@@ -42,6 +46,8 @@ static UncompressedImage loadImage(ThemeResource resource) { */ static ImageLoadResult tryLoadImage(ThemeResource resource) { try (var buffer = resource.toNativeBuffer()) { + validateHeader(buffer.buffer().slice()); + var width = new int[1]; var height = new int[1]; var channels = new int[1]; @@ -57,10 +63,27 @@ static ImageLoadResult tryLoadImage(ThemeResource resource) { } } + // Taken from Mojangs code to add validation that STB doesnt seem to have. + private static void validateHeader(ByteBuffer buffer) throws IOException { + ByteOrder byteorder = buffer.order(); + buffer.order(ByteOrder.BIG_ENDIAN); + if (buffer.getLong(0) != 0x89504e470d0a1a0aL) { + throw new IOException("Bad PNG Signature"); + } else if (buffer.getInt(8) != 13) { + throw new IOException("Bad length for IHDR chunk!"); + } else if (buffer.getInt(12) != 0x49484452) { + throw new IOException("Bad type for IHDR chunk!"); + } else { + buffer.order(byteorder); + } + } + public sealed interface ImageLoadResult { - record Success(UncompressedImage image) implements ImageLoadResult {} + record Success(UncompressedImage image) implements ImageLoadResult { + } - record Error(Exception exception) implements ImageLoadResult {} + record Error(Exception exception) implements ImageLoadResult { + } } private static UncompressedImage createBrokenImage() { @@ -86,5 +109,6 @@ private static UncompressedImage createBrokenImage() { BROKEN_TEXTURE_DIMENSIONS); } - private ImageLoader() {} + private ImageLoader() { + } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index f7c0df2c3..2e4cf6f30 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -48,9 +48,6 @@ public static Theme createDefaultTheme() { new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true, false)), false); - var squir = new ThemeImageElement(); - squir.setTexture(new ThemeTexture(classpathResource("squirrel.png"), new TextureScaling.Stretch(112, 112, true))); - var startupLog = new ThemeStartupLogElement(); startupLog.setLeft(StyleLength.ofPoints(10)); startupLog.setBottom(StyleLength.ofPoints(10)); @@ -105,7 +102,6 @@ FONT_DEFAULT, classpathResource("Monocraft.ttf")), startupLog, mojangLogo, Map.of( - "squir", squir, "fox", fox, "version", forgeVersion))); } diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json new file mode 100644 index 000000000..82fb40b6d --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json @@ -0,0 +1,24 @@ +{ + "extends": "builtin:default", + "loadingScreen": { + "decoration": { + "squir": { + "type": "image", + "texture": { + "resource": "squirrel.png", + "scaling": { + "type": "stretch", + "width": 112, + "height": 112, + "linearScaling": true + } + }, + "visible": true, + "maintainAspectRatio": true, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" + } + } + } +} \ No newline at end of file diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json index 08a57b358..7bccf2c6b 100644 --- a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json @@ -145,23 +145,6 @@ "centerHorizontally": false, "centerVertically": false, "font": "default" - }, - "squir": { - "type": "image", - "texture": { - "resource": "classpath:net/neoforged/fml/earlydisplay/theme/squirrel.png", - "scaling": { - "type": "stretch", - "width": 112, - "height": 112, - "linearScaling": true - } - }, - "visible": true, - "maintainAspectRatio": true, - "centerHorizontally": false, - "centerVertically": false, - "font": "default" } } } diff --git a/tests/src/moddevTest/java/TestEarlyDisplay.java b/tests/src/moddevTest/java/TestEarlyDisplay.java index 545de95c2..0ab5e794d 100644 --- a/tests/src/moddevTest/java/TestEarlyDisplay.java +++ b/tests/src/moddevTest/java/TestEarlyDisplay.java @@ -12,7 +12,7 @@ public class TestEarlyDisplay { public static void main(String[] args) throws Exception { System.setProperty("java.awt.headless", "true"); - System.setProperty("fml.earlyWindowDarkMode", "true"); + //System.setProperty("fml.earlyWindowDarkMode", "true"); FMLPaths.loadAbsolutePaths(findProjectRoot()); From ce911c483f0862ebc017926bcc6ead4876895eeb Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sat, 10 May 2025 14:45:05 +0200 Subject: [PATCH 19/22] Formatting --- earlydisplay/THEMING.md | 10 ++-- .../render/LoadingScreenRenderer.java | 3 +- .../render/elements/MojangLogoElement.java | 24 +++++--- .../earlydisplay/theme/ClasspathResource.java | 48 --------------- .../fml/earlydisplay/theme/FileResource.java | 31 ---------- .../fml/earlydisplay/theme/ImageLoader.java | 48 +++++++++------ .../fml/earlydisplay/theme/NativeBuffer.java | 59 +++++++++++++++++++ .../fml/earlydisplay/theme/Theme.java | 16 ++--- .../fml/earlydisplay/theme/ThemeLoader.java | 43 ++++---------- .../fml/earlydisplay/theme/ThemeResource.java | 31 ++++++---- .../fml/earlydisplay/theme/ThemeShader.java | 12 ++-- .../fml/earlydisplay/{ => theme}/gui.frag | 0 .../fml/earlydisplay/{ => theme}/gui.vert | 0 .../earlydisplay/{ => theme}/gui_color.frag | 0 .../earlydisplay/{ => theme}/gui_font.frag | 0 .../fml/earlydisplay/theme/theme-default.json | 24 ++++---- ...ortThemes.java => ExportDefaultTheme.java} | 2 +- .../earlydisplay/render/SimpleFontTest.java | 4 +- 18 files changed, 172 insertions(+), 183 deletions(-) delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java delete mode 100644 earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java rename earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/{ => theme}/gui.frag (100%) rename earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/{ => theme}/gui.vert (100%) rename earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/{ => theme}/gui_color.frag (100%) rename earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/{ => theme}/gui_font.frag (100%) rename earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/{ExportThemes.java => ExportDefaultTheme.java} (94%) diff --git a/earlydisplay/THEMING.md b/earlydisplay/THEMING.md index da44cbadc..de566afba 100644 --- a/earlydisplay/THEMING.md +++ b/earlydisplay/THEMING.md @@ -6,11 +6,11 @@ This document describes how to customize the theme for the FML early loading scr FML will try to load one of the following themes. -| Theme Id | Description | Filename | -|---------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| -| `default` | Loaded if no other criteria are met. | [theme-default.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json) | -| `darkmode` | Loaded if the player configured dark-mode in options.txt or via environment variable. | [theme-darkmode.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-darkmode.json) | -| `april-fools` | Loaded on April 1st. Takes precedence over the other themes. | [theme-aprils-fools.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json) | +| Theme Id | Description | Filename | +|---------------|---------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| +| `default` | Loaded if no other criteria are met. | [theme-default.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json) | +| `darkmode` | Loaded if the player configured dark-mode in options.txt or via environment variable. | [theme-darkmode.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-darkmode.json) | +| `april-fools` | Loaded on April 1st. Takes precedence over the other themes. | [theme-april-fools.json](./src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json) | ## Customization diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index f57851b7a..fd948f55d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -18,6 +18,7 @@ import static org.lwjgl.opengl.GL11C.GL_SRC_ALPHA; import static org.lwjgl.opengl.GL11C.GL_VENDOR; import static org.lwjgl.opengl.GL11C.GL_VERSION; +import static org.lwjgl.opengl.GL11C.GL_ZERO; import static org.lwjgl.opengl.GL11C.glClear; import static org.lwjgl.opengl.GL11C.glGetString; @@ -230,7 +231,7 @@ public void renderToFramebuffer() { GlState.clearColor(background.r(), background.g(), background.b(), 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); GlState.enableBlend(true); - GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + GlState.blendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE); for (var shader : theme.shaders().values()) { shader.activate(); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java index 63c6837ca..b5083f41b 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java @@ -5,12 +5,15 @@ package net.neoforged.fml.earlydisplay.render.elements; +import java.io.IOException; import net.neoforged.fml.earlydisplay.render.MaterializedTheme; import net.neoforged.fml.earlydisplay.render.RenderContext; import net.neoforged.fml.earlydisplay.render.Texture; -import net.neoforged.fml.earlydisplay.theme.ClasspathResource; +import net.neoforged.fml.earlydisplay.theme.ImageLoader; +import net.neoforged.fml.earlydisplay.theme.NativeBuffer; import net.neoforged.fml.earlydisplay.theme.TextureScaling; import net.neoforged.fml.earlydisplay.theme.ThemeMojangLogoElement; +import net.neoforged.fml.earlydisplay.theme.UncompressedImage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,13 +34,16 @@ public MojangLogoElement(ThemeMojangLogoElement element, MaterializedTheme theme Texture mojangLogo = null; for (var logoPath : LOGO_PATHS) { - try (var image = new ClasspathResource(logoPath).tryLoadAsImage()) { - if (image == null) { - LOGGER.debug("Failed to load Mojang logo from {}", logoPath); - continue; + try (var buffer = NativeBuffer.loadFromClasspath(logoPath)) { + var loadResult = ImageLoader.tryLoadImage("mojang logo", null, buffer); + if (loadResult instanceof ImageLoader.Result.Error(Exception exception)) { + LOGGER.debug("Failed to load Mojang logo from {}: {}", logoPath, exception); + } else if (loadResult instanceof ImageLoader.Result.Success(UncompressedImage image)) { + mojangLogo = Texture.create(image, "mojang logo", new TextureScaling.Stretch(512, 128, true), null); + break; } - - mojangLogo = Texture.create(image, "mojang logo", new TextureScaling.Stretch(512, 128, true), null); + } catch (IOException e) { + LOGGER.debug("Failed to load Mojang logo from {}", logoPath); } } this.mojangLogo = mojangLogo; @@ -45,6 +51,10 @@ public MojangLogoElement(ThemeMojangLogoElement element, MaterializedTheme theme @Override public void render(RenderContext context) { + if (this.mojangLogo == null) { + return; + } + var bounds = resolveBounds(context.availableWidth(), context.availableHeight(), 512, 128); float x0 = bounds.left(); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java deleted file mode 100644 index 32f41b896..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ClasspathResource.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay.theme; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.lwjgl.system.MemoryUtil; - -/** - * A resource loaded from the loading screens classloader. - * - * @param path Path to a file. This is expected to be an absolute path that doesn't start with a {@code /}. - */ -public record ClasspathResource(String path) implements ThemeResource { - public NativeBuffer toNativeBuffer() throws IOException { - var resource = getClass().getClassLoader().getResource(path); - if (resource == null) { - throw new FileNotFoundException("Couldn't find classpath resource " + path); - } - - var connection = resource.openConnection(); - try (var in = connection.getInputStream()) { - var contentLengthHint = connection.getContentLength(); - if (contentLengthHint == -1) { - contentLengthHint = 8 * 1024; - } - - ByteBuffer buffer = MemoryUtil.memAlloc(contentLengthHint); - byte[] tmp = new byte[8 * 1024]; - int read; - - while ((read = in.read(tmp)) != -1) { - if (buffer.remaining() < read) { - buffer = MemoryUtil.memRealloc(buffer, buffer.capacity() * 2); - } - buffer.put(tmp, 0, read); - } - - buffer.flip(); - - return new NativeBuffer(buffer, MemoryUtil::memFree); - } - } -} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java deleted file mode 100644 index 0ada53d84..000000000 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/FileResource.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.earlydisplay.theme; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -public record FileResource(File file) implements ThemeResource { - public NativeBuffer toNativeBuffer() throws IOException { - try (var fis = new FileInputStream(file)) { - var channel = fis.getChannel(); - - long size = channel.size(); - if (size > MAX_SIZE) { - throw new IOException("The resource " + this + " exceeds the maximum size of " + MAX_SIZE); - } - - // Allocate a ByteBuffer with the file size - var buffer = ByteBuffer.allocateDirect((int) size).order(ByteOrder.nativeOrder()); - channel.read(buffer); - buffer.flip(); - return new NativeBuffer(buffer, ignored -> {}); - } - } -} diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java index 131c18a58..f87fd2f9f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -5,21 +5,21 @@ package net.neoforged.fml.earlydisplay.theme; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.jetbrains.annotations.Nullable; import org.lwjgl.stb.STBImage; import org.lwjgl.system.MemoryUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - /** * Represents an image referenced from a theme. *

* Theme images will refer to a resource their content is loaded from, this is expected to be a PNG image. */ -final class ImageLoader { +public final class ImageLoader { private static final String BROKEN_TEXTURE_NAME = "broken texture"; private static final int BROKEN_TEXTURE_DIMENSIONS = 16; @@ -30,10 +30,10 @@ final class ImageLoader { * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ - static UncompressedImage loadImage(ThemeResource resource) { + public static UncompressedImage loadImage(ThemeResource resource) { return switch (tryLoadImage(resource)) { - case ImageLoadResult.Success(UncompressedImage image) -> image; - case ImageLoadResult.Error(Exception exception) -> { + case Result.Success(UncompressedImage image) -> image; + case Result.Error(Exception exception) -> { LOGGER.error("Failed to load theme image {}", resource, exception); yield createBrokenImage(); } @@ -44,8 +44,20 @@ static UncompressedImage loadImage(ThemeResource resource) { * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ - static ImageLoadResult tryLoadImage(ThemeResource resource) { + public static Result tryLoadImage(ThemeResource resource) { try (var buffer = resource.toNativeBuffer()) { + return tryLoadImage(resource.toString(), resource, buffer); + } catch (Exception e) { + return new Result.Error(e); + } + } + + /** + * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. + * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. + */ + public static Result tryLoadImage(String debugName, @Nullable ThemeResource source, NativeBuffer buffer) { + try { validateHeader(buffer.buffer().slice()); var width = new int[1]; @@ -53,13 +65,14 @@ static ImageLoadResult tryLoadImage(ThemeResource resource) { var channels = new int[1]; var decodedImage = STBImage.stbi_load_from_memory(buffer.buffer(), width, height, channels, 4); // TODO: Handle image decoding error - return new ImageLoadResult.Success(new UncompressedImage(resource.toString(), - resource, + return new Result.Success(new UncompressedImage( + debugName, + source, new NativeBuffer(decodedImage, STBImage::stbi_image_free), width[0], height[0])); } catch (Exception e) { - return new ImageLoadResult.Error(e); + return new Result.Error(e); } } @@ -78,12 +91,10 @@ private static void validateHeader(ByteBuffer buffer) throws IOException { } } - public sealed interface ImageLoadResult { - record Success(UncompressedImage image) implements ImageLoadResult { - } + public sealed interface Result { + record Success(UncompressedImage image) implements Result {} - record Error(Exception exception) implements ImageLoadResult { - } + record Error(Exception exception) implements Result {} } private static UncompressedImage createBrokenImage() { @@ -109,6 +120,5 @@ private static UncompressedImage createBrokenImage() { BROKEN_TEXTURE_DIMENSIONS); } - private ImageLoader() { - } + private ImageLoader() {} } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java index e09454a5d..a5a5636e5 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java @@ -5,11 +5,22 @@ package net.neoforged.fml.earlydisplay.theme; +import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import org.lwjgl.system.MemoryUtil; public final class NativeBuffer implements AutoCloseable { + /** + * Sanity check to stop going OOM instead of showing the loading screen. + */ + private static final int MAX_SIZE = 100_000_000; + private final ByteBuffer buffer; private final Consumer deallocator; private final AtomicBoolean deallocated = new AtomicBoolean(); @@ -19,6 +30,54 @@ public NativeBuffer(ByteBuffer buffer, Consumer deallocator) { this.deallocator = deallocator; } + public static NativeBuffer loadFromPath(Path path) throws IOException { + try (var channel = Files.newByteChannel(path)) { + long size = channel.size(); + if (size > MAX_SIZE) { + throw new IOException("The resource " + path + " exceeds the maximum size of " + MAX_SIZE); + } + + // Allocate a ByteBuffer with the file size + var buffer = ByteBuffer.allocateDirect((int) size).order(ByteOrder.nativeOrder()); + channel.read(buffer); + buffer.flip(); + return new NativeBuffer(buffer, ignored -> {}); + } + } + + /** + * @throws NoSuchFileException If the resource does not exist. + */ + public static NativeBuffer loadFromClasspath(String path) throws IOException { + var resource = NativeBuffer.class.getClassLoader().getResource(path); + if (resource == null) { + throw new NoSuchFileException("Couldn't find theme resource " + path); + } + + var connection = resource.openConnection(); + try (var in = connection.getInputStream()) { + var contentLengthHint = connection.getContentLength(); + if (contentLengthHint == -1) { + contentLengthHint = 8 * 1024; + } + + ByteBuffer buffer = MemoryUtil.memAlloc(contentLengthHint); + byte[] tmp = new byte[8 * 1024]; + int read; + + while ((read = in.read(tmp)) != -1) { + if (buffer.remaining() < read) { + buffer = MemoryUtil.memRealloc(buffer, buffer.capacity() * 2); + } + buffer.put(tmp, 0, read); + } + + buffer.flip(); + + return new NativeBuffer(buffer, MemoryUtil::memFree); + } + } + public ByteBuffer buffer() { return buffer; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index 2e4cf6f30..5c97f1eee 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -38,13 +38,13 @@ public record Theme( public static Theme createDefaultTheme() { var sprites = new ThemeSprites( new ThemeTexture( - classpathResource("progress_bar_bg.png"), + new ThemeResource("progress_bar_bg.png"), new TextureScaling.NineSlice(40, 20, 2, 2, 2, 2, true, true, false)), new ThemeTexture( - classpathResource("progress_bar_fg.png"), + new ThemeResource("progress_bar_fg.png"), new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true, false)), new ThemeTexture( - classpathResource("progress_bar_fg.png"), + new ThemeResource("progress_bar_fg.png"), new TextureScaling.NineSlice(40, 20, 4, 4, 4, 4, true, true, false)), false); @@ -55,7 +55,7 @@ public static Theme createDefaultTheme() { var fox = new ThemeImageElement(); fox.setTexture( new ThemeTexture( - classpathResource("fox_running.png"), + new ThemeResource("fox_running.png"), new TextureScaling.Stretch(151, 128, false), new AnimationMetadata(28))); fox.setRight(StyleLength.ofPoints(10)); @@ -84,9 +84,9 @@ public static Theme createDefaultTheme() { mojangLogo.setTop(StyleLength.ofPoints(96)); return new Theme( - classpathResource("neoforged_icon.png"), + new ThemeResource("neoforged_icon.png"), Map.of( - FONT_DEFAULT, classpathResource("Monocraft.ttf")), + FONT_DEFAULT, new ThemeResource("Monocraft.ttf")), Map.of( SHADER_GUI, ThemeShader.DEFAULT_GUI, @@ -105,8 +105,4 @@ FONT_DEFAULT, classpathResource("Monocraft.ttf")), "fox", fox, "version", forgeVersion))); } - - private static ClasspathResource classpathResource(String name) { - return new ClasspathResource("net/neoforged/fml/earlydisplay/theme/" + name); - } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java index 38a5b4ede..6e823f75f 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java @@ -31,7 +31,6 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -291,40 +290,22 @@ public ThemeResourceAdapter(Path baseDirectory, boolean exportResources) { } @Override - public void write(JsonWriter out, ThemeResource value) throws IOException { - switch (value) { - case ClasspathResource classpathResource -> { - if (exportResources) { - var idx = Math.max( - classpathResource.path().lastIndexOf('/'), - classpathResource.path().lastIndexOf('\\')); - var filename = classpathResource.path().substring(idx + 1); - var diskPath = baseDirectory.resolve(filename); - try (var buffer = value.toNativeBuffer()) { - Files.write(diskPath, buffer.toByteArray()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - out.value(filename); - } else { - out.value("classpath:" + classpathResource.path()); - } - } - case FileResource fileResource -> { - var diskPath = baseDirectory.resolve(fileResource.file().getName()); - Files.copy(fileResource.file().toPath(), diskPath, StandardCopyOption.REPLACE_EXISTING); - out.value(fileResource.file().getName()); - } - } + public ThemeResource read(JsonReader in) throws IOException { + return new ThemeResource(baseDirectory, in.nextString()); } @Override - public ThemeResource read(JsonReader in) throws IOException { - var text = in.nextString(); - if (text.startsWith("classpath:")) { - return new ClasspathResource(text.substring("classpath:".length())); + public void write(JsonWriter out, ThemeResource value) throws IOException { + if (exportResources) { + // Try loading it from the classpath + var diskPath = baseDirectory.resolve(value.path()); + try (var buffer = value.toNativeBuffer()) { + Files.write(diskPath, buffer.toByteArray()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } - return new FileResource(baseDirectory.resolve(text).toFile()); + out.value(value.path()); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java index c49848f87..d8a285a49 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java @@ -6,21 +6,32 @@ package net.neoforged.fml.earlydisplay.theme; import java.io.IOException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import org.jetbrains.annotations.Nullable; -public sealed interface ThemeResource permits ClasspathResource, FileResource { - /** - * Sanity check to stop going OOM instead of showing the loading screen. - */ - int MAX_SIZE = 100_000_000; +public record ThemeResource(@Nullable Path themeDirectory, String path) { + public ThemeResource(String path) { + this(null, path); + } - NativeBuffer toNativeBuffer() throws IOException; + public NativeBuffer toNativeBuffer() throws IOException { + if (themeDirectory != null) { + try { + return NativeBuffer.loadFromPath(themeDirectory.resolve(path)); + } catch (NoSuchFileException ignored) { + // Fall through and load fallback resource from built-in resources + } + } + + return NativeBuffer.loadFromClasspath("net/neoforged/fml/earlydisplay/theme/" + path); + } /** * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ - default UncompressedImage loadAsImage() { + public UncompressedImage loadAsImage() { return ImageLoader.loadImage(this); } @@ -29,10 +40,10 @@ default UncompressedImage loadAsImage() { * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ @Nullable - default UncompressedImage tryLoadAsImage() { + public UncompressedImage tryLoadAsImage() { return switch (ImageLoader.tryLoadImage(this)) { - case ImageLoader.ImageLoadResult.Error error -> null; - case ImageLoader.ImageLoadResult.Success(UncompressedImage image) -> image; + case ImageLoader.Result.Error error -> null; + case ImageLoader.Result.Success(UncompressedImage image) -> image; }; } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java index 93ea40db1..b7a2d335d 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeShader.java @@ -9,12 +9,12 @@ public record ThemeShader( ThemeResource vertexShader, ThemeResource fragmentShader) { public static final ThemeShader DEFAULT_GUI = new ThemeShader( - new ClasspathResource("net/neoforged/fml/earlydisplay/gui.vert"), - new ClasspathResource("net/neoforged/fml/earlydisplay/gui.frag")); + new ThemeResource("gui.vert"), + new ThemeResource("gui.frag")); public static final ThemeShader DEFAULT_FONT = new ThemeShader( - new ClasspathResource("net/neoforged/fml/earlydisplay/gui.vert"), - new ClasspathResource("net/neoforged/fml/earlydisplay/gui_font.frag")); + new ThemeResource("gui.vert"), + new ThemeResource("gui_font.frag")); public static final ThemeShader DEFAULT_COLOR = new ThemeShader( - new ClasspathResource("net/neoforged/fml/earlydisplay/gui.vert"), - new ClasspathResource("net/neoforged/fml/earlydisplay/gui_color.frag")); + new ThemeResource("gui.vert"), + new ThemeResource("gui_color.frag")); } diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.frag b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.frag similarity index 100% rename from earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.frag rename to earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.frag diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.vert b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.vert similarity index 100% rename from earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui.vert rename to earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui.vert diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_color.frag b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui_color.frag similarity index 100% rename from earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_color.frag rename to earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui_color.frag diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_font.frag b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui_font.frag similarity index 100% rename from earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/gui_font.frag rename to earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/gui_font.frag diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json index 7bccf2c6b..82ca49a67 100644 --- a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json @@ -1,21 +1,21 @@ { "version": 1, - "windowIcon": "classpath:net/neoforged/fml/earlydisplay/theme/neoforged_icon.png", + "windowIcon": "neoforged_icon.png", "fonts": { - "default": "classpath:net/neoforged/fml/earlydisplay/theme/Monocraft.ttf" + "default": "Monocraft.ttf" }, "shaders": { "color": { - "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", - "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui_color.frag" + "vertexShader": "gui.vert", + "fragmentShader": "gui_color.frag" }, "font": { - "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", - "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui_font.frag" + "vertexShader": "gui.vert", + "fragmentShader": "gui_font.frag" }, "gui": { - "vertexShader": "classpath:net/neoforged/fml/earlydisplay/gui.vert", - "fragmentShader": "classpath:net/neoforged/fml/earlydisplay/gui.frag" + "vertexShader": "gui.vert", + "fragmentShader": "gui.frag" } }, "colorScheme": { @@ -26,7 +26,7 @@ }, "sprites": { "progressBarBackground": { - "resource": "classpath:net/neoforged/fml/earlydisplay/theme/progress_bar_bg.png", + "resource": "progress_bar_bg.png", "scaling": { "type": "nine_slice", "width": 40, @@ -41,7 +41,7 @@ } }, "progressBarForeground": { - "resource": "classpath:net/neoforged/fml/earlydisplay/theme/progress_bar_fg.png", + "resource": "progress_bar_fg.png", "scaling": { "type": "nine_slice", "width": 40, @@ -56,7 +56,7 @@ } }, "progressBarIndeterminate": { - "resource": "classpath:net/neoforged/fml/earlydisplay/theme/progress_bar_fg.png", + "resource": "progress_bar_fg.png", "scaling": { "type": "nine_slice", "width": 40, @@ -116,7 +116,7 @@ "fox": { "type": "image", "texture": { - "resource": "classpath:net/neoforged/fml/earlydisplay/theme/fox_running.png", + "resource": "fox_running.png", "scaling": { "type": "stretch", "width": 151, diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportDefaultTheme.java similarity index 94% rename from earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java rename to earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportDefaultTheme.java index ecc644ea3..e70182598 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportThemes.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportDefaultTheme.java @@ -8,7 +8,7 @@ import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeLoader; -public class ExportThemes { +public class ExportDefaultTheme { public static void main(String[] args) throws Exception { var projectRoot = TestUtil.findProjectRoot(); diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java index 4d105f99c..c7af19fca 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java @@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; -import net.neoforged.fml.earlydisplay.theme.ClasspathResource; +import net.neoforged.fml.earlydisplay.theme.ThemeResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,7 +20,7 @@ class SimpleFontTest { @BeforeEach void setUp() throws IOException { - font = new SimpleFont(new ClasspathResource("net/neoforged/fml/earlydisplay/theme/Monocraft.ttf"), 1); + font = new SimpleFont(new ThemeResource("Monocraft.ttf"), 1); } @AfterEach From ac7f85a5afbaf4a9d84e821e41baa1b1da52e6c6 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 11 May 2025 23:35:17 +0200 Subject: [PATCH 20/22] Fix logo lookup on system classloader --- .../fml/earlydisplay/DisplayWindow.java | 3 -- .../render/elements/MojangLogoElement.java | 31 +++++++++++++------ .../fml/earlydisplay/theme/NativeBuffer.java | 8 +++-- .../fml/earlydisplay/theme/ThemeResource.java | 2 +- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 48cdd486d..480ce54f0 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -193,9 +193,6 @@ public Runnable initialize(String[] arguments) { this.rendererFuture = renderScheduler.schedule(() -> new LoadingScreenRenderer(renderScheduler, window, theme, mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS); updateProgress("Initializing Game Graphics"); - StartupNotificationManager.addModMessage("BLAHFASEL"); - var pp = StartupNotificationManager.prependProgressBar("Minecraft Progress", 1000); - pp.setAbsolute(250); return this::periodicTick; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java index b5083f41b..27038faea 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/MojangLogoElement.java @@ -32,20 +32,33 @@ public class MojangLogoElement extends RenderElement { public MojangLogoElement(ThemeMojangLogoElement element, MaterializedTheme theme) { super(element, theme); + // Try the context classloader first, but if we cannot find a logo there, + // try the system loader, since FML might already have isolated us from + // the MC assets that can be found there. + ClassLoader[] classLoaders = { + Thread.currentThread().getContextClassLoader(), + ClassLoader.getSystemClassLoader() + }; + Texture mojangLogo = null; for (var logoPath : LOGO_PATHS) { - try (var buffer = NativeBuffer.loadFromClasspath(logoPath)) { - var loadResult = ImageLoader.tryLoadImage("mojang logo", null, buffer); - if (loadResult instanceof ImageLoader.Result.Error(Exception exception)) { - LOGGER.debug("Failed to load Mojang logo from {}: {}", logoPath, exception); - } else if (loadResult instanceof ImageLoader.Result.Success(UncompressedImage image)) { - mojangLogo = Texture.create(image, "mojang logo", new TextureScaling.Stretch(512, 128, true), null); - break; + for (var classLoader : classLoaders) { + try (var buffer = NativeBuffer.loadFromClasspath(logoPath, classLoader)) { + var loadResult = ImageLoader.tryLoadImage("mojang logo", null, buffer); + if (loadResult instanceof ImageLoader.Result.Error(Exception exception)) { + LOGGER.debug("Failed to load Mojang logo from {}: {}", logoPath, exception); + } else if (loadResult instanceof ImageLoader.Result.Success(UncompressedImage image)) { + mojangLogo = Texture.create(image, "mojang logo", new TextureScaling.Stretch(512, 128, true), null); + break; + } + } catch (IOException e) { + LOGGER.debug("Failed to load Mojang logo from {}", logoPath); } - } catch (IOException e) { - LOGGER.debug("Failed to load Mojang logo from {}", logoPath); } } + if (mojangLogo == null) { + LOGGER.warn("Failed to find Mojang logo at any of the expected classpath locations."); + } this.mojangLogo = mojangLogo; } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java index a5a5636e5..e555bde94 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/NativeBuffer.java @@ -13,6 +13,7 @@ import java.nio.file.Path; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import org.jetbrains.annotations.Nullable; import org.lwjgl.system.MemoryUtil; public final class NativeBuffer implements AutoCloseable { @@ -48,8 +49,11 @@ public static NativeBuffer loadFromPath(Path path) throws IOException { /** * @throws NoSuchFileException If the resource does not exist. */ - public static NativeBuffer loadFromClasspath(String path) throws IOException { - var resource = NativeBuffer.class.getClassLoader().getResource(path); + public static NativeBuffer loadFromClasspath(String path, @Nullable ClassLoader classLoader) throws IOException { + if (classLoader == null) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + var resource = classLoader.getResource(path); if (resource == null) { throw new NoSuchFileException("Couldn't find theme resource " + path); } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java index d8a285a49..7d7b182a6 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java @@ -24,7 +24,7 @@ public NativeBuffer toNativeBuffer() throws IOException { } } - return NativeBuffer.loadFromClasspath("net/neoforged/fml/earlydisplay/theme/" + path); + return NativeBuffer.loadFromClasspath("net/neoforged/fml/earlydisplay/theme/" + path, null); } /** From 17e49644e71b9eefd2740377414ec73cc2fd8a3b Mon Sep 17 00:00:00 2001 From: shartte Date: Sun, 11 May 2025 23:40:40 +0200 Subject: [PATCH 21/22] Update earlydisplay/THEMING.md Co-authored-by: Dennis C --- earlydisplay/THEMING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/earlydisplay/THEMING.md b/earlydisplay/THEMING.md index de566afba..6cf944aba 100644 --- a/earlydisplay/THEMING.md +++ b/earlydisplay/THEMING.md @@ -146,7 +146,7 @@ Inherits all properties of [element](#element), and adds the following: | Property | Type | Description | |-----------------------|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `visible` | boolean | Defaults to true. Can be used to hide pre-defined elements. | -| `maintainAspectRatio` | boolean | Defaults to true. If only one dimension of the element is fully defined, the other dimension will scaled from the implicit element size while maintaining its original aspect ratio. | +| `maintainAspectRatio` | boolean | Defaults to true. If only one dimension of the element is fully defined, the other dimension will be scaled from the implicit element size while maintaining its original aspect ratio. | | `left` | [style length](#style-length) | Defaults to undefined. Positions the left edge of the element relative to the layout box. | | `top` | [style length](#style-length) | Defaults to undefined. Positions the top edge of the element relative to the layout box. | | `right` | [style length](#style-length) | Defaults to undefined. Positions the right edge of the element relative to the layout box. | From 263aef9c6a13ef8fdda91701ebb0aa2f067e60f8 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 18 May 2025 16:15:27 +0200 Subject: [PATCH 22/22] slight refactor of the theme loading API (using a non-contextual GSON adapter) --- earlydisplay/build.gradle | 1 + .../fml/earlydisplay/DisplayWindow.java | 41 +++++-- .../earlydisplay/render/ElementShader.java | 8 +- .../render/LoadingScreenRenderer.java | 5 +- .../render/MaterializedTheme.java | 29 +++-- .../fml/earlydisplay/render/SimpleFont.java | 6 +- .../fml/earlydisplay/render/Texture.java | 5 +- .../render/elements/ImageElement.java | 2 +- .../fml/earlydisplay/theme/ImageLoader.java | 9 +- .../fml/earlydisplay/theme/Theme.java | 6 + .../fml/earlydisplay/theme/ThemeIds.java | 1 + .../fml/earlydisplay/theme/ThemeLoader.java | 111 ++++++------------ .../fml/earlydisplay/theme/ThemeResource.java | 22 ++-- .../theme/theme-april-fools-darkmode.json | 25 ++++ .../earlydisplay/theme/theme-april-fools.json | 3 +- .../fml/earlydisplay/theme/theme-default.json | 30 ++--- .../fml/earlydisplay/ExportDefaultTheme.java | 2 +- .../earlydisplay/render/SimpleFontTest.java | 2 +- .../earlydisplay/theme/BundledThemesTest.java | 98 ++++++++++++++++ .../earlydisplay/theme/ThemeLoaderTest.java | 4 +- .../net/neoforged/fml/loading/FMLConfig.java | 4 +- .../src/moddevTest/java/TestEarlyDisplay.java | 2 + 22 files changed, 275 insertions(+), 141 deletions(-) create mode 100644 earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools-darkmode.json create mode 100644 earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/BundledThemesTest.java diff --git a/earlydisplay/build.gradle b/earlydisplay/build.gradle index bfb9d18da..b383db048 100644 --- a/earlydisplay/build.gradle +++ b/earlydisplay/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation("net.sf.jopt-simple:jopt-simple:${jopt_simple_version}") testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiter_version}") + testImplementation("org.junit.jupiter:junit-jupiter-params:$jupiter_version") testImplementation("org.assertj:assertj-core:${assertj_version}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${jupiter_version}") testRuntimeOnly("org.lwjgl:lwjgl") { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index 480ce54f0..2fa470ca6 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -171,7 +171,14 @@ public Runnable initialize(String[] arguments) { LOGGER.warn("Failed to read dark-mode settings from options.txt", e); } } - this.theme = loadTheme(darkMode); + + var forcedTheme = FMLConfig.getConfigValue(FMLConfig.ConfigValue.EARLY_LOADING_SCREEN_THEME); + if (!forcedTheme.isEmpty()) { + LOGGER.info("Trying to load configured early loading screen theme '{}'", forcedTheme); + this.theme = loadTheme(forcedTheme); + } else { + this.theme = loadTheme(darkMode); + } this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED); var forgeVersion = parsed.valueOf(forgeversionopt); @@ -190,7 +197,13 @@ public Runnable initialize(String[] arguments) { var mcVersion = parsed.valueOf(mcversionopt); initWindow(mcVersion); - this.rendererFuture = renderScheduler.schedule(() -> new LoadingScreenRenderer(renderScheduler, window, theme, mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS); + this.rendererFuture = renderScheduler.schedule(() -> new LoadingScreenRenderer( + renderScheduler, + window, + theme, + getThemePath(), + mcVersion, + forgeVersion), 1, TimeUnit.MILLISECONDS); updateProgress("Initializing Game Graphics"); @@ -198,14 +211,11 @@ public Runnable initialize(String[] arguments) { } private static Theme loadTheme(boolean darkMode) { - Path themePath = getThemePath(); - var themeId = darkMode ? ThemeIds.DARK_MODE : ThemeIds.DEFAULT; + return loadTheme(getThemeId(darkMode)); + } - // Specials - var today = LocalDate.now(); - if (today.getMonth() == Month.APRIL && today.getDayOfMonth() == 1) { - themeId = ThemeIds.APRIL_FOOLS; - } + private static Theme loadTheme(String themeId) { + var themePath = getThemePath(); Theme theme; try { @@ -217,6 +227,17 @@ private static Theme loadTheme(boolean darkMode) { return theme; } + private static String getThemeId(boolean darkMode) { + var themeId = darkMode ? ThemeIds.DARK_MODE : ThemeIds.DEFAULT; + + // Specials + var today = LocalDate.now(); + if (today.getMonth() == Month.APRIL && today.getDayOfMonth() == 1) { + themeId = darkMode ? ThemeIds.APRIL_FOOLS_DARK_MODE : ThemeIds.APRIL_FOOLS; + } + return themeId; + } + private static Path getThemePath() { return FMLPaths.CONFIGDIR.get().resolve("fml"); } @@ -367,7 +388,7 @@ public void initWindow(@Nullable String mcVersion) { // Attempt setting the icon try (var glfwImgBuffer = GLFWImage.malloc(1); var glfwImages = GLFWImage.malloc(); - var icon = theme.windowIcon().loadAsImage()) { + var icon = theme.windowIcon().loadAsImage(getThemePath())) { glfwImgBuffer.put(glfwImages.set(icon.width(), icon.height(), icon.imageData())); glfwImgBuffer.flip(); glfwSetWindowIcon(window, glfwImgBuffer); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java index 2a3f4c8dc..123a121cc 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/ElementShader.java @@ -31,11 +31,13 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import net.neoforged.fml.earlydisplay.theme.ThemeResource; +import org.jetbrains.annotations.Nullable; import org.lwjgl.PointerBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,9 +59,9 @@ private ElementShader(String name, int program, Map uniformLoca this.uniformLocations = uniformLocations; } - public static ElementShader create(String name, ThemeResource vertexShader, ThemeResource fragmentShader) { - try (var vertexShaderBuffer = vertexShader.toNativeBuffer(); - var fragmentShaderBuffer = fragmentShader.toNativeBuffer()) { + public static ElementShader create(String name, ThemeResource vertexShader, ThemeResource fragmentShader, @Nullable Path externalThemeDirectory) { + try (var vertexShaderBuffer = vertexShader.toNativeBuffer(externalThemeDirectory); + var fragmentShaderBuffer = fragmentShader.toNativeBuffer(externalThemeDirectory)) { return create(name, vertexShaderBuffer.buffer(), fragmentShaderBuffer.buffer()); } catch (IOException e) { throw new RuntimeException("Failed to read shaders for " + name); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java index fd948f55d..c6c3f1322 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/LoadingScreenRenderer.java @@ -22,6 +22,7 @@ import static org.lwjgl.opengl.GL11C.glClear; import static org.lwjgl.opengl.GL11C.glGetString; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -42,6 +43,7 @@ import net.neoforged.fml.earlydisplay.theme.elements.ThemeImageElement; import net.neoforged.fml.earlydisplay.theme.elements.ThemeLabelElement; import net.neoforged.fml.earlydisplay.util.Bounds; +import org.jetbrains.annotations.Nullable; import org.lwjgl.opengl.GL32C; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,6 +83,7 @@ public class LoadingScreenRenderer implements AutoCloseable { public LoadingScreenRenderer(ScheduledExecutorService scheduler, long glfwWindow, Theme theme, + @Nullable Path externalThemeDirectory, String mcVersion, String neoForgeVersion) { this.glfwWindow = glfwWindow; @@ -97,7 +100,7 @@ public LoadingScreenRenderer(ScheduledExecutorService scheduler, LOGGER.info("GL info: {} GL version {}, {}", glGetString(GL_RENDERER), glGetString(GL_VERSION), glGetString(GL_VENDOR)); // Create GL resources - this.theme = MaterializedTheme.materialize(theme); + this.theme = MaterializedTheme.materialize(theme, externalThemeDirectory); this.elements = loadElements(); // we always render to an 854x480 texture and then fit that to the screen diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java index 9d0cb63c9..f459f6806 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/MaterializedTheme.java @@ -6,46 +6,51 @@ package net.neoforged.fml.earlydisplay.render; import java.io.IOException; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import net.neoforged.fml.earlydisplay.theme.Theme; import net.neoforged.fml.earlydisplay.theme.ThemeResource; import net.neoforged.fml.earlydisplay.theme.ThemeShader; import net.neoforged.fml.earlydisplay.theme.ThemeSprites; +import org.jetbrains.annotations.Nullable; /** * A themes resources loaded for rendering at runtime. */ public record MaterializedTheme( Theme theme, + @Nullable Path externalThemeDirectory, Map fonts, Map shaders, MaterializedThemeSprites sprites) implements AutoCloseable { - public static MaterializedTheme materialize(Theme theme) { + public static MaterializedTheme materialize(Theme theme, @Nullable Path externalThemeDirectory) { return new MaterializedTheme( theme, - loadFonts(theme.fonts()), - loadShaders(theme.shaders()), - loadSprites(theme.sprites())); + externalThemeDirectory, + loadFonts(theme.fonts(), externalThemeDirectory), + loadShaders(theme.shaders(), externalThemeDirectory), + loadSprites(theme.sprites(), externalThemeDirectory)); } - private static Map loadShaders(Map themeShaders) { + private static Map loadShaders(Map themeShaders, @Nullable Path externalThemeDirectory) { var shaders = new HashMap(themeShaders.size()); for (var entry : themeShaders.entrySet()) { var shader = ElementShader.create( entry.getKey(), entry.getValue().vertexShader(), - entry.getValue().fragmentShader()); + entry.getValue().fragmentShader(), + externalThemeDirectory); shaders.put(entry.getKey(), shader); } return shaders; } - private static Map loadFonts(Map themeFonts) { + private static Map loadFonts(Map themeFonts, @Nullable Path externalThemeDirectory) { var fonts = new HashMap(themeFonts.size()); for (var entry : themeFonts.entrySet()) { try { - fonts.put(entry.getKey(), new SimpleFont(entry.getValue(), 1)); + fonts.put(entry.getKey(), new SimpleFont(entry.getValue(), externalThemeDirectory)); } catch (IOException e) { throw new RuntimeException("Failed to load font " + entry.getKey(), e); } @@ -53,11 +58,11 @@ private static Map loadFonts(Map them return fonts; } - private static MaterializedThemeSprites loadSprites(ThemeSprites sprites) { + private static MaterializedThemeSprites loadSprites(ThemeSprites sprites, @Nullable Path externalThemeDirectory) { return new MaterializedThemeSprites( - Texture.create(sprites.progressBarBackground()), - Texture.create(sprites.progressBarForeground()), - Texture.create(sprites.progressBarIndeterminate())); + Texture.create(sprites.progressBarBackground(), externalThemeDirectory), + Texture.create(sprites.progressBarForeground(), externalThemeDirectory), + Texture.create(sprites.progressBarIndeterminate(), externalThemeDirectory)); } public SimpleFont getFont(String fontId) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java index 50dd5e198..198ec851a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/SimpleFont.java @@ -30,8 +30,10 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import net.neoforged.fml.earlydisplay.theme.ThemeResource; import net.neoforged.fml.earlydisplay.util.Size; +import org.jetbrains.annotations.Nullable; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL32C; import org.lwjgl.stb.STBTTAlignedQuad; @@ -105,8 +107,8 @@ Pos loadQuad(Pos pos, int colour, SimpleBufferBuilder bb) { /** * Build the font and store it in the textureNumber location */ - public SimpleFont(ThemeResource resource, int scale) throws IOException { - try (var nativeBuffer = resource.toNativeBuffer()) { + public SimpleFont(ThemeResource resource, @Nullable Path externalThemeDirectory) throws IOException { + try (var nativeBuffer = resource.toNativeBuffer(externalThemeDirectory)) { var buf = nativeBuffer.buffer(); var info = STBTTFontinfo.create(); if (!stbtt_InitFont(info, buf)) { diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java index e1a34e7ba..7de98dd59 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/Texture.java @@ -20,6 +20,7 @@ import static org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE; import static org.lwjgl.opengl.GL13C.GL_TEXTURE0; +import java.nio.file.Path; import net.neoforged.fml.earlydisplay.theme.AnimationMetadata; import net.neoforged.fml.earlydisplay.theme.TextureScaling; import net.neoforged.fml.earlydisplay.theme.ThemeTexture; @@ -41,8 +42,8 @@ public int height() { /** * Loads a resource into an OpenGL texture. */ - public static Texture create(ThemeTexture themeTexture) { - try (var image = themeTexture.resource().loadAsImage()) { + public static Texture create(ThemeTexture themeTexture, @Nullable Path externalThemeDirectory) { + try (var image = themeTexture.resource().loadAsImage(externalThemeDirectory)) { return create(image, "EarlyDisplay " + themeTexture, themeTexture.scaling(), themeTexture.animation()); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java index d1748dcb7..f87c03de6 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/render/elements/ImageElement.java @@ -15,7 +15,7 @@ public class ImageElement extends RenderElement { public ImageElement(ThemeImageElement element, MaterializedTheme theme) { super(element, theme); - this.texture = Texture.create(element.texture()); + this.texture = Texture.create(element.texture(), theme.externalThemeDirectory()); } @Override diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java index f87fd2f9f..1f23ee611 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ImageLoader.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.file.Path; import org.jetbrains.annotations.Nullable; import org.lwjgl.stb.STBImage; import org.lwjgl.system.MemoryUtil; @@ -30,8 +31,8 @@ public final class ImageLoader { * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ - public static UncompressedImage loadImage(ThemeResource resource) { - return switch (tryLoadImage(resource)) { + public static UncompressedImage loadImage(ThemeResource resource, @Nullable Path externalThemeDirectory) { + return switch (tryLoadImage(resource, externalThemeDirectory)) { case Result.Success(UncompressedImage image) -> image; case Result.Error(Exception exception) -> { LOGGER.error("Failed to load theme image {}", resource, exception); @@ -44,8 +45,8 @@ public static UncompressedImage loadImage(ThemeResource resource) { * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ - public static Result tryLoadImage(ThemeResource resource) { - try (var buffer = resource.toNativeBuffer()) { + public static Result tryLoadImage(ThemeResource resource, @Nullable Path externalThemeDirectory) { + try (var buffer = resource.toNativeBuffer(externalThemeDirectory)) { return tryLoadImage(resource.toString(), resource, buffer); } catch (Exception e) { return new Result.Error(e); diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java index 5c97f1eee..5f1eddff1 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/Theme.java @@ -35,6 +35,12 @@ public record Theme( public static final String SHADER_GUI = "gui"; public static final String SHADER_FONT = "font"; public static final String SHADER_COLOR = "color"; + /** + * Creates the default theme. At runtime, it will actually be loaded from an embedded JSON File containing this structure, + * which makes it easier for modpacks to inspect the default theme and how to adjust it. + *

+ * To update the built-in theme JSON file, see the ExportDefaultTheme class. + */ public static Theme createDefaultTheme() { var sprites = new ThemeSprites( new ThemeTexture( diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java index 5abd83d64..af6663e1a 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeIds.java @@ -14,6 +14,7 @@ public final class ThemeIds { public static final String DEFAULT = "default"; public static final String DARK_MODE = "darkmode"; public static final String APRIL_FOOLS = "april-fools"; + public static final String APRIL_FOOLS_DARK_MODE = "april-fools-darkmode"; private ThemeIds() {} } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java index 6e823f75f..f0782efc9 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeLoader.java @@ -10,7 +10,6 @@ import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; -import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; @@ -25,7 +24,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.UncheckedIOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -48,42 +46,53 @@ public final class ThemeLoader { private static final int VERSION = 1; private static final String BUILTIN_PREFIX = "builtin:"; + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(TextureScaling.class, new TextureScalingSerializer()) + .registerTypeAdapterFactory(new ThemeElementAdapterFactory()) + .registerTypeHierarchyAdapter(ThemeResource.class, new ThemeResourceAdapter()) + .registerTypeAdapter(StyleLength.class, new StyleLengthAdapter()) + .registerTypeAdapter(ThemeColor.class, new ThemeColorAdapter()) + .create(); + private ThemeLoader() {} - public static Theme load(Path baseDirectory, String id) throws IOException { + public static Theme load(@Nullable Path externalThemeDirectory, String id) throws IOException { var sources = new LinkedHashSet(); - var themeTree = readThemeTree(baseDirectory, id, sources); + var themeTree = readThemeTree(externalThemeDirectory, id, sources); try { - return createGson(baseDirectory, false).fromJson(themeTree, Theme.class); + return GSON.fromJson(themeTree, Theme.class); } catch (Exception e) { throw new IOException("Failed to load theme '" + id + "' from JSON structure.", e); } } - private static JsonObject readThemeTree(Path baseDirectory, String id, Set sources) throws IOException { + private static JsonObject readThemeTree(@Nullable Path externalThemeDirectory, String id, Set sources) throws IOException { if (id.startsWith(BUILTIN_PREFIX)) { id = id.substring(BUILTIN_PREFIX.length()); - return readBuiltinThemeTree(baseDirectory, id, sources); + return readBuiltinThemeTree(externalThemeDirectory, id, sources); } - String filename = getThemeFilename(id); - Path themePath = baseDirectory.resolve(filename); - try (var in = openIfExists(themePath)) { - if (in != null) { - if (!sources.add(id)) { - throw new IllegalStateException("Detected recursion in theme extends clause: " + sources + " -> " + id); - } + if (externalThemeDirectory != null) { + String filename = getThemeFilename(id); + Path themePath = externalThemeDirectory.resolve(filename); + try (var in = openIfExists(themePath)) { + if (in != null) { + if (!sources.add(id)) { + throw new IllegalStateException("Detected recursion in theme extends clause: " + sources + " -> " + id); + } - LOG.debug("Loading theme from {}", themePath); - return readThemeTree(baseDirectory, in, sources); + LOG.debug("Loading theme from {}", themePath); + return readThemeTree(externalThemeDirectory, in, sources); + } } } - return readBuiltinThemeTree(baseDirectory, id, sources); + return readBuiltinThemeTree(externalThemeDirectory, id, sources); } - private static JsonObject readBuiltinThemeTree(Path baseDirectory, String id, Set sources) throws IOException { + private static JsonObject readBuiltinThemeTree(@Nullable Path externalThemeDirectory, String id, Set sources) throws IOException { if (!sources.add(BUILTIN_PREFIX + id)) { throw new IllegalStateException("Detected recursion in theme extends clause: " + sources + " -> " + BUILTIN_PREFIX + id); } @@ -95,7 +104,7 @@ private static JsonObject readBuiltinThemeTree(Path baseDirectory, String id, Se if (in == null) { throw new NoSuchFileException("Failed to find embedded theme resource " + classpathLocation); } - return readThemeTree(baseDirectory, in, sources); + return readThemeTree(externalThemeDirectory, in, sources); } } @@ -108,12 +117,12 @@ private static InputStream openIfExists(Path themePath) throws IOException { } } - private static JsonObject readThemeTree(Path baseDirectory, + private static JsonObject readThemeTree(@Nullable Path externalThemeDirectory, InputStream in, Set sources) throws IOException { var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); - var themeRoot = createGson(baseDirectory, false).fromJson(reader, JsonObject.class); + var themeRoot = GSON.fromJson(reader, JsonObject.class); var themeVersion = takeInt(themeRoot, "version"); if (themeVersion == null || themeVersion != VERSION) { throw new JsonParseException("Expected theme version " + VERSION + " but found: " + themeVersion); @@ -121,7 +130,7 @@ private static JsonObject readThemeTree(Path baseDirectory, var extendsId = takeString(themeRoot, "extends"); if (extendsId != null) { - var baseThemeRoot = readThemeTree(baseDirectory, extendsId, sources); + var baseThemeRoot = readThemeTree(externalThemeDirectory, extendsId, sources); themeRoot = mergeThemeRoot(baseThemeRoot, themeRoot); } @@ -203,11 +212,10 @@ private static JsonPrimitive takePrimitive(JsonElement el, String field) { return primitive; } - public static void save(Path path, Theme theme, boolean exportResources) { + public static void save(Path path, Theme theme) { LOG.info("Saving theme to {}", path); - Gson gson = createGson(path.toAbsolutePath().getParent(), exportResources); - var themeTree = (JsonObject) gson.toJsonTree(theme); + var themeTree = (JsonObject) GSON.toJsonTree(theme); var merged = new JsonObject(); merged.addProperty("version", VERSION); for (var entry : themeTree.entrySet()) { @@ -215,24 +223,12 @@ public static void save(Path path, Theme theme, boolean exportResources) { } try (var out = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { - gson.toJson(merged, out); + GSON.toJson(merged, out); } catch (IOException e) { LOG.error("Failed to save theme to {}", path, e); } } - private static Gson createGson(Path baseDirectory, boolean exportResources) { - return new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(TextureScaling.class, new TextureScalingSerializer()) - .registerTypeAdapterFactory(new ThemeElementAdapterFactory()) - .registerTypeHierarchyAdapter(ThemeResource.class, new ThemeResourceAdapter(baseDirectory, exportResources)) - .registerTypeAdapter(UncompressedImage.class, new UncompressedImageSerializer()) - .registerTypeAdapter(StyleLength.class, new StyleLengthAdapter()) - .registerTypeAdapter(ThemeColor.class, new ThemeColorAdapter()) - .create(); - } - private static class StyleLengthAdapter extends TypeAdapter { @Override public void write(JsonWriter out, StyleLength value) throws IOException { @@ -264,47 +260,18 @@ public StyleLength read(JsonReader in) throws IOException { } } - private static class UncompressedImageSerializer implements JsonSerializer, JsonDeserializer { - @Override - public UncompressedImage deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - var resource = (ThemeResource) context.deserialize(json, ThemeResource.class); - return resource.loadAsImage(); - } - - @Override - public JsonElement serialize(UncompressedImage value, Type typeOfSrc, JsonSerializationContext context) { - if (value.source() != null) { - return context.serialize(value.source()); - } - return JsonNull.INSTANCE; - } - } - + /** + * This adapter will copy all encountered theme resources from the built-in theme directory + * to the target directory. + */ private static class ThemeResourceAdapter extends TypeAdapter { - private final Path baseDirectory; - private final boolean exportResources; - - public ThemeResourceAdapter(Path baseDirectory, boolean exportResources) { - this.baseDirectory = baseDirectory; - this.exportResources = exportResources; - } - @Override public ThemeResource read(JsonReader in) throws IOException { - return new ThemeResource(baseDirectory, in.nextString()); + return new ThemeResource(in.nextString()); } @Override public void write(JsonWriter out, ThemeResource value) throws IOException { - if (exportResources) { - // Try loading it from the classpath - var diskPath = baseDirectory.resolve(value.path()); - try (var buffer = value.toNativeBuffer()) { - Files.write(diskPath, buffer.toByteArray()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } out.value(value.path()); } } diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java index 7d7b182a6..f648314ae 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/theme/ThemeResource.java @@ -10,15 +10,11 @@ import java.nio.file.Path; import org.jetbrains.annotations.Nullable; -public record ThemeResource(@Nullable Path themeDirectory, String path) { - public ThemeResource(String path) { - this(null, path); - } - - public NativeBuffer toNativeBuffer() throws IOException { - if (themeDirectory != null) { +public record ThemeResource(String path) { + public NativeBuffer toNativeBuffer(@Nullable Path externalThemeDirectory) throws IOException { + if (externalThemeDirectory != null) { try { - return NativeBuffer.loadFromPath(themeDirectory.resolve(path)); + return NativeBuffer.loadFromPath(externalThemeDirectory.resolve(path)); } catch (NoSuchFileException ignored) { // Fall through and load fallback resource from built-in resources } @@ -31,17 +27,17 @@ public NativeBuffer toNativeBuffer() throws IOException { * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. */ - public UncompressedImage loadAsImage() { - return ImageLoader.loadImage(this); + public UncompressedImage loadAsImage(@Nullable Path externalThemeDirectory) { + return ImageLoader.loadImage(this, externalThemeDirectory); } /** * Load the image resource, and decompress it into native memory for use with OpenGL and other native APIs. - * Note that if the image fails to load for any reason, a dummy "missing" texture is returned instead. + * Note that if the image fails to load for any reason, null is returned. */ @Nullable - public UncompressedImage tryLoadAsImage() { - return switch (ImageLoader.tryLoadImage(this)) { + public UncompressedImage tryLoadAsImage(@Nullable Path externalThemeDirectory) { + return switch (ImageLoader.tryLoadImage(this, externalThemeDirectory)) { case ImageLoader.Result.Error error -> null; case ImageLoader.Result.Success(UncompressedImage image) -> image; }; diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools-darkmode.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools-darkmode.json new file mode 100644 index 000000000..d9f33eb24 --- /dev/null +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools-darkmode.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "extends": "darkmode", + "loadingScreen": { + "decoration": { + "squir": { + "type": "image", + "texture": { + "resource": "squirrel.png", + "scaling": { + "type": "stretch", + "width": 112, + "height": 112, + "linearScaling": true + } + }, + "visible": true, + "maintainAspectRatio": true, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" + } + } + } +} \ No newline at end of file diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json index 82fb40b6d..3fa942ec3 100644 --- a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-april-fools.json @@ -1,5 +1,6 @@ { - "extends": "builtin:default", + "version": 1, + "extends": "default", "loadingScreen": { "decoration": { "squir": { diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json index 82ca49a67..98976b8f3 100644 --- a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json +++ b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/theme-default.json @@ -5,17 +5,17 @@ "default": "Monocraft.ttf" }, "shaders": { - "color": { + "gui": { "vertexShader": "gui.vert", - "fragmentShader": "gui_color.frag" + "fragmentShader": "gui.frag" }, "font": { "vertexShader": "gui.vert", "fragmentShader": "gui_font.frag" }, - "gui": { + "color": { "vertexShader": "gui.vert", - "fragmentShader": "gui.frag" + "fragmentShader": "gui_color.frag" } }, "colorScheme": { @@ -113,6 +113,17 @@ "font": "default" }, "decoration": { + "version": { + "type": "label", + "text": "${version}", + "visible": true, + "maintainAspectRatio": true, + "right": 10.0, + "bottom": 10.0, + "centerHorizontally": false, + "centerVertically": false, + "font": "default" + }, "fox": { "type": "image", "texture": { @@ -134,17 +145,6 @@ "centerHorizontally": false, "centerVertically": false, "font": "default" - }, - "version": { - "type": "label", - "text": "${version}", - "visible": true, - "maintainAspectRatio": true, - "right": 10.0, - "bottom": 10.0, - "centerHorizontally": false, - "centerVertically": false, - "font": "default" } } } diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportDefaultTheme.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportDefaultTheme.java index e70182598..f30415cc2 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportDefaultTheme.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/ExportDefaultTheme.java @@ -14,6 +14,6 @@ public static void main(String[] args) throws Exception { var defaultTheme = Theme.createDefaultTheme(); var builtInThemePath = projectRoot.resolve("src/main/resources/net/neoforged/fml/earlydisplay/theme"); - ThemeLoader.save(builtInThemePath.resolve("theme-default.json"), defaultTheme, false); + ThemeLoader.save(builtInThemePath.resolve("theme-default.json"), defaultTheme); } } diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java index c7af19fca..b24f62e46 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/render/SimpleFontTest.java @@ -20,7 +20,7 @@ class SimpleFontTest { @BeforeEach void setUp() throws IOException { - font = new SimpleFont(new ThemeResource("Monocraft.ttf"), 1); + font = new SimpleFont(new ThemeResource("Monocraft.ttf"), null); } @AfterEach diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/BundledThemesTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/BundledThemesTest.java new file mode 100644 index 000000000..a6aadf334 --- /dev/null +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/BundledThemesTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.earlydisplay.theme; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests that the bundled themes can all be loaded without errors, and that the bundled default theme isn't outdated. + */ +public class BundledThemesTest { + @TempDir + Path tempDir; + + @ParameterizedTest + @MethodSource("builtInThemeIds") + void testLoadBuiltInTheme(String themeId) { + Assertions.assertDoesNotThrow(() -> ThemeLoader.load(null, themeId)); + } + + /** + * Checks that the theme in the resources directory matches what we'd get from exporting the in-code representation. + * If this test fails, run {@link net.neoforged.fml.earlydisplay.ExportDefaultTheme}. + */ + @Test + public void testDefaultThemeMatchesInCodeVersion() throws Exception { + Path expectedTheme = tempDir.resolve("expected.json"); + ThemeLoader.save(expectedTheme, Theme.createDefaultTheme()); + var expectedThemeContent = Files.readString(expectedTheme, StandardCharsets.UTF_8); + var expectedTree = prettyPrint(normalize(JsonParser.parseString(expectedThemeContent))); + + String actualThemeContent; + try (var in = getClass().getResourceAsStream("/net/neoforged/fml/earlydisplay/theme/theme-default.json")) { + Objects.requireNonNull(in, "built-in theme is missing?"); + actualThemeContent = new String(in.readAllBytes(), StandardCharsets.UTF_8); + } + var actualTree = prettyPrint(normalize(JsonParser.parseString(actualThemeContent))); + + assertEquals(actualTree, expectedTree); + } + + public static String[] builtInThemeIds() throws Exception { + var fields = ThemeIds.class.getFields(); + return Arrays.stream(fields).map(f -> { + try { + return (String) f.get(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }).toArray(String[]::new); + } + + private static JsonElement normalize(JsonElement element) { + if (element instanceof JsonObject obj) { + // Alphabetically sort the keys + var t = new ArrayList<>(obj.keySet()); + Collections.sort(t); + + var newObject = new JsonObject(); + for (String s : t) { + newObject.add(s, normalize(obj.get(s))); + } + return newObject; + } else if (element instanceof JsonArray arr) { + var newArr = new JsonArray(); + for (var childEl : arr) { + newArr.add(normalize(childEl)); + } + return newArr; + } else { + return element; + } + } + + private static String prettyPrint(JsonElement el) { + return new GsonBuilder().setPrettyPrinting().create().toJson(el); + } +} diff --git a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java index 80f2d21cf..c64fb0d89 100644 --- a/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java +++ b/earlydisplay/src/test/java/net/neoforged/fml/earlydisplay/theme/ThemeLoaderTest.java @@ -26,7 +26,7 @@ class ThemeLoaderTest { void testDefaultThemeRoundtrip() throws IOException { var defaultTheme = Theme.createDefaultTheme(); Path themePath = tempDir.resolve("theme-default.json"); - ThemeLoader.save(themePath, defaultTheme, false); + ThemeLoader.save(themePath, defaultTheme); var loadedTheme = ThemeLoader.load(tempDir, "default"); assertThat(loadedTheme) @@ -88,7 +88,7 @@ void testOverridingBuiltInTheme() throws Exception { return null; } try { - return resource.toNativeBuffer().toByteArray(); + return resource.toNativeBuffer(null).toByteArray(); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java b/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java index 5cd7b9158..c312ab7b3 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java @@ -44,7 +44,9 @@ public enum ConfigValue { EARLY_WINDOW_WIDTH("earlyWindowWidth", 854, "Early window width"), EARLY_WINDOW_HEIGHT("earlyWindowHeight", 480, "Early window height"), EARLY_WINDOW_MAXIMIZED("earlyWindowMaximized", Boolean.FALSE, "Early window starts maximized"), - EARLY_WINDOW_SQUIR("earlyWindowSquir", Boolean.FALSE, "Squir?"); + @Deprecated(forRemoval = true) + EARLY_WINDOW_SQUIR("earlyWindowSquir", Boolean.FALSE, "Squir?"), + EARLY_LOADING_SCREEN_THEME("earlyLoadingScreenTheme", "", "Force a given theme-id to be used for the early loading screen"); private final String entry; private final Object defaultValue; diff --git a/tests/src/moddevTest/java/TestEarlyDisplay.java b/tests/src/moddevTest/java/TestEarlyDisplay.java index 0ab5e794d..b386c4ab9 100644 --- a/tests/src/moddevTest/java/TestEarlyDisplay.java +++ b/tests/src/moddevTest/java/TestEarlyDisplay.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import net.neoforged.fml.earlydisplay.DisplayWindow; +import net.neoforged.fml.loading.FMLConfig; import net.neoforged.fml.loading.FMLPaths; public class TestEarlyDisplay { @@ -15,6 +16,7 @@ public static void main(String[] args) throws Exception { //System.setProperty("fml.earlyWindowDarkMode", "true"); FMLPaths.loadAbsolutePaths(findProjectRoot()); + FMLConfig.load(); var window = new DisplayWindow(); var periodicTick = window.initialize(new String[] {

!PmMz3P5qVlAg4aHOzE5x?Ajj#xi~ypDc-%|oh* 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≈jx=J|D{o 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 diff --git a/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/Monocraft.ttf b/earlydisplay/src/main/resources/net/neoforged/fml/earlydisplay/theme/Monocraft.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4066b0a9889c2505d31b953487ac1d48fafaf9e9 GIT binary patch literal 202764 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