diff --git a/patches/minecraft/net/minecraft/client/gui/Gui.java.patch b/patches/minecraft/net/minecraft/client/gui/Gui.java.patch
index b2990bf74a..bbb465a3bd 100644
--- a/patches/minecraft/net/minecraft/client/gui/Gui.java.patch
+++ b/patches/minecraft/net/minecraft/client/gui/Gui.java.patch
@@ -9,6 +9,25 @@
private final SubtitleOverlay subtitleOverlay;
private final SpectatorGui spectatorGui;
private final PlayerTabOverlay tabList;
+@@ -194,6 +_,7 @@
+ () -> new JumpableVehicleBarRenderer(p_330021_)
+ );
+ this.resetTitleTimes();
++ net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.init(this, p_330021_);
+ }
+
+ public void resetTitleTimes() {
+@@ -204,6 +_,10 @@
+
+ public void render(GuiGraphics p_282884_, DeltaTracker p_342095_) {
+ if (this.minecraft.screen == null || !(this.minecraft.screen instanceof ReceivingLevelScreen)) {
++ if (Boolean.valueOf(true)) {
++ net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.beginRender(p_282884_, p_342095_);
++ return;
++ }
+ if (!this.minecraft.options.hideGui) {
+ this.renderCameraOverlays(p_282884_, p_342095_);
+ this.renderCrosshair(p_282884_, p_342095_);
@@ -377,7 +_,7 @@
int i = Mth.floor(this.minecraft.mouseHandler.getScaledXPos(window));
int j = Mth.floor(this.minecraft.mouseHandler.getScaledYPos(window));
@@ -38,7 +57,7 @@
@@ -598,6 +_,10 @@
}
- private void renderSelectedItemName(GuiGraphics p_283501_) {
+ public void renderSelectedItemName(GuiGraphics p_283501_) {
+ renderSelectedItemName(p_283501_, 0);
+ }
+
@@ -71,7 +90,7 @@
}
}
-@@ -1148,7 +_,7 @@
+@@ -1146,7 +_,7 @@
ItemStack itemstack = this.minecraft.player.getInventory().getSelectedItem();
if (itemstack.isEmpty()) {
this.toolHighlightTimer = 0;
diff --git a/src/main/java/net/minecraftforge/client/event/AddGuiOverlayLayersEvent.java b/src/main/java/net/minecraftforge/client/event/AddGuiOverlayLayersEvent.java
new file mode 100644
index 0000000000..d3192d0d14
--- /dev/null
+++ b/src/main/java/net/minecraftforge/client/event/AddGuiOverlayLayersEvent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.client.event;
+
+import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw;
+import net.minecraftforge.eventbus.api.bus.BusGroup;
+import net.minecraftforge.eventbus.api.bus.EventBus;
+import net.minecraftforge.eventbus.api.event.RecordEvent;
+import net.minecraftforge.eventbus.api.event.characteristic.SelfDestructing;
+import net.minecraftforge.fml.event.IModBusEvent;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Fired when the {@linkplain ForgeLayeredDraw#VANILLA_ROOT}'s order is resolved during{@link ForgeLayeredDraw#resolveLayers().
+ * This can be used to add additional or move gui layers and entire layer stacks as needed.
+ *
+ *
This event is fired on the {@linkplain IModBusEvent mod event bus},
+ * only on the {@linkplain net.minecraftforge.fml.LogicalSide logical client}.
+ *
+ * @param getLayedDraw The provided {@linkplain ForgeLayeredDraw#instance}. By default will be {@linkplain ForgeLayeredDraw#VANILLA_ROOT}.
+ */
+@NullMarked
+public record AddGuiOverlayLayersEvent(ForgeLayeredDraw getLayeredDraw) implements IModBusEvent, SelfDestructing, RecordEvent {
+ @ApiStatus.Internal
+ public AddGuiOverlayLayersEvent {}
+
+ public static EventBus getBus(BusGroup modBusGroup) {
+ return IModBusEvent.getBus(modBusGroup, AddGuiOverlayLayersEvent.class);
+ }
+}
diff --git a/src/main/java/net/minecraftforge/client/event/ForgeEventFactoryClient.java b/src/main/java/net/minecraftforge/client/event/ForgeEventFactoryClient.java
index 70307eae6e..6b8a5764d4 100644
--- a/src/main/java/net/minecraftforge/client/event/ForgeEventFactoryClient.java
+++ b/src/main/java/net/minecraftforge/client/event/ForgeEventFactoryClient.java
@@ -10,6 +10,7 @@ import java.util.List;
import java.util.Map;
import java.util.function.Function;
+import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.util.Result;
import net.minecraftforge.event.TickEvent;
@@ -283,4 +284,8 @@ public final class ForgeEventFactoryClient {
// This is on the mod bus because it happens during the initial texture reload, which is while the Forge bus is shut down.
return fireModBus(new ModelEvent.RegisterModelStateDefinitions());
}
+
+ public static void onComputeLayerOrder(ForgeLayeredDraw layeredDraw) {
+ fireModBus(new AddGuiOverlayLayersEvent(layeredDraw));
+ }
}
diff --git a/src/main/java/net/minecraftforge/client/gui/overlay/ForgeLayer.java b/src/main/java/net/minecraftforge/client/gui/overlay/ForgeLayer.java
new file mode 100644
index 0000000000..c5ae37ee6e
--- /dev/null
+++ b/src/main/java/net/minecraftforge/client/gui/overlay/ForgeLayer.java
@@ -0,0 +1,8 @@
+package net.minecraftforge.client.gui.overlay;
+
+import net.minecraft.client.DeltaTracker;
+import net.minecraft.client.gui.GuiGraphics;
+
+public interface ForgeLayer {
+ void render(GuiGraphics gg, DeltaTracker dt);
+}
diff --git a/src/main/java/net/minecraftforge/client/gui/overlay/ForgeLayeredDraw.java b/src/main/java/net/minecraftforge/client/gui/overlay/ForgeLayeredDraw.java
new file mode 100644
index 0000000000..62746dfbc8
--- /dev/null
+++ b/src/main/java/net/minecraftforge/client/gui/overlay/ForgeLayeredDraw.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.client.gui.overlay;
+
+import com.mojang.logging.LogUtils;
+import net.minecraft.FieldsAreNonnullByDefault;
+import net.minecraft.MethodsReturnNonnullByDefault;
+import net.minecraft.client.DeltaTracker;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.Gui;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraftforge.client.event.ForgeEventFactoryClient;
+import net.minecraftforge.client.event.AddGuiOverlayLayersEvent;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+import java.util.*;
+import java.util.function.BooleanSupplier;
+
+/**
+ * As vanilla has switched to a layered drawing system for overlays, this system replaces ForgeGui and its associated headaches.
+ * Vanilla will now have resource locations to represent its render layers which modders can order against.
+ * This class is effectively a pseudo-registry for Layers. Add what you need during {@linkplain AddGuiOverlayLayersEvent}
+ * Layers must be uniquely named per ForgeLayeredDraw, but may be duplicated across different instances.
+ * Any change which would result in a duplicate will not be applied.
+ * All methods which return a {@linkplain ForgeLayeredDraw} will return its caller's instance.
+ * To select a specific instance, use {@linkplain ForgeLayeredDraw#locateStack(ResourceLocation)} before adding
+ * or use the methods that include a stack identifier.
+ */
+@FieldsAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@ParametersAreNonnullByDefault
+public final class ForgeLayeredDraw implements ForgeLayer {
+ private final Map namedLayers = new HashMap<>();
+ private final Map> subLayerStacks = new HashMap<>();
+ private final List order = new LinkedList<>();
+ private final List bakedLayers = new ArrayList<>();
+ private final ResourceLocation name;
+
+ public static final ResourceLocation PRE_SLEEP_STACK = ResourceLocation.withDefaultNamespace("pre_sleep_phase");
+ public static final ResourceLocation CAMERA_OVERLAY = ResourceLocation.withDefaultNamespace("camera_overlay");
+ public static final ResourceLocation CROSSHAIR = ResourceLocation.withDefaultNamespace("crosshair");
+ public static final ResourceLocation CHANGE_STRATUM = ResourceLocation.withDefaultNamespace("stratum_change");
+ public static final ResourceLocation HOTBAR_AND_DECOS = ResourceLocation.withDefaultNamespace("hotbar");
+ public static final ResourceLocation POTION_EFFECTS = ResourceLocation.withDefaultNamespace("potion_effects");
+ public static final ResourceLocation BOSS_OVERLAY = ResourceLocation.withDefaultNamespace("boss_overlay");
+
+ public static final ResourceLocation POST_SLEEP_STACK = ResourceLocation.withDefaultNamespace("post_sleep_phase");
+ public static final ResourceLocation DEMO_OVERLAY = ResourceLocation.withDefaultNamespace("demo");
+ public static final ResourceLocation DEBUG_OVERLAY = ResourceLocation.withDefaultNamespace("debug");
+ public static final ResourceLocation SCOREBOARD = ResourceLocation.withDefaultNamespace("scoreboard");
+ public static final ResourceLocation HOTBAR_MESSAGE = ResourceLocation.withDefaultNamespace("hotbar_message");
+ public static final ResourceLocation TITLE_OVERLAY = ResourceLocation.withDefaultNamespace("title");
+ public static final ResourceLocation CHAT_OVERLAY = ResourceLocation.withDefaultNamespace("chat_overlay");
+ public static final ResourceLocation TAB_LIST = ResourceLocation.withDefaultNamespace("tab_list");
+ public static final ResourceLocation SUBTITLE_OVERLAY = ResourceLocation.withDefaultNamespace("subtitle");
+
+ public static final ResourceLocation VANILLA_ROOT = ResourceLocation.withDefaultNamespace("vanilla_root");
+ public static final ResourceLocation SLEEP_OVERLAY = ResourceLocation.withDefaultNamespace("sleep_overlay");
+
+ private static final ForgeLayeredDraw instance = new ForgeLayeredDraw(VANILLA_ROOT);
+
+ /**
+ * Creates an empty draw list. Add entries with {@linkplain ForgeLayeredDraw#add(ResourceLocation, ForgeLayer)}
+ * @param name marker for which phase this is.
+ */
+ public ForgeLayeredDraw(ResourceLocation name) {
+ this.name = name;
+ }
+
+ /**
+ * Adds a full draw stack with its provided condition.
+ * Use {@linkplain ForgeLayeredDraw#putAbove} and {@linkplain ForgeLayeredDraw#putBelow} for fine location adjustment.
+ * @param name RL of the name to identify this stack with.
+ * @param layeredDraw the draw stack
+ * @param supplier condition for this stack to render
+ * @return this
+ */
+ public ForgeLayeredDraw add(ResourceLocation name, ForgeLayeredDraw layeredDraw, BooleanSupplier supplier) {
+ if (isNameAvailable(name)) {
+ subLayerStacks.put(name, Map.entry(layeredDraw, supplier));
+ order.add(name);
+ } else {
+ nameTakenWarning(name);
+ }
+ return this;
+ }
+
+ /**
+ * Add a layer to the layer list. This layer will be at the end of the list, which means
+ * it will be rendered last (on top) of already added layers.
+ * @param name RL for other mods to order against.
+ * @param layer layer render code, see {@linkplain ForgeLayer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw add(ResourceLocation targetStack, ResourceLocation name, ForgeLayer layer) {
+ locateStack(targetStack).ifPresentOrElse((stack) -> stack.add(name, layer), () -> stackNotPresentWarning(targetStack));
+ return this;
+ }
+
+ /**
+ * Helper method, assumes the intended draw stack instance is the caller.
+ * Use any of the non-deprecated add methods if you want a specific instance,
+ * or call {@linkplain ForgeLayeredDraw#locateStack(ResourceLocation)} to get a reference to an instance.
+ * @param name RL of layer to add
+ * @param layer layer render code, see {@linkplain ForgeLayer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw add(ResourceLocation name, ForgeLayer layer) {
+ if (isNameAvailable(name)) {
+ namedLayers.put(name, layer);
+ order.add(name);
+ } else {
+ nameTakenWarning(name);
+ }
+ return this;
+ }
+
+ /**
+ * Use to specify where your custom draw stack should go. Can also be used to re-order layers.
+ * Both target and destination must be in the same draw stack.
+ * @param expectedStack Which ForgeLayeredDraw the target is expected to be in. If you already have a reference to it, you can use {@linkplain ForgeLayeredDraw#move(ResourceLocation, ResourceLocation, LayerOffset)}
+ * @param target layer name to move
+ * @param destination layer name to order against
+ * @return this
+ */
+ public ForgeLayeredDraw putAbove(ResourceLocation expectedStack, ResourceLocation target, ResourceLocation destination) {
+ locateStack(expectedStack).ifPresentOrElse((stack) -> stack.move(target, destination, LayerOffset.ABOVE), () -> stackNotPresentWarning(expectedStack));
+ return this;
+ }
+
+ /**
+ * Use to specify where your custom draw stack should go. Can also be used to re-order layers.
+ * Both target and destination must be in the same draw stack.
+ * @param expectedStack Which ForgeLayeredDraw the target is expected to be in. If you already have a reference to it, you can use {@linkplain ForgeLayeredDraw#move(ResourceLocation, ResourceLocation, LayerOffset)}
+ * @param target layer name to move
+ * @param destination layer name to order against
+ * @return this
+ */
+ public ForgeLayeredDraw putBelow(ResourceLocation expectedStack, ResourceLocation target, ResourceLocation destination) {
+ locateStack(expectedStack).ifPresentOrElse((stack) -> stack.move(target, destination, LayerOffset.BELOW), () -> stackNotPresentWarning(expectedStack));
+ return this;
+ }
+
+ /**
+ * Helper method, assumes the intended draw stack instance is the caller.
+ * Moves pre-existing names around within the order.
+ * @param target Layer being moved.
+ * @param destination Layer being ordered against
+ * @param offset Self-explanatory
+ * @return this
+ */
+ public ForgeLayeredDraw move(ResourceLocation target, ResourceLocation destination, LayerOffset offset) {
+ if (!order.contains(target)) {
+ layerNotPresentWarning(target);
+ return this;
+ }
+ int loc = order.indexOf(destination);
+ if (loc == -1) {
+ layerNotPresentWarning(destination);
+ return this;
+ }
+ order.remove(target);
+ order.add(loc + (offset == LayerOffset.ABOVE ? 1 : 0), target);
+ return this;
+ }
+
+ /**
+ * Adds an overlay layer to be rendered above the other provided layer.
+ * To render "above" another layer means thisLayer will be rendered after otherLayer
+ * If the current stack does not contain otherLayer, no changes will be made.
+ * @param newLayer name of the layer to be added
+ * @param otherLayer name of the layer being ordered against
+ * @param layer layer render code, see {@linkplain ForgeLayer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw addAbove(ResourceLocation expectedStack, ResourceLocation newLayer, ResourceLocation otherLayer, ForgeLayer layer) {
+ locateStack(expectedStack).ifPresentOrElse((stack) -> {
+ if (!stack.isNameAvailable(otherLayer)) {
+ stack.add(newLayer, layer).move(newLayer, otherLayer, LayerOffset.ABOVE);
+ } else {
+ layerNotPresentWarning(otherLayer);
+ }
+ }, () -> stackNotPresentWarning(expectedStack));
+ return this;
+ }
+
+ /**
+ * Helper method, assumes intended draw stack is the caller
+ */
+ public ForgeLayeredDraw addAbove(ResourceLocation newLayer, ResourceLocation otherLayer, ForgeLayer layer) {
+ return addAbove(name, newLayer, otherLayer, layer);
+ }
+
+ /**
+ * Adds an overlay layer to be rendered below the other provided layer.
+ * To render "below" another layer means thisLayer will be rendered before otherLayer
+ * If the current stack does not contain otherLayer, no changes will be made.
+ * @param newLayer name of the layer to be added
+ * @param otherLayer name of the layer being ordered against
+ * @param layer layer render code, see {@linkplain ForgeLayer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw addBelow(ResourceLocation expectedStack, ResourceLocation newLayer, ResourceLocation otherLayer, ForgeLayer layer) {
+ locateStack(expectedStack).ifPresentOrElse((stack) -> {
+ if (!stack.isNameAvailable(otherLayer)) {
+ stack.add(newLayer, layer).move(newLayer, otherLayer, LayerOffset.BELOW);
+ } else {
+ layerNotPresentWarning(otherLayer);
+ }
+ }, () -> stackNotPresentWarning(expectedStack));
+ return this;
+ }
+
+ /**
+ * Helper method, assumes intended draw stack is the caller
+ */
+ public ForgeLayeredDraw addBelow(ResourceLocation newLayer, ResourceLocation otherLayer, ForgeLayer layer) {
+ return addBelow(name, newLayer, otherLayer, layer);
+ }
+
+ /**
+ * Add a new layer that will only be rendered when the condition is met. The layer will be added
+ * at the end of the list, which means it will render last (on top) of already added layers
+ * @param name name of the layer to be added
+ * @param layer render code of layer being added
+ * @param condition supplier for the condition
+ * @return this
+ */
+ public ForgeLayeredDraw addWithCondition(ResourceLocation targetStack, ResourceLocation name, ForgeLayer layer, BooleanSupplier condition) {
+ locateStack(targetStack).ifPresentOrElse((stack) -> stack.addWithCondition(name, layer, condition), () -> stackNotPresentWarning(targetStack));
+ return this;
+ }
+
+ /**
+ * Assumes the correct stack is the caller of this method, otherwise functions the same as other method.
+ * @param name name of layer to be added
+ * @param layer render code of layer being added
+ * @param condition supplier for the condition
+ * @return this
+ */
+ public ForgeLayeredDraw addWithCondition(ResourceLocation name, ForgeLayer layer, BooleanSupplier condition) {
+ add(name, layer).addConditionTo(name, condition);
+ return this;
+ }
+
+ /**
+ * Add a condition to a pre-existing layer in the specified stack, its render order is not changed.
+ * If the target is not present, no changes are made.
+ * @param targetStack name of the draw stack the target is expected to be in
+ * @param targetLayer name of layer to add a condition to
+ * @param condition supplier for the condition
+ * @return this
+ */
+ public ForgeLayeredDraw addConditionTo(ResourceLocation targetStack, ResourceLocation targetLayer, BooleanSupplier condition) {
+ locateStack(targetStack).ifPresentOrElse((stack) -> stack.addConditionTo(targetLayer, condition), () -> stackNotPresentWarning(targetStack));
+ return this;
+ }
+
+ /**
+ * Assumes the correct stack is the caller of this method.
+ * Use {@linkplain ForgeLayeredDraw#addConditionTo(ResourceLocation, ResourceLocation, BooleanSupplier)}
+ * if you do not have a reference to the draw stack you want.
+ * @param targetLayer name of layer to add condition to
+ * @param condition supplier for the condition
+ * @return this
+ */
+ public ForgeLayeredDraw addConditionTo(ResourceLocation targetLayer, BooleanSupplier condition) {
+ var result = namedLayers.computeIfPresent(targetLayer,
+ (name, layer) -> (guiGraphics, deltaTracker) -> {
+ if (condition.getAsBoolean()) {
+ layer.render(guiGraphics, deltaTracker);
+ }
+ });
+ if (result == null) {
+ layerNotPresentWarning(targetLayer);
+ }
+ return this;
+ }
+
+ /**
+ * Propagate the layer order down to the inner render list after providing modders an opportunity to alter the list as they wish.
+ * @apiNote Modders should NEVER be calling this method.
+ * @return this
+ */
+ @ApiStatus.Internal
+ public ForgeLayeredDraw resolveLayers() {
+ if (!order.isEmpty()) {
+ ForgeEventFactoryClient.onComputeLayerOrder(this);
+ }
+ resolveNested();
+ order.clear();
+ return this;
+ }
+
+ /**
+ * Resolve the layer order per stack, recursively as needed.
+ * Parent layer stack {@linkplain ForgeLayeredDraw#VANILLA_ROOT} holds the results.
+ */
+ private void resolveNested() {
+ for (ResourceLocation layerName : order) {
+ if (subLayerStacks.containsKey(layerName)) {
+ var entry = subLayerStacks.get(layerName);
+ entry.getKey().resolveNested();
+ bakedLayers.add((gg, tr) -> {
+ if (entry.getValue().getAsBoolean()) entry.getKey().render(gg,tr);
+ });
+ } else {
+ bakedLayers.add(namedLayers.get(layerName));
+ }
+ }
+ }
+
+ /**
+ * Attempt to locate a particular draw stack. Search starts at caller's instance. For global search call on VANILLA_ROOT
+ * Entries which don't extend ForgeLayeredDraw may not have the proper fields to support internal list adjustment, so they are skipped.
+ * @param targetStack Name of ForgeLayeredDraw to find
+ * @return Filled Optional if target exists, empty otherwise.
+ */
+ public Optional locateStack(ResourceLocation targetStack) {
+ if (!name.equals(targetStack)) {
+ for (Map.Entry value : subLayerStacks.values()) {
+ if (value.getKey() instanceof ForgeLayeredDraw searchable) {
+ var res = searchable.locateStack(targetStack);
+ if(res.isPresent()) return res;
+ }
+ }
+ return Optional.empty();
+ } else {
+ return Optional.of(this);
+ }
+ }
+
+ @Nullable
+ public ForgeLayeredDraw getChild(ResourceLocation childName) {
+ return locateStack(childName).orElse(null);
+ }
+
+ @Nullable
+ public ForgeLayer getLayer(ResourceLocation layerName) {
+ return namedLayers.get(layerName);
+ }
+
+ public ResourceLocation getName() {
+ return name;
+ }
+
+ private void stackNotPresentWarning(ResourceLocation stackName) {
+ LogUtils.getLogger().warn("Target stack {} was not present anywhere. Is your ResourceLocation correct?", stackName);
+ }
+
+ private void layerNotPresentWarning(ResourceLocation layer) {
+ LogUtils.getLogger().warn("Expected layer {} was not found in stack {}, no layer modifications have been made.", layer, name);
+ }
+
+ private void nameTakenWarning(ResourceLocation layer) {
+ LogUtils.getLogger().warn("Name {} was already present in {} and cannot be re-used.", layer, name);
+ }
+
+ private boolean isNameAvailable(ResourceLocation name) {
+ return !namedLayers.containsKey(name) && !subLayerStacks.containsKey(name);
+ }
+
+ public enum LayerOffset {
+ ABOVE,
+ BELOW
+ }
+
+ @ApiStatus.Internal
+ public static void beginRender(GuiGraphics gg, DeltaTracker dt) {
+ instance.render(gg,dt);
+ }
+
+ @ApiStatus.Internal
+ public void render(GuiGraphics gg, DeltaTracker dt) {
+ for (ForgeLayer bakedLayer : bakedLayers) {
+ bakedLayer.render(gg, dt);
+ }
+ }
+
+ @ApiStatus.Internal
+ public static void init(Gui gui, Minecraft minecraft) {
+ var preSleepDraw = new ForgeLayeredDraw(PRE_SLEEP_STACK)
+ .add(CAMERA_OVERLAY, gui::renderCameraOverlays)
+ .add(CROSSHAIR, gui::renderCrosshair)
+ .add(CHANGE_STRATUM, (gg, dt) -> gg.nextStratum())
+ .add(HOTBAR_AND_DECOS, gui::renderHotbarAndDecorations)
+ .add(POTION_EFFECTS, gui::renderEffects)
+ .add(BOSS_OVERLAY, gui::renderBossOverlay);
+ var postSleepDraw = new ForgeLayeredDraw(POST_SLEEP_STACK)
+ .add(DEMO_OVERLAY, gui::renderDemoOverlay)
+ .add(DEBUG_OVERLAY, gui::renderDebugOverlay)
+ .add(SCOREBOARD, gui::renderScoreboardSidebar)
+ .add(HOTBAR_MESSAGE, gui::renderOverlayMessage)
+ .add(TITLE_OVERLAY, gui::renderTitle)
+ .add(CHAT_OVERLAY, gui::renderChat)
+ .add(TAB_LIST, gui::renderTabList)
+ .add(SUBTITLE_OVERLAY, gui::renderSubtitleOverlay);
+ instance
+ .add(PRE_SLEEP_STACK, preSleepDraw, () -> !minecraft.options.hideGui)
+ .add(SLEEP_OVERLAY, gui::renderSleepOverlay)
+ .add(POST_SLEEP_STACK, postSleepDraw, () -> !minecraft.options.hideGui);
+ instance.resolveLayers();
+ }
+}
diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg
index 058e52cddb..3efc47bdf2 100644
--- a/src/main/resources/META-INF/accesstransformer.cfg
+++ b/src/main/resources/META-INF/accesstransformer.cfg
@@ -344,6 +344,7 @@ public net.minecraft.client.data.models.ModelProvider$ItemInfoCollector
public net.minecraft.client.data.models.ModelProvider$ItemInfoCollector ()V # constructor
public net.minecraft.client.data.models.ModelProvider$SimpleModelCollector
public net.minecraft.client.data.models.ModelProvider$SimpleModelCollector ()V # constructor
+public net.minecraft.client.gui.Gui *() # render methods
public net.minecraft.client.gui.GuiGraphics$ScissorStack
public net.minecraft.client.gui.GuiGraphics$ScissorStack ()V # constructor
protected net.minecraft.client.gui.components.AbstractButton f_290895_ # SPRITES
diff --git a/src/test/generated/closed_module/module-info.dat b/src/test/generated/closed_module/module-info.dat
index 9c2ea90bb9..dbb5f663cd 100644
Binary files a/src/test/generated/closed_module/module-info.dat and b/src/test/generated/closed_module/module-info.dat differ
diff --git a/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/add_condition.json b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/add_condition.json
new file mode 100644
index 0000000000..3a87a5d121
--- /dev/null
+++ b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/add_condition.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "environment": "minecraft:default",
+ "function": "forge:modify_overlay_test/add_condition",
+ "max_ticks": 100,
+ "structure": "forge:empty3x3x3"
+}
\ No newline at end of file
diff --git a/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/full_stack_condition.json b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/full_stack_condition.json
new file mode 100644
index 0000000000..80de29b253
--- /dev/null
+++ b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/full_stack_condition.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "environment": "minecraft:default",
+ "function": "forge:modify_overlay_test/full_stack_condition",
+ "max_ticks": 100,
+ "structure": "forge:empty3x3x3"
+}
\ No newline at end of file
diff --git a/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/not_present_in_stack.json b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/not_present_in_stack.json
new file mode 100644
index 0000000000..80ea2261a0
--- /dev/null
+++ b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/not_present_in_stack.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "environment": "minecraft:default",
+ "function": "forge:modify_overlay_test/not_present_in_stack",
+ "max_ticks": 100,
+ "structure": "forge:empty3x3x3"
+}
\ No newline at end of file
diff --git a/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/ordered_layers.json b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/ordered_layers.json
new file mode 100644
index 0000000000..e1789c75b8
--- /dev/null
+++ b/src/test/generated/modify_overlay_test/data/forge/test_instance/modify_overlay_test/ordered_layers.json
@@ -0,0 +1,7 @@
+{
+ "type": "minecraft:function",
+ "environment": "minecraft:default",
+ "function": "forge:modify_overlay_test/ordered_layers",
+ "max_ticks": 100,
+ "structure": "forge:empty3x3x3"
+}
\ No newline at end of file
diff --git a/src/test/java/net/minecraftforge/debug/client/ModifyOverlayTest.java b/src/test/java/net/minecraftforge/debug/client/ModifyOverlayTest.java
new file mode 100644
index 0000000000..a6651fb073
--- /dev/null
+++ b/src/test/java/net/minecraftforge/debug/client/ModifyOverlayTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.debug.client;
+
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraftforge.client.event.AddGuiOverlayLayersEvent;
+import net.minecraftforge.client.gui.overlay.ForgeLayer;
+import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw;
+import net.minecraftforge.common.extensions.IForgeGameTestHelper;
+import net.minecraftforge.eventbus.api.listener.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
+import net.minecraftforge.gametest.GameTest;
+import net.minecraftforge.gametest.GameTestNamespace;
+import net.minecraftforge.test.BaseTestMod;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BooleanSupplier;
+
+import static net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.*;
+
+@GameTestNamespace("forge")
+@Mod(ModifyOverlayTest.MODID)
+public class ModifyOverlayTest extends BaseTestMod {
+ public static final String MODID = "modify_overlay_test";
+
+ private static final ResourceLocation myStackName = name("my_stack_name");
+ private static final ForgeLayeredDraw myLayerStack = new ForgeLayeredDraw(myStackName);
+
+ private static final ForgeLayer notAddedLayer = (gg, tr) -> {};
+ private static final ForgeLayer layerA = (gg,tr) -> {};
+ private static final ResourceLocation layerAName = name("layer_a");
+ private static final ForgeLayer layerB = (gg,tr) -> {};
+ private static final ResourceLocation layerBName = name("layer_b");
+ private static final ForgeLayer layerC = (gg,tr) -> {};
+ private static final ResourceLocation layerCName = name("layer_c");
+
+
+ private static final IForgeGameTestHelper.BoolFlag detectConditionFlag = new IForgeGameTestHelper.BoolFlag("det_cond_flag");
+ private static final IForgeGameTestHelper.BoolFlag detectConditionStackFlag = new IForgeGameTestHelper.BoolFlag("det_cond_stack_flag");
+ private static final IForgeGameTestHelper.BoolFlag enableConditionFlag = new IForgeGameTestHelper.BoolFlag("en_cond_flag");
+ private static final IForgeGameTestHelper.BoolFlag enableConditionStackFlag = new IForgeGameTestHelper.BoolFlag("en_cond_stack_flag");
+
+ private static ForgeLayeredDraw drawStack;
+
+ public ModifyOverlayTest(FMLJavaModLoadingContext context) {
+ super(context, false, false);
+ AddGuiOverlayLayersEvent.getBus(context.getModBusGroup()).addListener(this::overlayTestListener);
+ }
+
+ @GameTest
+ public static void not_present_in_stack(GameTestHelper helper) {
+ // Test that we can't order against non-existent layers.
+ List internalLayersList = null;
+ try {
+ internalLayersList = getInternalLayersList(drawStack);
+ } catch (Exception e) {
+ helper.fail("Threw a " + e.getMessage() + " when trying to get the inner layer list.");
+ }
+ helper.assertFalse(internalLayersList.contains(notAddedLayer), "Found our layer when we shouldn't have. Not good!");
+ helper.succeed();
+ }
+
+ @GameTest
+ public static void add_condition(GameTestHelper helper) {
+ // Test that we can add conditions to pre-existing layers.
+ helper.assertFalse(detectConditionFlag.getBool(), "Conditional rendering ran when it shouldn't have.");
+ enableConditionFlag.set(true);
+ helper.runAfterDelay(5, () -> {
+ boolean result = detectConditionFlag.getBool();// Just in case of weird race conditions.
+ enableConditionFlag.set(false);
+ helper.assertTrue(result, "");
+ helper.succeed();
+ });
+ }
+
+ @GameTest
+ public static void ordered_layers(GameTestHelper helper) {
+ // Test that layers are in the correct order.
+ List internalLayersList = null;
+ Map check = null;
+ try {
+ Class> cls = drawStack.getClass();
+ var field = cls.getDeclaredField("subLayerStacks");
+ var field1 = cls.getDeclaredField("namedLayers");
+ field.setAccessible(true);
+ field1.setAccessible(true);
+ Map> VROOT = ((Map>) field.get(drawStack));
+ var PSS = ((ForgeLayeredDraw) VROOT.get(PRE_SLEEP_STACK).getKey());
+ check = ((Map) field1.get(PSS));
+ internalLayersList = getInternalLayersList(PSS);
+ } catch (Exception e) {
+ helper.fail("Threw a " + e.getMessage() + " when trying to get the inner layer list.");
+ }
+ int locationOfPotionEffects = internalLayersList.indexOf(check.get(POTION_EFFECTS));
+ int locationOfLayerA = internalLayersList.indexOf(check.get(layerAName));
+ int locationOfLayerB = internalLayersList.indexOf(check.get(layerBName));
+ int locationOfLayerC = internalLayersList.indexOf(check.get(layerCName));
+ helper.assertTrue(locationOfPotionEffects == locationOfLayerA - 1, "Layer offset from vanilla -> A was incorrect");
+ helper.assertTrue(locationOfLayerB == locationOfLayerA + 1, "Layer offset from A -> B was incorrect");
+ helper.assertTrue(locationOfLayerC == locationOfLayerB + 1, "Layer offset from B -> C was incorrect");
+ helper.succeed();
+ }
+
+ @GameTest
+ public static void full_stack_condition(GameTestHelper helper) {
+ // Test conditional rendering & whether a full stack can be added in the first place.
+ helper.assertFalse(detectConditionStackFlag.getBool(), "Conditional rendering ran when it shouldn't have.");
+ enableConditionStackFlag.set(true);
+ helper.runAfterDelay(5, () -> {
+ boolean result = detectConditionStackFlag.getBool();// Just in case of weird race conditions.
+ enableConditionStackFlag.set(false);
+ helper.assertTrue(result, "");
+ helper.succeed();
+ });
+ }
+
+ @SubscribeEvent
+ private void overlayTestListener(AddGuiOverlayLayersEvent event) {
+ drawStack = event.getLayeredDraw();
+ var layeredDraw = event.getLayeredDraw();
+ layeredDraw.addAbove(VANILLA_ROOT, name("i_won_t_exist"), name("non_existent_target_layer"), notAddedLayer);
+ var pEffects = layeredDraw.locateStack(PRE_SLEEP_STACK).get();
+ // Test if layers may be ordered against other layers.
+ // Layers have to be present to be ordered against, of course, but we tested for that already above.
+ pEffects.addAbove(layerBName, POTION_EFFECTS, layerB);
+ pEffects.addAbove(layerCName, layerBName, layerC);
+ layeredDraw.addBelow(PRE_SLEEP_STACK, layerAName, layerBName, layerA);
+ layeredDraw.addConditionTo(PRE_SLEEP_STACK, BOSS_OVERLAY, () -> {
+ if (enableConditionFlag.getBool()) {
+ detectConditionFlag.set(true);
+ return true;
+ } else {
+ detectConditionFlag.set(false);
+ return false;
+ }
+ });
+
+ myLayerStack.add(name("my_inner_layer_name"), (gg, tr) -> {
+ detectConditionStackFlag.set(true);
+ });
+ // Demonstrates that entire stacks can have conditions attached
+ event.getLayeredDraw()
+ .add(myStackName, myLayerStack, () -> {
+ if (enableConditionStackFlag.getBool()) {
+ return true;
+ } else {
+ detectConditionStackFlag.set(false);
+ return false;
+ }
+ });
+ }
+
+
+ private static ResourceLocation name(String name) {
+ return ResourceLocation.fromNamespaceAndPath(MODID, name);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List getInternalLayersList(ForgeLayeredDraw stack) throws ClassCastException, NoSuchFieldException, IllegalAccessException {
+ Class> cls = stack.getClass();
+ var layersField = cls.getDeclaredField("bakedLayers");
+ layersField.setAccessible(true);
+ return (List) layersField.get(stack);
+ }
+}
diff --git a/src/test/resources/modify_overlay_test/META-INF/mods.toml b/src/test/resources/modify_overlay_test/META-INF/mods.toml
new file mode 100644
index 0000000000..c361869e8e
--- /dev/null
+++ b/src/test/resources/modify_overlay_test/META-INF/mods.toml
@@ -0,0 +1 @@
+clientSideOnly=true
\ No newline at end of file