diff --git a/patches/minecraft/net/minecraft/client/gui/Gui.java.patch b/patches/minecraft/net/minecraft/client/gui/Gui.java.patch
index 1687e9df6c..ae1254756d 100644
--- a/patches/minecraft/net/minecraft/client/gui/Gui.java.patch
+++ b/patches/minecraft/net/minecraft/client/gui/Gui.java.patch
@@ -9,6 +9,40 @@
private final SubtitleOverlay subtitleOverlay;
private final SpectatorGui spectatorGui;
private final PlayerTabOverlay tabList;
+@@ -167,7 +_,7 @@
+ private int lastBubblePopSoundPlayed;
+ private float autosaveIndicatorValue;
+ private float lastAutosaveIndicatorValue;
+- private final LayeredDraw layers = new LayeredDraw();
++ private final LayeredDraw layers = new net.minecraftforge.client.gui.overlay.ForgeLayeredDraw(net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.VANILLA_ROOT, net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.COMBINE_LIST);
+ private float scopeScale;
+
+ public Gui(Minecraft p_330021_) {
+@@ -179,14 +_,14 @@
+ this.bossOverlay = new BossHealthOverlay(p_330021_);
+ this.subtitleOverlay = new SubtitleOverlay(p_330021_);
+ this.resetTitleTimes();
+- LayeredDraw layereddraw = new LayeredDraw()
++ LayeredDraw layereddraw = new net.minecraftforge.client.gui.overlay.ForgeLayeredDraw(net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.PRE_SLEEP_STACK, net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.PRE_LIST)
+ .add(this::renderCameraOverlays)
+ .add(this::renderCrosshair)
+ .add(this::renderHotbarAndDecorations)
+ .add(this::renderExperienceLevel)
+ .add(this::renderEffects)
+ .add((p_325313_, p_342678_) -> this.bossOverlay.render(p_325313_));
+- LayeredDraw layereddraw1 = new LayeredDraw()
++ LayeredDraw layereddraw1 = new net.minecraftforge.client.gui.overlay.ForgeLayeredDraw(net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.POST_SLEEP_STACK,net.minecraftforge.client.gui.overlay.ForgeLayeredDraw.POST_LIST)
+ .add(this::renderDemoOverlay)
+ .add((p_325315_, p_344521_) -> {
+ if (this.debugOverlay.showDebugScreen()) {
+@@ -203,6 +_,7 @@
+ .add(layereddraw, () -> !p_330021_.options.hideGui)
+ .add(this::renderSleepOverlay)
+ .add(layereddraw1, () -> !p_330021_.options.hideGui);
++ ((net.minecraftforge.client.gui.overlay.ForgeLayeredDraw) layers).resolveLayers();
+ }
+
+ public void resetTitleTimes() {
@@ -345,7 +_,7 @@
Window window = this.minecraft.getWindow();
int i = Mth.floor(this.minecraft.mouseHandler.xpos() * (double)window.getGuiScaledWidth() / (double)window.getScreenWidth());
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..0707eb1a15
--- /dev/null
+++ b/src/main/java/net/minecraftforge/client/event/AddGuiOverlayLayersEvent.java
@@ -0,0 +1,40 @@
+/*
+ * 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.Event;
+import net.minecraftforge.fml.event.IModBusEvent;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * 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 not {@linkplain net.minecraftforge.eventbus.api.Cancelable cancellable} and does not {@linkplain net.minecraftforge.eventbus.api.Event.HasResult have a result}
+ *
+ * This event is fired on the {@linkplain IModBusEvent mod event bus},
+ * only on the {@linkplain net.minecraftforge.fml.LogicalSide logical client}.
+ */
+public final class AddGuiOverlayLayersEvent extends Event implements IModBusEvent {
+ @NotNull
+ private final ForgeLayeredDraw layeredDraw;
+
+ @ApiStatus.Internal
+ public AddGuiOverlayLayersEvent(ForgeLayeredDraw layeredDraw) {
+ this.layeredDraw = layeredDraw;
+ }
+
+ /**
+ * Get the layered draw.
+ * @return the draw layer stack
+ */
+ @NotNull
+ public ForgeLayeredDraw getLayeredDraw() {
+ return layeredDraw;
+ }
+}
diff --git a/src/main/java/net/minecraftforge/client/event/ForgeEventFactoryClient.java b/src/main/java/net/minecraftforge/client/event/ForgeEventFactoryClient.java
index ed1e8c882e..dae9aeb218 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.event.TickEvent;
import net.minecraftforge.eventbus.api.Cancelable;
@@ -305,7 +306,11 @@ 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));
+ }
+
public static void onLastFramePassAddition(FrameGraphBuilder builder, LevelTargetBundle bundle) {
post(new AddFramePassEvent(builder, bundle));
}
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..f35fb6b3a0
--- /dev/null
+++ b/src/main/java/net/minecraftforge/client/gui/overlay/ForgeLayeredDraw.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.client.gui.overlay;
+
+import com.google.common.collect.ImmutableList;
+import com.mojang.logging.LogUtils;
+import net.minecraft.FieldsAreNonnullByDefault;
+import net.minecraft.MethodsReturnNonnullByDefault;
+import net.minecraft.client.gui.Gui;
+import net.minecraft.client.gui.LayeredDraw;
+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 extends LayeredDraw {
+ private static int unknown = 0;
+ private final Map namedLayers = new HashMap<>();
+ private final Map> subLayerStacks = new HashMap<>();
+ private final List order = new LinkedList<>();
+ private final List expectedNames = 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 HOTBAR = ResourceLocation.withDefaultNamespace("hotbar");
+ public static final ResourceLocation EXPERIENCE = ResourceLocation.withDefaultNamespace("experience");
+ 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");
+
+
+ public static final ImmutableList PRE_LIST = ImmutableList.of(CAMERA_OVERLAY, CROSSHAIR, HOTBAR, EXPERIENCE, POTION_EFFECTS, BOSS_OVERLAY);
+ public static final ImmutableList POST_LIST = ImmutableList.of(DEMO_OVERLAY, DEBUG_OVERLAY, SCOREBOARD, HOTBAR_MESSAGE, TITLE_OVERLAY, CHAT_OVERLAY, TAB_LIST, SUBTITLE_OVERLAY);
+ public static final ImmutableList COMBINE_LIST = ImmutableList.of(PRE_SLEEP_STACK, SLEEP_OVERLAY, POST_SLEEP_STACK);
+
+ /**
+ * Creates a stack of pre-named layers for vanilla.
+ * Not intended for modder use. Use {@linkplain ForgeLayeredDraw(ResourceLocation, Boolean, String...)} instead.
+ * @param name Stack identifier
+ * @param layers locations of layers to add
+ */
+ @ApiStatus.Internal
+ public ForgeLayeredDraw(ResourceLocation name, List layers) {
+ this.name = name;
+ expectedNames.addAll(layers);
+ }
+
+ /**
+ * Creates an empty draw list. Add entries with {@linkplain ForgeLayeredDraw#add(ResourceLocation, Layer)}
+ * @param name marker for which phase this is.
+ */
+ public ForgeLayeredDraw(ResourceLocation name) {
+ this.name = name;
+ }
+
+ /**
+ * Adds a full, pre-named draw stack.
+ * @param layeredDraw layer stack to be added.
+ * @param booleanSupplier requirement for it to render.
+ * @deprecated Modders should be using {@linkplain ForgeLayeredDraw#add(ResourceLocation, LayeredDraw, BooleanSupplier)}
+ * @return this
+ */
+ @Deprecated
+ @Override
+ public ForgeLayeredDraw add(LayeredDraw layeredDraw, BooleanSupplier booleanSupplier) {
+ return add(getNextPreName(), layeredDraw, booleanSupplier);
+ }
+
+ /**
+ * 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, LayeredDraw layeredDraw, BooleanSupplier supplier) {
+ if (isNameAvailable(name)) {
+ subLayerStacks.put(name, Map.entry(layeredDraw, supplier));
+ order.add(name);
+ } else {
+ nameTakenWarning(name);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a layer with an already known name provided by {@linkplain ForgeLayeredDraw(ResourceLocation, List)}
+ * The layer will be rendered last (on top) of already added layers.
+ * @param layer layer render code, see {@linkplain Layer} and example usages in {@linkplain Gui}
+ * @deprecated Use {@linkplain ForgeLayeredDraw#add(ResourceLocation, Layer)} for adding individual layers.
+ * @return this
+ */
+ @Deprecated
+ @Override
+ public LayeredDraw add(Layer layer) {
+ add(getNextPreName(), layer);
+ 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 Layer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw add(ResourceLocation targetStack, ResourceLocation name, Layer 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 Layer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw add(ResourceLocation name, Layer 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 Layer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw addAbove(ResourceLocation expectedStack, ResourceLocation newLayer, ResourceLocation otherLayer, Layer 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, Layer 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 Layer} and example usages in {@linkplain Gui}
+ * @return this
+ */
+ public ForgeLayeredDraw addBelow(ResourceLocation expectedStack, ResourceLocation newLayer, ResourceLocation otherLayer, Layer 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, Layer 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, Layer 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, Layer 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);
+ }
+ if (namedLayers.size() + subLayerStacks.size() < order.size()) {
+ LogUtils.getLogger().warn("Found {} unbound pre-defined layer names when resolving gui overlay order. This is not an error, but potentially indicates a mod directly modifying Gui instead of using this api.", order.size() - namedLayers.size());
+ }
+ 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);
+ if (entry.getKey() instanceof ForgeLayeredDraw resolveable) {
+ resolveable.resolveNested();
+ // Although unlikely, a LayeredDraw could be in the list, and we can't call resolveNested on one.
+ }
+ super.add((gg, tr) -> {
+ if (entry.getValue().getAsBoolean()) entry.getKey().render(gg,tr);
+ });
+ } else {
+ super.add(namedLayers.get(layerName));
+ }
+ }
+ }
+
+ /**
+ * Attempt to locate a particular draw stack. Search starts at caller's instance. For global search call on VANILLA_ROOT
+ * i.e. {@linkplain net.minecraftforge.client.event.AddGuiOverlayLayersEvent#getLayeredDraw()}
+ * 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 LayeredDraw getChild(ResourceLocation childName) {
+ return locateStack(childName).orElse(null);
+ }
+
+ @Nullable
+ public Layer getLayer(ResourceLocation layerName) {
+ return namedLayers.get(layerName);
+ }
+
+ private ResourceLocation getNextPreName() {
+ return expectedNames.isEmpty() ? ResourceLocation.fromNamespaceAndPath("unknown", "layer_" + unknown++) : expectedNames.removeFirst();
+ }
+
+ 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
+ }
+}
diff --git a/src/test/generated/closed_module/module-info.dat b/src/test/generated/closed_module/module-info.dat
index 50ef464ee2..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/java/net/minecraftforge/debug/client/ModifyOverlayTest.java b/src/test/java/net/minecraftforge/debug/client/ModifyOverlayTest.java
new file mode 100644
index 0000000000..79af1e8cc4
--- /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.client.gui.LayeredDraw;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraftforge.client.event.AddGuiOverlayLayersEvent;
+import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw;
+import net.minecraftforge.common.extensions.IForgeGameTestHelper;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
+import net.minecraft.gametest.framework.GameTest;
+import net.minecraftforge.gametest.GameTestHolder;
+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.*;
+
+
+@Mod(ModifyOverlayTest.MODID)
+@GameTestHolder("forge." + 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 Layer notAddedLayer = (gg,tr) -> {};
+ private static final Layer layerA = (gg,tr) -> {};
+ private static final ResourceLocation layerAName = name("layer_a");
+ private static final Layer layerB = (gg,tr) -> {};
+ private static final ResourceLocation layerBName = name("layer_b");
+ private static final Layer 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);
+ context.getModEventBus().addListener(this::overlayTestListener);
+ }
+
+ @GameTest(template = "forge:empty3x3x3")
+ 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(template = "forge:empty3x3x3")
+ 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(template = "forge:empty3x3x3")
+ 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(template = "forge:empty3x3x3")
+ 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();
+ });
+ }
+
+
+ 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().getSuperclass();
+ var layersField = cls.getDeclaredField("layers");
+ 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