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 PointUse 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