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