diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/screenshot/TestScreenshotComparisonOptions.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/screenshot/TestScreenshotComparisonOptions.java
index 4f3663c9bb..e4149fbadd 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/screenshot/TestScreenshotComparisonOptions.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/screenshot/TestScreenshotComparisonOptions.java
@@ -69,6 +69,17 @@ static TestScreenshotComparisonOptions of(NativeImage templateImage) {
return new TestScreenshotComparisonOptionsImpl(templateImage);
}
+ /**
+ * Additionally save the screenshot which was compared against with the template image name.
+ * This method only works when a template image name instead of a {@link NativeImage} is used.
+ * This method works as if by calling {@link ClientGameTestContext#takeScreenshot(TestScreenshotOptions)}
+ * with these screenshot options, except that the screenshot saved is from the same render of the game
+ * as the one that is compared against in this screenshot comparison.
+ *
+ * @return This screenshot comparison options instance
+ */
+ TestScreenshotComparisonOptions save();
+
/**
* Additionally save the screenshot which was compared against. This method works as if by calling
* {@link ClientGameTestContext#takeScreenshot(TestScreenshotOptions)} with these screenshot options, except that
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/screenshot/TestScreenshotComparisonOptionsImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/screenshot/TestScreenshotComparisonOptionsImpl.java
index 3a667f8a25..8fcb64ad9b 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/screenshot/TestScreenshotComparisonOptionsImpl.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/screenshot/TestScreenshotComparisonOptionsImpl.java
@@ -50,6 +50,11 @@ public TestScreenshotComparisonOptionsImpl(NativeImage templateImage) {
this.templateImage = Either.right(templateImage);
}
+ @Override
+ public TestScreenshotComparisonOptions save() {
+ return saveWithFileName(getTemplateImagePath());
+ }
+
@Override
public TestScreenshotComparisonOptions saveWithFileName(String fileName) {
Preconditions.checkNotNull(fileName, "fileName");
diff --git a/fabric-rendering-v1/build.gradle b/fabric-rendering-v1/build.gradle
index 61680942a1..93b1b30e6a 100644
--- a/fabric-rendering-v1/build.gradle
+++ b/fabric-rendering-v1/build.gradle
@@ -3,5 +3,6 @@ version = getSubprojectVersion(project)
moduleDependencies(project, ['fabric-api-base'])
testDependencies(project, [
+ ':fabric-client-gametest-api-v1',
':fabric-object-builder-api-v1'
])
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudLayerRegistrationCallback.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudLayerRegistrationCallback.java
new file mode 100644
index 0000000000..0a7b0483e9
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudLayerRegistrationCallback.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.rendering.v1;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+/**
+ * Callback for when hud layers are registered.
+ *
+ *
To register a layer, register a listener to this event and register your layers in the listener.
+ * For common use cases, see {@link LayeredDrawerWrapper}.
+ *
+ *
For example, the following code registers a layer after {@link IdentifiedLayer#MISC_OVERLAYS}:
+ * {@snippet :
+ * // @link region substring=HudLayerRegistrationCallback target=HudLayerRegistrationCallback
+ * // @link region substring=EVENT target="HudLayerRegistrationCallback#EVENT"
+ * // @link region substring=layeredDrawer target="LayeredDrawerWrapper"
+ * // @link region substring=attachLayerAfter target="LayeredDrawerWrapper#attachLayerAfter"
+ * // @link region substring=IdentifiedLayer target=IdentifiedLayer
+ * // @link region substring=MISC_OVERLAYS target="IdentifiedLayer#MISC_OVERLAYS"
+ * // @link region substring=Identifier target="net.minecraft.util.Identifier"
+ * // @link region substring=of target="net.minecraft.util.Identifier#of"
+ * // @link region substring=context target="net.minecraft.client.gui.DrawContext"
+ * // @link region substring=tickCounter target="net.minecraft.client.render.RenderTickCounter"
+ * HudLayerRegistrationCallback.EVENT.register(layeredDrawer -> layeredDrawer.attachLayerAfter(IdentifiedLayer.MISC_OVERLAYS, Identifier.of("example", "example_layer_after_misc_overlays"), (context, tickCounter) -> {
+ * // Your rendering code here
+ * }));
+ * // @end @end @end @end @end @end @end @end @end @end
+ * }
+ *
+ * @see LayeredDrawerWrapper
+ */
+public interface HudLayerRegistrationCallback {
+ Event EVENT = EventFactory.createArrayBacked(HudLayerRegistrationCallback.class, callbacks -> layeredDrawer -> {
+ for (HudLayerRegistrationCallback callback : callbacks) {
+ callback.register(layeredDrawer);
+ }
+ });
+
+ /**
+ * Called when registering hud layers.
+ *
+ * @param layeredDrawer the layered drawer to register layers to
+ * @see LayeredDrawerWrapper
+ */
+ void register(LayeredDrawerWrapper layeredDrawer);
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudRenderCallback.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudRenderCallback.java
index 4890d53ed6..e50a055939 100644
--- a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudRenderCallback.java
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudRenderCallback.java
@@ -22,10 +22,14 @@
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
+/**
+ * @deprecated Use {@link HudLayerRegistrationCallback} instead. For common use cases, see {@link LayeredDrawerWrapper}.
+ */
+@Deprecated
public interface HudRenderCallback {
- Event EVENT = EventFactory.createArrayBacked(HudRenderCallback.class, (listeners) -> (matrixStack, delta) -> {
+ Event EVENT = EventFactory.createArrayBacked(HudRenderCallback.class, (listeners) -> (context, tickCounter) -> {
for (HudRenderCallback event : listeners) {
- event.onHudRender(matrixStack, delta);
+ event.onHudRender(context, tickCounter);
}
});
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/IdentifiedLayer.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/IdentifiedLayer.java
new file mode 100644
index 0000000000..ab4d60727c
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/IdentifiedLayer.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.rendering.v1;
+
+import net.minecraft.client.gui.LayeredDrawer;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.client.rendering.WrappedLayer;
+
+/**
+ * A hud layer that has an identifier attached for use in {@link LayeredDrawerWrapper}.
+ *
+ * The identifiers in this interface are the vanilla hud layers in the order they are drawn in.
+ * The first layer is drawn first, which means it is at the bottom.
+ * All vanilla layers except {@link #SLEEP} are in sub drawers and have a render condition attached ({@link net.minecraft.client.option.GameOptions#hudHidden}).
+ * Operations relative to any layer will generally inherit that layer's render condition.
+ * There is currently no mechanism to change the render condition of a layer.
+ *
+ *
For common use cases and more details on how this API deals with render condition, see {@link LayeredDrawerWrapper}.
+ */
+public interface IdentifiedLayer extends LayeredDrawer.Layer {
+ /**
+ * The identifier for the vanilla miscellaneous overlays (such as vignette, spyglass, and powder snow) layer.
+ */
+ Identifier MISC_OVERLAYS = Identifier.ofVanilla("misc_overlays");
+ /**
+ * The identifier for the vanilla crosshair layer.
+ */
+ Identifier CROSSHAIR = Identifier.ofVanilla("crosshair");
+ /**
+ * The identifier for the vanilla hotbar, spectator hud, experience bar, and status bars layer.
+ */
+ Identifier HOTBAR_AND_BARS = Identifier.ofVanilla("hotbar_and_bars");
+ /**
+ * The identifier for the vanilla experience level layer.
+ */
+ Identifier EXPERIENCE_LEVEL = Identifier.ofVanilla("experience_level");
+ /**
+ * The identifier for the vanilla status effects layer.
+ */
+ Identifier STATUS_EFFECTS = Identifier.ofVanilla("status_effects");
+ /**
+ * The identifier for the vanilla boss bar layer.
+ */
+ Identifier BOSS_BAR = Identifier.ofVanilla("boss_bar");
+ /**
+ * The identifier for the vanilla sleep overlay layer.
+ */
+ Identifier SLEEP = Identifier.ofVanilla("sleep");
+ /**
+ * The identifier for the vanilla demo timer layer.
+ */
+ Identifier DEMO_TIMER = Identifier.ofVanilla("demo_timer");
+ /**
+ * The identifier for the vanilla debug hud layer.
+ */
+ Identifier DEBUG = Identifier.ofVanilla("debug");
+ /**
+ * The identifier for the vanilla scoreboard layer.
+ */
+ Identifier SCOREBOARD = Identifier.ofVanilla("scoreboard");
+ /**
+ * The identifier for the vanilla overlay message layer.
+ */
+ Identifier OVERLAY_MESSAGE = Identifier.ofVanilla("overlay_message");
+ /**
+ * The identifier for the vanilla title and subtitle layer.
+ *
+ *
Note that this is not the sound subtitles.
+ */
+ Identifier TITLE_AND_SUBTITLE = Identifier.ofVanilla("title_and_subtitle");
+ /**
+ * The identifier for the vanilla chat layer.
+ */
+ Identifier CHAT = Identifier.ofVanilla("chat");
+ /**
+ * The identifier for the vanilla player list layer.
+ */
+ Identifier PLAYER_LIST = Identifier.ofVanilla("player_list");
+ /**
+ * The identifier for the vanilla sound subtitles layer.
+ */
+ Identifier SUBTITLES = Identifier.ofVanilla("subtitles");
+
+ /**
+ * @return the identifier of the layer
+ */
+ Identifier id();
+
+ /**
+ * Wraps a hud layer in an identified layer.
+ *
+ * @param id the identifier to give the layer
+ * @param layer the layer to wrap
+ * @return the identified layer
+ */
+ static IdentifiedLayer of(Identifier id, LayeredDrawer.Layer layer) {
+ return new WrappedLayer(id, layer);
+ }
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/LayeredDrawerWrapper.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/LayeredDrawerWrapper.java
new file mode 100644
index 0000000000..e13b5f7eeb
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/LayeredDrawerWrapper.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.rendering.v1;
+
+import java.util.function.Function;
+
+import org.jetbrains.annotations.Contract;
+
+import net.minecraft.client.gui.LayeredDrawer;
+import net.minecraft.util.Identifier;
+
+/**
+ * A layered drawer that has an identifier attached to each layer and methods to add layers in specific positions.
+ *
+ *
Operations relative to a layer will generally inherit that layer's render condition.
+ * The render condition for all vanilla layers except {@link IdentifiedLayer#SLEEP} is {@link net.minecraft.client.option.GameOptions#hudHidden}.
+ * Only {@link #addLayer(IdentifiedLayer)} will not inherit any render condition.
+ * There is currently no mechanism to change the render condition of a layer.
+ * For vanilla layers, see {@link IdentifiedLayer}.
+ *
+ *
Common places to add layers (as of 1.21.4):
+ *
+ *
+ * Injection Point |
+ * Use Case |
+ *
+ *
+ * Before {@link IdentifiedLayer#MISC_OVERLAYS MISC_OVERLAYS} |
+ * Render before everything |
+ *
+ *
+ * After {@link IdentifiedLayer#MISC_OVERLAYS MISC_OVERLAYS} |
+ * Render after misc overlays (vignette, spyglass, and powder snow) and before the crosshair |
+ *
+ *
+ * After {@link IdentifiedLayer#EXPERIENCE_LEVEL EXPERIENCE_LEVEL} |
+ * Render after most main hud elements like hotbar, spectator hud, status bars, experience bar, status effects overlays, and boss bar and before the sleep overlay |
+ *
+ *
+ * Before {@link IdentifiedLayer#DEMO_TIMER DEMO_TIMER} |
+ * Render after sleep overlay and before the demo timer, debug HUD, scoreboard, overlay message (action bar), and title and subtitle |
+ *
+ *
+ * Before {@link IdentifiedLayer#CHAT CHAT} |
+ * Render after the debug HUD, scoreboard, overlay message (action bar), and title and subtitle and before {@link net.minecraft.client.gui.hud.ChatHud ChatHud}, player list, and sound subtitles |
+ *
+ *
+ * After {@link IdentifiedLayer#SUBTITLES SUBTITLES} |
+ * Render after everything |
+ *
+ *
+ *
+ * @see HudLayerRegistrationCallback
+ */
+public interface LayeredDrawerWrapper {
+ /**
+ * Adds a layer to the end of the layered drawer.
+ *
+ * @param layer the layer to add
+ * @return this layered drawer
+ */
+ @Contract("_ -> this")
+ LayeredDrawerWrapper addLayer(IdentifiedLayer layer);
+
+ /**
+ * Attaches a layer before the layer with the specified identifier.
+ *
+ * The render condition of the layer being attached to, if any, also applies to the new layer.
+ *
+ * @param beforeThis the identifier of the layer to add the new layer before
+ * @param layer the layer to add
+ * @return this layered drawer
+ */
+ @Contract("_, _ -> this")
+ LayeredDrawerWrapper attachLayerBefore(Identifier beforeThis, IdentifiedLayer layer);
+
+ /**
+ * Attaches a layer before the layer with the specified identifier.
+ *
+ *
The render condition of the layer being attached to, if any, also applies to the new layer.
+ *
+ * @param beforeThis the identifier of the layer to add the new layer before
+ * @param identifier the identifier of the new layer
+ * @param layer the layer to add
+ * @return this layered drawer
+ */
+ @Contract("_, _, _ -> this")
+ default LayeredDrawerWrapper attachLayerBefore(Identifier beforeThis, Identifier identifier, LayeredDrawer.Layer layer) {
+ return attachLayerBefore(beforeThis, IdentifiedLayer.of(identifier, layer));
+ }
+
+ /**
+ * Attaches a layer after the layer with the specified identifier.
+ *
+ *
The render condition of the layer being attached to, if any, also applies to the new layer.
+ *
+ * @param afterThis the identifier of the layer to add the new layer after
+ * @param layer the layer to add
+ * @return this layered drawer
+ */
+ @Contract("_, _ -> this")
+ LayeredDrawerWrapper attachLayerAfter(Identifier afterThis, IdentifiedLayer layer);
+
+ /**
+ * Attaches a layer after the layer with the specified identifier.
+ *
+ *
The render condition of the layer being attached to, if any, also applies to the new layer.
+ *
+ * @param afterThis the identifier of the layer to add the new layer after
+ * @param identifier the identifier of the new layer
+ * @param layer the layer to add
+ * @return this layered drawer
+ */
+ @Contract("_, _, _ -> this")
+ default LayeredDrawerWrapper attachLayerAfter(Identifier afterThis, Identifier identifier, LayeredDrawer.Layer layer) {
+ return attachLayerAfter(afterThis, IdentifiedLayer.of(identifier, layer));
+ }
+
+ /**
+ * Removes a layer with the specified identifier.
+ *
+ * @param identifier the identifier of the layer to remove
+ * @return this layered drawer
+ */
+ @Contract("_ -> this")
+ LayeredDrawerWrapper removeLayer(Identifier identifier);
+
+ /**
+ * Replaces a layer with the specified identifier.
+ *
+ *
The render condition of the layer being replaced, if any, also applies to the new layer.
+ *
+ * @param identifier the identifier of the layer to replace
+ * @param replacer a function that takes the old layer and returns the new layer
+ * @return this layered drawer
+ */
+ @Contract("_, _ -> this")
+ LayeredDrawerWrapper replaceLayer(Identifier identifier, Function replacer);
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/LayerInjectionPoint.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/LayerInjectionPoint.java
new file mode 100644
index 0000000000..f3a2570637
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/LayerInjectionPoint.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.rendering;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.InvokeDynamicInsnNode;
+import org.spongepowered.asm.mixin.injection.InjectionPoint;
+import org.spongepowered.asm.mixin.injection.struct.InjectionPointData;
+import org.spongepowered.asm.mixin.injection.struct.MemberInfo;
+
+public final class LayerInjectionPoint extends InjectionPoint {
+ private final MemberInfo target;
+
+ public LayerInjectionPoint(InjectionPointData data) {
+ super(data);
+ this.target = (MemberInfo) data.getTarget();
+ }
+
+ @Override
+ public boolean find(String desc, InsnList insns, Collection nodes) {
+ List targetNodes = new ArrayList<>();
+
+ ListIterator iterator = insns.iterator();
+
+ outer: while (iterator.hasNext()) {
+ AbstractInsnNode insn = iterator.next();
+
+ if (insn.getOpcode() == Opcodes.INVOKEDYNAMIC && matchesInvokeDynamic((InvokeDynamicInsnNode) insn)) {
+ // We have found our target InvokeDynamicInsnNode, now we need to find the next INVOKEVIRTUAL
+
+ while (iterator.hasNext()) {
+ insn = iterator.next();
+
+ if (insn.getOpcode() == Opcodes.INVOKEVIRTUAL) {
+ targetNodes.add(insn);
+ break outer;
+ }
+ }
+ }
+ }
+
+ nodes.addAll(targetNodes);
+ return !targetNodes.isEmpty();
+ }
+
+ private boolean matchesInvokeDynamic(InvokeDynamicInsnNode insnNode) {
+ for (Object bsmArg : insnNode.bsmArgs) {
+ if (bsmArg instanceof Handle handle && matchesHandle(handle)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean matchesHandle(Handle handle) {
+ return handle.getOwner().equals(target.getOwner())
+ && handle.getName().equals(target.getName())
+ && handle.getDesc().equals(target.getDesc());
+ }
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/LayeredDrawerWrapperImpl.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/LayeredDrawerWrapperImpl.java
new file mode 100644
index 0000000000..4b8ea69ac7
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/LayeredDrawerWrapperImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.rendering;
+
+import java.util.List;
+import java.util.ListIterator;
+import java.util.function.Function;
+
+import org.apache.commons.lang3.mutable.MutableBoolean;
+import org.jetbrains.annotations.VisibleForTesting;
+
+import net.minecraft.client.gui.LayeredDrawer;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer;
+import net.fabricmc.fabric.api.client.rendering.v1.LayeredDrawerWrapper;
+import net.fabricmc.fabric.mixin.client.rendering.LayeredDrawerAccessor;
+
+public final class LayeredDrawerWrapperImpl implements LayeredDrawerWrapper {
+ private final LayeredDrawer base;
+
+ public LayeredDrawerWrapperImpl(LayeredDrawer base) {
+ this.base = base;
+ }
+
+ private static List getLayers(LayeredDrawer drawer) {
+ return ((LayeredDrawerAccessor) drawer).getLayers();
+ }
+
+ @Override
+ public LayeredDrawerWrapper addLayer(IdentifiedLayer layer) {
+ validateUnique(layer);
+ getLayers(this.base).add(layer);
+ return this;
+ }
+
+ @Override
+ public LayeredDrawerWrapper attachLayerAfter(Identifier afterThis, IdentifiedLayer layer) {
+ validateUnique(layer);
+
+ boolean didChange = findLayer(afterThis, (l, iterator) -> {
+ iterator.add(layer);
+ return true;
+ });
+
+ if (!didChange) {
+ throw new IllegalArgumentException("Layer with identifier " + afterThis + " not found");
+ }
+
+ return this;
+ }
+
+ @Override
+ public LayeredDrawerWrapper attachLayerBefore(Identifier beforeThis, IdentifiedLayer layer) {
+ validateUnique(layer);
+ boolean didChange = findLayer(beforeThis, (l, iterator) -> {
+ iterator.previous();
+ iterator.add(layer);
+ iterator.next();
+ return true;
+ });
+
+ if (!didChange) {
+ throw new IllegalArgumentException("Layer with identifier " + beforeThis + " not found");
+ }
+
+ return this;
+ }
+
+ @Override
+ public LayeredDrawerWrapper removeLayer(Identifier identifier) {
+ boolean didChange = findLayer(identifier, (l, iterator) -> {
+ iterator.remove();
+ return true;
+ });
+
+ if (!didChange) {
+ throw new IllegalArgumentException("Layer with identifier " + identifier + " not found");
+ }
+
+ return this;
+ }
+
+ @Override
+ public LayeredDrawerWrapper replaceLayer(Identifier identifier, Function replacer) {
+ boolean didChange = findLayer(identifier, (l, iterator) -> {
+ iterator.set(replacer.apply((IdentifiedLayer) l));
+ return true;
+ });
+
+ if (!didChange) {
+ throw new IllegalArgumentException("Layer with identifier " + identifier + " not found");
+ }
+
+ return this;
+ }
+
+ @VisibleForTesting
+ void validateUnique(IdentifiedLayer layer) {
+ visitLayers((l, iterator) -> {
+ if (matchesIdentifier(l, layer.id())) {
+ throw new IllegalArgumentException("Layer with identifier " + layer.id() + " already exists");
+ }
+
+ return false;
+ });
+ }
+
+ /**
+ * @return true if a layer with the given identifier was found
+ */
+ @VisibleForTesting
+ boolean findLayer(Identifier identifier, LayerVisitor visitor) {
+ MutableBoolean found = new MutableBoolean(false);
+
+ visitLayers((l, iterator) -> {
+ if (matchesIdentifier(l, identifier)) {
+ found.setTrue();
+ return visitor.visit(l, iterator);
+ }
+
+ return false;
+ });
+
+ return found.booleanValue();
+ }
+
+ @VisibleForTesting
+ boolean visitLayers(LayerVisitor visitor) {
+ return visitLayers(getLayers(base), visitor);
+ }
+
+ private boolean visitLayers(List layers, LayerVisitor visitor) {
+ MutableBoolean modified = new MutableBoolean(false);
+ ListIterator iterator = layers.listIterator();
+
+ while (iterator.hasNext()) {
+ LayeredDrawer.Layer layer = iterator.next();
+
+ if (visitor.visit(layer, iterator)) {
+ modified.setTrue();
+ continue; // Skip visiting children if the current layer was modified
+ }
+
+ if (layer instanceof SubLayer subLayer) {
+ modified.setValue(visitLayers(getLayers(subLayer.delegate()), visitor));
+ }
+ }
+
+ return modified.booleanValue();
+ }
+
+ private static boolean matchesIdentifier(LayeredDrawer.Layer layer, Identifier identifier) {
+ return layer instanceof IdentifiedLayer il && il.id().equals(identifier);
+ }
+
+ @VisibleForTesting
+ interface LayerVisitor {
+ /**
+ * @return true if the list has been modified, false if not modified
+ */
+ boolean visit(LayeredDrawer.Layer layer, ListIterator iterator);
+ }
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/SubLayer.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/SubLayer.java
new file mode 100644
index 0000000000..8919572e37
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/SubLayer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.rendering;
+
+import java.util.function.BooleanSupplier;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.LayeredDrawer;
+import net.minecraft.client.render.RenderTickCounter;
+
+/**
+ * A layer that wraps another layered drawer that can be added to {@link net.fabricmc.fabric.api.client.rendering.v1.LayeredDrawerWrapper LayeredDrawerWrapper}.
+ *
+ * This wraps the vanilla sub drawers, so we can retrieve sub layers as needed in the layered drawer wrapper.
+ *
+ * @param delegate the layered drawer to wrap
+ * @param shouldRender a boolean supplier that determines if the layer should render
+ */
+public record SubLayer(LayeredDrawer delegate, BooleanSupplier shouldRender) implements LayeredDrawer.Layer {
+ @Override
+ public void render(DrawContext context, RenderTickCounter tickCounter) {
+ if (shouldRender.getAsBoolean()) {
+ delegate.render(context, tickCounter);
+ }
+ }
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/WrappedLayer.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/WrappedLayer.java
new file mode 100644
index 0000000000..dc6ab244db
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/impl/client/rendering/WrappedLayer.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.rendering;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.LayeredDrawer;
+import net.minecraft.client.render.RenderTickCounter;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer;
+
+/**
+ * A simple layer that wraps a {@link LayeredDrawer.Layer} that can be added to {@link net.fabricmc.fabric.api.client.rendering.v1.LayeredDrawerWrapper LayeredDrawerWrapper}.
+ *
+ * @param id the identifier of the layer
+ * @param layer the layer to wrap
+ */
+public record WrappedLayer(Identifier id, LayeredDrawer.Layer layer) implements IdentifiedLayer {
+ @Override
+ public void render(DrawContext context, RenderTickCounter tickCounter) {
+ layer.render(context, tickCounter);
+ }
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/InGameHudMixin.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/InGameHudMixin.java
index 61f48b3ca2..0b0c31f18b 100644
--- a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/InGameHudMixin.java
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/InGameHudMixin.java
@@ -16,21 +16,182 @@
package net.fabricmc.fabric.mixin.client.rendering;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.BOSS_BAR;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.CHAT;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.CROSSHAIR;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.DEBUG;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.DEMO_TIMER;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.EXPERIENCE_LEVEL;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.HOTBAR_AND_BARS;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.MISC_OVERLAYS;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.OVERLAY_MESSAGE;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.PLAYER_LIST;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.SCOREBOARD;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.SLEEP;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.STATUS_EFFECTS;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.SUBTITLES;
+import static net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer.TITLE_AND_SUBTITLE;
+
+import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.LayeredDrawer;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
+import net.minecraft.util.Identifier;
+import net.fabricmc.fabric.api.client.rendering.v1.HudLayerRegistrationCallback;
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
+import net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer;
+import net.fabricmc.fabric.impl.client.rendering.LayeredDrawerWrapperImpl;
@Mixin(InGameHud.class)
public class InGameHudMixin {
+ @Shadow
+ @Final
+ private LayeredDrawer layeredDrawer;
+
@Inject(method = "render", at = @At(value = "TAIL"))
public void render(DrawContext drawContext, RenderTickCounter tickCounter, CallbackInfo callbackInfo) {
HudRenderCallback.EVENT.invoker().onHudRender(drawContext, tickCounter);
}
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderMiscOverlays(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapMiscOverlays(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(MISC_OVERLAYS, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderCrosshair(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapCrosshair(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(CROSSHAIR, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderMainHud(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapHotbarAndBars(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(HOTBAR_AND_BARS, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderExperienceLevel(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapExperienceLevel(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(EXPERIENCE_LEVEL, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderStatusEffectOverlay(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapStatusEffects(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(STATUS_EFFECTS, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;method_55808(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapBossBar(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(BOSS_BAR, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderDemoTimer(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapDemoTimer(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(DEMO_TIMER, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;method_55807(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapDebug(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(DEBUG, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderScoreboardSidebar(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapScoreboard(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(SCOREBOARD, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderOverlayMessage(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapOverlayMessage(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(OVERLAY_MESSAGE, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderTitleAndSubtitle(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapTitleAndSubtitle(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(TITLE_AND_SUBTITLE, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderChat(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapChat(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(CHAT, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderPlayerList(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapPlayerList(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(PLAYER_LIST, instance, layer);
+ }
+
+ @Redirect(method = "", at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;method_55806(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V")
+ )
+ private LayeredDrawer wrapSubtitlesHud(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(SUBTITLES, instance, layer);
+ }
+
+ @Redirect(method = "",
+ at = @At(
+ value = "net.fabricmc.fabric.impl.client.rendering.LayerInjectionPoint",
+ target = "Lnet/minecraft/client/gui/hud/InGameHud;renderSleepOverlay(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V"
+ )
+ )
+ private LayeredDrawer wrapSleepOverlay(LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return wrap(SLEEP, instance, layer);
+ }
+
+ @Inject(method = "", at = @At("RETURN"))
+ private void registerLayers(CallbackInfo ci) {
+ HudLayerRegistrationCallback.EVENT.invoker().register(new LayeredDrawerWrapperImpl(layeredDrawer));
+ }
+
+ @Unique
+ private static LayeredDrawer wrap(Identifier identifier, LayeredDrawer instance, LayeredDrawer.Layer layer) {
+ return instance.addLayer(IdentifiedLayer.of(identifier, layer));
+ }
}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/LayeredDrawerAccessor.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/LayeredDrawerAccessor.java
new file mode 100644
index 0000000000..36f6111fec
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/LayeredDrawerAccessor.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.rendering;
+
+import java.util.List;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.gui.LayeredDrawer;
+
+@Mixin(LayeredDrawer.class)
+public interface LayeredDrawerAccessor {
+ @Accessor
+ List getLayers();
+}
diff --git a/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/LayeredDrawerMixin.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/LayeredDrawerMixin.java
new file mode 100644
index 0000000000..791b69a9fc
--- /dev/null
+++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/mixin/client/rendering/LayeredDrawerMixin.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.rendering;
+
+import java.util.function.BooleanSupplier;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import net.minecraft.client.gui.LayeredDrawer;
+
+import net.fabricmc.fabric.impl.client.rendering.SubLayer;
+
+@Mixin(LayeredDrawer.class)
+public abstract class LayeredDrawerMixin {
+ @Shadow
+ public abstract LayeredDrawer addLayer(LayeredDrawer.Layer layer);
+
+ @Inject(method = "addSubDrawer", at = @At("HEAD"), cancellable = true)
+ private void wrapSubDrawer(LayeredDrawer drawer, BooleanSupplier shouldRender, CallbackInfoReturnable cir) {
+ addLayer(new SubLayer(drawer, shouldRender));
+ cir.setReturnValue((LayeredDrawer) (Object) this);
+ }
+}
diff --git a/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json b/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json
index e0e8f9c415..d6c01f6853 100644
--- a/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json
+++ b/fabric-rendering-v1/src/client/resources/fabric-rendering-v1.mixins.json
@@ -14,6 +14,8 @@
"EntityModelsMixin",
"EntityRenderersMixin",
"InGameHudMixin",
+ "LayeredDrawerAccessor",
+ "LayeredDrawerMixin",
"LivingEntityRendererAccessor",
"SpecialModelTypesMixin",
"TooltipComponentMixin",
diff --git a/fabric-rendering-v1/src/test/java/net/fabricmc/fabric/impl/client/rendering/LayeredDrawerWrapperTest.java b/fabric-rendering-v1/src/test/java/net/fabricmc/fabric/impl/client/rendering/LayeredDrawerWrapperTest.java
new file mode 100644
index 0000000000..3998de4c38
--- /dev/null
+++ b/fabric-rendering-v1/src/test/java/net/fabricmc/fabric/impl/client/rendering/LayeredDrawerWrapperTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.rendering;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.LayeredDrawer;
+import net.minecraft.client.render.RenderTickCounter;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer;
+
+public class LayeredDrawerWrapperTest {
+ private List drawnLayers;
+ private LayeredDrawer base;
+ private LayeredDrawerWrapperImpl layers;
+
+ @BeforeEach
+ void setUp() {
+ drawnLayers = new ArrayList<>();
+ base = new LayeredDrawer();
+ layers = new LayeredDrawerWrapperImpl(base);
+ }
+
+ @Test
+ void addLayer() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3"));
+
+ assertOrder(base, List.of("layer1", "layer2", "layer3"));
+ }
+
+ @Test
+ void addBefore() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"));
+
+ layers.attachLayerBefore(testIdentifier("layer1"), testLayer("before1"));
+
+ assertOrder(base, List.of("before1", "layer1", "layer2"));
+ }
+
+ @Test
+ void addAfter() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"));
+
+ layers.attachLayerAfter(testIdentifier("layer1"), testLayer("after1"));
+
+ assertOrder(base, List.of("layer1", "after1", "layer2"));
+ }
+
+ @Test
+ void removeLayer() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3"))
+ .addLayer(testLayer("layer4"));
+
+ layers.removeLayer(testIdentifier("layer2"))
+ .removeLayer(testIdentifier("layer4"));
+
+ assertOrder(base, List.of("layer1", "layer3"));
+ }
+
+ @Test
+ void replaceLayer() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3"));
+
+ layers.replaceLayer(testIdentifier("layer2"), layer -> testLayer("temp"))
+ .replaceLayer(testIdentifier("temp"), layer -> testLayer("replaced"));
+
+ assertOrder(base, List.of("layer1", "replaced", "layer3"));
+ }
+
+ @Test
+ void validateUnique() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3"));
+
+ Assertions.assertDoesNotThrow(() -> layers.validateUnique(testLayer("layer4")));
+ Assertions.assertThrows(IllegalArgumentException.class, () -> layers.validateUnique(testLayer("layer2")));
+ }
+
+ @Test
+ void findLayer() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3"));
+
+ Assertions.assertTrue(layers.findLayer(testIdentifier("layer2"), (layer, iterator) -> {
+ iterator.add(testLayer("found"));
+ return true;
+ }));
+
+ assertOrder(base, List.of("layer1", "layer2", "found", "layer3"));
+ }
+
+ @Test
+ void visitLayers() {
+ layers.addLayer(testLayer("layer1"))
+ .addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3"));
+
+ Assertions.assertTrue(layers.visitLayers((layer, iterator) -> {
+ String name = ((IdentifiedLayer) layer).id().getPath();
+ iterator.add(testLayer("visited" + name.substring(name.length() - 1)));
+ return true;
+ }));
+
+ assertOrder(base, List.of("layer1", "visited1", "layer2", "visited2", "layer3", "visited3"));
+ }
+
+ @Test
+ void replaceSubLayer() {
+ layers.addLayer(testLayer("layer1"));
+ base.addLayer(new SubLayer(
+ new LayeredDrawer().addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3")),
+ () -> true
+ ));
+ layers.addLayer(testLayer("layer4"));
+
+ layers.replaceLayer(testIdentifier("layer2"), layer -> testLayer("replaced"));
+
+ assertOrder(base, List.of("layer1", "replaced", "layer3", "layer4"));
+ }
+
+ @Test
+ void visitSubLayers() {
+ layers.addLayer(testLayer("layer1"));
+ base.addLayer(new SubLayer(
+ new LayeredDrawer().addLayer(testLayer("layer2"))
+ .addLayer(testLayer("layer3")),
+ () -> true
+ ));
+ layers.addLayer(testLayer("layer4"));
+
+ // Return true when we encounter layer3, which is in a sub drawer
+ // Even though it's not modified. This is just for testing.
+ Assertions.assertTrue(layers.visitLayers((layer, iterator) -> layer instanceof IdentifiedLayer il && il.id().equals(testIdentifier("layer3"))));
+
+ assertOrder(base, List.of("layer1", "layer2", "layer3", "layer4"));
+ }
+
+ private IdentifiedLayer testLayer(String name) {
+ return IdentifiedLayer.of(testIdentifier(name), (context, tickCounter) -> drawnLayers.add(name));
+ }
+
+ private Identifier testIdentifier(String name) {
+ return Identifier.of("test", name);
+ }
+
+ private void assertOrder(LayeredDrawer drawer, List expectedLayers) {
+ DrawContext drawContext = mock(DrawContext.class);
+ RenderTickCounter tickCounter = mock(RenderTickCounter.class);
+ MatrixStack matrixStack = mock(MatrixStack.class);
+
+ when(drawContext.getMatrices()).thenReturn(matrixStack);
+
+ drawnLayers.clear();
+ drawer.render(drawContext, tickCounter);
+ assertEquals(drawnLayers, expectedLayers);
+ }
+}
diff --git a/fabric-rendering-v1/src/testmod/resources/fabric.mod.json b/fabric-rendering-v1/src/testmod/resources/fabric.mod.json
index 1c57a5f298..be823f1e4c 100644
--- a/fabric-rendering-v1/src/testmod/resources/fabric.mod.json
+++ b/fabric-rendering-v1/src/testmod/resources/fabric.mod.json
@@ -18,9 +18,13 @@
"net.fabricmc.fabric.test.rendering.client.DimensionalRenderingTest",
"net.fabricmc.fabric.test.rendering.client.FeatureRendererTest",
"net.fabricmc.fabric.test.rendering.client.HudAndShaderTest",
+ "net.fabricmc.fabric.test.rendering.client.HudLayerTests",
"net.fabricmc.fabric.test.rendering.client.SpecialBlockRendererTest",
"net.fabricmc.fabric.test.rendering.client.TooltipComponentTests",
"net.fabricmc.fabric.test.rendering.client.WorldRenderEventsTests"
+ ],
+ "fabric-client-gametest": [
+ "net.fabricmc.fabric.test.rendering.client.HudLayerTests"
]
}
}
diff --git a/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/HudLayerTests.java b/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/HudLayerTests.java
new file mode 100644
index 0000000000..ad59542c46
--- /dev/null
+++ b/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/HudLayerTests.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.rendering.client;
+
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.render.RenderTickCounter;
+import net.minecraft.client.util.InputUtil;
+import net.minecraft.text.Text;
+import net.minecraft.util.Colors;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.BlockPos;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
+import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.context.TestSingleplayerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.screenshot.TestScreenshotComparisonOptions;
+import net.fabricmc.fabric.api.client.rendering.v1.HudLayerRegistrationCallback;
+import net.fabricmc.fabric.api.client.rendering.v1.IdentifiedLayer;
+
+public class HudLayerTests implements ClientModInitializer, FabricClientGameTest {
+ private static final String MOD_ID = "fabric";
+ private static final String BEFORE_MISC_OVERLAY = "test_before_misc_overlay";
+ private static final String AFTER_MISC_OVERLAY = "test_after_misc_overlay";
+ private static final String AFTER_EXPERIENCE_LEVEL = "test_after_experience_level";
+ private static final String BEFORE_DEMO_TIMER = "test_before_demo_timer";
+ private static final String BEFORE_CHAT = "test_before_chat";
+ private static final String AFTER_SUBTITLES = "test_after_subtitles";
+ private static boolean shouldRender = false;
+
+ @Override
+ public void onInitializeClient() {
+ HudLayerRegistrationCallback.EVENT.register(layeredDrawer -> layeredDrawer
+ .attachLayerBefore(IdentifiedLayer.MISC_OVERLAYS, Identifier.of(MOD_ID, BEFORE_MISC_OVERLAY), HudLayerTests::renderBeforeMiscOverlay)
+ .attachLayerAfter(IdentifiedLayer.MISC_OVERLAYS, Identifier.of(MOD_ID, AFTER_MISC_OVERLAY), HudLayerTests::renderAfterMiscOverlay)
+ .attachLayerAfter(IdentifiedLayer.EXPERIENCE_LEVEL, Identifier.of(MOD_ID, AFTER_EXPERIENCE_LEVEL), HudLayerTests::renderAfterExperienceLevel)
+ .attachLayerBefore(IdentifiedLayer.DEMO_TIMER, Identifier.of(MOD_ID, BEFORE_DEMO_TIMER), HudLayerTests::renderBeforeDemoTimer)
+ .attachLayerBefore(IdentifiedLayer.CHAT, Identifier.of(MOD_ID, BEFORE_CHAT), HudLayerTests::renderBeforeChat)
+ .attachLayerAfter(IdentifiedLayer.SUBTITLES, Identifier.of(MOD_ID, AFTER_SUBTITLES), HudLayerTests::renderAfterSubtitles)
+ );
+ }
+
+ private static void renderBeforeMiscOverlay(DrawContext context, RenderTickCounter tickCounter) {
+ if (!shouldRender) return;
+ // Render a blue rectangle at the top right of the screen, and it should be blocked by misc overlays such as vignette, spyglass, and powder snow
+ context.fill(context.getScaledWindowWidth() - 200, 0, context.getScaledWindowWidth(), 30, Colors.BLUE);
+ context.drawTextWithShadow(MinecraftClient.getInstance().textRenderer, "1. Blue rectangle blocked by overlays", context.getScaledWindowWidth() - 196, 10, Colors.WHITE);
+ context.drawTextWithShadow(MinecraftClient.getInstance().textRenderer, "such as powder snow", context.getScaledWindowWidth() - 111, 20, Colors.WHITE);
+ }
+
+ private static void renderAfterMiscOverlay(DrawContext context, RenderTickCounter tickCounter) {
+ if (!shouldRender) return;
+ // Render a red square in the center of the screen underneath the crosshair
+ context.fill(context.getScaledWindowWidth() / 2 - 10, context.getScaledWindowHeight() / 2 - 10, context.getScaledWindowWidth() / 2 + 10, context.getScaledWindowHeight() / 2 + 10, Colors.RED);
+ context.drawCenteredTextWithShadow(MinecraftClient.getInstance().textRenderer, "2. Red square underneath crosshair", context.getScaledWindowWidth() / 2, context.getScaledWindowHeight() / 2 + 10, Colors.WHITE);
+ }
+
+ private static void renderAfterExperienceLevel(DrawContext context, RenderTickCounter tickCounter) {
+ if (!shouldRender) return;
+ // Render a green rectangle at the bottom of the screen, and it should block the hotbar and status bars
+ context.fill(context.getScaledWindowWidth() / 2 - 50, context.getScaledWindowHeight() - 50, context.getScaledWindowWidth() / 2 + 50, context.getScaledWindowHeight() - 10, Colors.GREEN);
+ context.drawCenteredTextWithShadow(MinecraftClient.getInstance().textRenderer, "3. This green rectangle should block the hotbar and status bars.", context.getScaledWindowWidth() / 2, context.getScaledWindowHeight() - 40, Colors.WHITE);
+ }
+
+ private static void renderBeforeDemoTimer(DrawContext context, RenderTickCounter tickCounter) {
+ if (!shouldRender) return;
+ // Render a yellow rectangle at the right of the screen, and it should be above the sleep overlay but below the scoreboard
+ context.fill(context.getScaledWindowWidth() - 240, context.getScaledWindowHeight() / 2 - 10, context.getScaledWindowWidth(), context.getScaledWindowHeight() / 2 + 10, Colors.YELLOW);
+ context.drawTextWithShadow(MinecraftClient.getInstance().textRenderer, "4. This yellow rectangle should be above", context.getScaledWindowWidth() - 236, context.getScaledWindowHeight() / 2 - 10, Colors.WHITE);
+ context.drawTextWithShadow(MinecraftClient.getInstance().textRenderer, "the sleep overlay but below the scoreboard.", context.getScaledWindowWidth() - 236, context.getScaledWindowHeight() / 2, Colors.WHITE);
+ }
+
+ private static void renderBeforeChat(DrawContext context, RenderTickCounter tickCounter) {
+ if (!shouldRender) return;
+ // Render a blue rectangle at the bottom left of the screen, and it should be blocked by the chat
+ context.fill(0, context.getScaledWindowHeight() - 40, 300, context.getScaledWindowHeight() - 50, Colors.BLUE);
+ context.drawTextWithShadow(MinecraftClient.getInstance().textRenderer, "5. This blue rectangle should be blocked by the chat.", 0, context.getScaledWindowHeight() - 50, Colors.WHITE);
+ }
+
+ private static void renderAfterSubtitles(DrawContext context, RenderTickCounter tickCounter) {
+ if (!shouldRender) return;
+ // Render a yellow rectangle at the top of the screen, and it should block the player list
+ context.fill(context.getScaledWindowWidth() / 2 - 150, 0, context.getScaledWindowWidth() / 2 + 150, 15, Colors.YELLOW);
+ context.drawCenteredTextWithShadow(MinecraftClient.getInstance().textRenderer, "6. This yellow rectangle should block the player list.", context.getScaledWindowWidth() / 2, 0, Colors.WHITE);
+ }
+
+ @Override
+ public void runTest(ClientGameTestContext context) {
+ // Set up required test environment
+ context.getInput().resizeWindow(1708, 960); // Twice the default dimensions
+ context.runOnClient(client -> {
+ client.options.hudHidden = false;
+ client.options.getGuiScale().setValue(2);
+ });
+ shouldRender = true;
+
+ try (TestSingleplayerContext singleplayer = context.worldBuilder().create()) {
+ // Set up the test world
+ singleplayer.getServer().runCommand("/tp @a 0 -60 0");
+ singleplayer.getServer().runCommand("/scoreboard objectives add hud_layer_test dummy");
+ singleplayer.getServer().runCommand("/scoreboard objectives setdisplay list hud_layer_test"); // Hack to show player list
+ singleplayer.getServer().runCommand("/scoreboard objectives setdisplay sidebar hud_layer_test"); // Hack to show sidebar
+ singleplayer.getServer().runOnServer(server -> server.getOverworld().setBlockState(new BlockPos(0, -59, 0), Blocks.POWDER_SNOW.getDefaultState()));
+
+ // Wait for stuff to load
+ singleplayer.getClientWorld().waitForChunksRender();
+ singleplayer.getServer().runOnServer(server -> server.getPlayerManager().broadcast(Text.of("hud_layer_" + BEFORE_CHAT), false)); // Chat messages disappear in 200 ticks so we send one 150 ticks in advance to test the before chat layer
+ context.waitTicks(150); // The powder snow frosty vignette takes 140 ticks to fully appear, so we additionally wait for a total of 150 ticks
+
+ // Take and assert screenshots
+ context.assertScreenshotEquals(TestScreenshotComparisonOptions.of("hud_layer_" + BEFORE_MISC_OVERLAY).withRegion(1308, 0, 400, 60).save());
+ context.assertScreenshotEquals(TestScreenshotComparisonOptions.of("hud_layer_" + AFTER_MISC_OVERLAY).withRegion(668, 460, 372, 56).save());
+ context.assertScreenshotEquals(TestScreenshotComparisonOptions.of("hud_layer_" + AFTER_EXPERIENCE_LEVEL).withRegion(754, 860, 200, 80).save());
+
+ // The sleep overlay takes 100 ticks to fully appear, so we start sleeping and wait for 100 ticks
+ context.runOnClient(client -> client.player.setSleepingPosition(new BlockPos(0, -59, 0)));
+ context.waitTicks(100);
+
+ context.assertScreenshotEquals(TestScreenshotComparisonOptions.of("hud_layer_" + BEFORE_DEMO_TIMER).withRegion(1228, 460, 480, 40).save());
+ context.assertScreenshotEquals(TestScreenshotComparisonOptions.of("hud_layer_" + BEFORE_CHAT).withRegion(0, 860, 600, 20).save());
+
+ context.runOnClient(client -> client.player.clearSleepingPosition());
+ context.waitTick();
+ context.getInput().holdKey(InputUtil.GLFW_KEY_TAB); // Show player list
+ context.waitTick();
+ context.assertScreenshotEquals(TestScreenshotComparisonOptions.of("hud_layer_" + AFTER_SUBTITLES).withRegion(554, 0, 600, 30).save());
+ }
+
+ shouldRender = false;
+ }
+}
diff --git a/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_experience_level.png b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_experience_level.png
new file mode 100644
index 0000000000..e39b4770c9
Binary files /dev/null and b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_experience_level.png differ
diff --git a/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_misc_overlay.png b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_misc_overlay.png
new file mode 100644
index 0000000000..070826ae75
Binary files /dev/null and b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_misc_overlay.png differ
diff --git a/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_subtitles.png b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_subtitles.png
new file mode 100644
index 0000000000..89cf78e07a
Binary files /dev/null and b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_after_subtitles.png differ
diff --git a/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_chat.png b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_chat.png
new file mode 100644
index 0000000000..5053e1f941
Binary files /dev/null and b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_chat.png differ
diff --git a/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_demo_timer.png b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_demo_timer.png
new file mode 100644
index 0000000000..8e4d0acf34
Binary files /dev/null and b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_demo_timer.png differ
diff --git a/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_misc_overlay.png b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_misc_overlay.png
new file mode 100644
index 0000000000..c6e4b87a23
Binary files /dev/null and b/fabric-rendering-v1/src/testmodClient/resources/templates/hud_layer_test_before_misc_overlay.png differ