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..f73909b6e4 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 HudRenderEvents} instead. For the closest equivalent, use {@link HudRenderEvents#LAST}. + */ +@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/HudRenderEvents.java b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudRenderEvents.java new file mode 100644 index 0000000000..d405274c4f --- /dev/null +++ b/fabric-rendering-v1/src/client/java/net/fabricmc/fabric/api/client/rendering/v1/HudRenderEvents.java @@ -0,0 +1,69 @@ +/* + * 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.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +/** + * Events for rendering elements on the HUD. + * + *

These events will not be called if the HUD is hidden with F1. + */ +public final class HudRenderEvents { + /** + * Called at the start of HUD rendering, right before anything is rendered. + */ + public static final Event START = createEventForStage(); + + /** + * Called after misc overlays (vignette, spyglass, and powder snow) have been rendered, and before the crosshair is rendered. + */ + public static final Event AFTER_MISC_OVERLAYS = createEventForStage(); + + /** + * Called after the hotbar, status bars, experience bar, status effects overlays, and boss bar have been rendered, and before the sleep overlay is rendered. + */ + public static final Event AFTER_MAIN_HUD = createEventForStage(); + + /** + * Called after the sleep overlay has been rendered, and before the demo timer, debug HUD, scoreboard, overlay message (action bar), and title and subtitle are rendered. + */ + public static final Event AFTER_SLEEP_OVERLAY = createEventForStage(); + + /** + * Called after the debug HUD, scoreboard, overlay message (action bar), and title and subtitle have been rendered, and before the {@link net.minecraft.client.gui.hud.ChatHud} is rendered. + */ + public static final Event BEFORE_CHAT = createEventForStage(); + + /** + * Called after the entire HUD is rendered. + */ + public static final Event LAST = createEventForStage(); + + private HudRenderEvents() { } + + private static Event createEventForStage() { + return EventFactory.createArrayBacked(LayeredDrawer.Layer.class, listeners -> (context, tickCounter) -> { + for (LayeredDrawer.Layer listener : listeners) { + listener.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..df8bc84ece 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 @@ -19,16 +19,68 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.Slice; 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.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderEvents; @Mixin(InGameHud.class) public class InGameHudMixin { + // Targeting the first addLayer call of the first layered drawer, currently the misc overlays layer (renderMiscOverlays) as of 1.21. + @ModifyArg(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/LayeredDrawer;addLayer(Lnet/minecraft/client/gui/LayeredDrawer$Layer;)Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 0)) + private LayeredDrawer.Layer fabric$beforeStartAndAfterMiscOverlays(LayeredDrawer.Layer miscOverlaysLayer) { + return (context, tickCounter) -> { + HudRenderEvents.START.invoker().render(context, tickCounter); + miscOverlaysLayer.render(context, tickCounter); + HudRenderEvents.AFTER_MISC_OVERLAYS.invoker().render(context, tickCounter); + }; + } + + // Targeting the last addLayer call of the first layered drawer, which is after the main hud, currently the boss bar layer (bossBarHud.render) as of 1.21. + @ModifyArg(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/LayeredDrawer;addLayer(Lnet/minecraft/client/gui/LayeredDrawer$Layer;)Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 5)) + private LayeredDrawer.Layer fabric$afterMainHudExperienceLevelStatusEffectOverlayAndBossBar(LayeredDrawer.Layer experienceLevelLayer) { + return (context, tickCounter) -> { + experienceLevelLayer.render(context, tickCounter); + HudRenderEvents.AFTER_MAIN_HUD.invoker().render(context, tickCounter); + }; + } + + // Targeting the first addLayer call of the second layered drawer, currently the demo timer layer (renderDemoTimer) as of 1.21. + @ModifyArg(method = "", slice = @Slice(from = @At(value = "NEW", target = "Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 2)), at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/LayeredDrawer;addLayer(Lnet/minecraft/client/gui/LayeredDrawer$Layer;)Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 0)) + private LayeredDrawer.Layer fabric$afterSleepOverlay(LayeredDrawer.Layer demoTimerLayer) { + return (context, tickCounter) -> { + HudRenderEvents.AFTER_SLEEP_OVERLAY.invoker().render(context, tickCounter); + demoTimerLayer.render(context, tickCounter); + }; + } + + // Targeting the chat layer (renderChat), currently the sixth addLayer call of the second layered drawer as of 1.21. + @ModifyArg(method = "", slice = @Slice(from = @At(value = "NEW", target = "Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 2)), at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/LayeredDrawer;addLayer(Lnet/minecraft/client/gui/LayeredDrawer$Layer;)Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 5)) + private LayeredDrawer.Layer fabric$beforeChat(LayeredDrawer.Layer beforeChatLayer) { + return (context, tickCounter) -> { + HudRenderEvents.BEFORE_CHAT.invoker().render(context, tickCounter); + beforeChatLayer.render(context, tickCounter); + }; + } + + // Targeting the last addLayer call of the second layered drawer, currently the subtitles hud layer (subtitlesHud.render) as of 1.21. + @ModifyArg(method = "", slice = @Slice(from = @At(value = "NEW", target = "Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 2)), at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/LayeredDrawer;addLayer(Lnet/minecraft/client/gui/LayeredDrawer$Layer;)Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 7)) + private LayeredDrawer.Layer fabric$afterSubtitlesHud(LayeredDrawer.Layer subtitlesHudLayer) { + return (context, tickCounter) -> { + subtitlesHudLayer.render(context, tickCounter); + HudRenderEvents.LAST.invoker().render(context, tickCounter); + }; + } + + // Inject after the HUD is rendered. Deprecated in favor of HudRenderEvents. + @Deprecated @Inject(method = "render", at = @At(value = "TAIL")) public void render(DrawContext drawContext, RenderTickCounter tickCounter, CallbackInfo callbackInfo) { HudRenderCallback.EVENT.invoker().onHudRender(drawContext, tickCounter); diff --git a/fabric-rendering-v1/src/testmod/resources/fabric.mod.json b/fabric-rendering-v1/src/testmod/resources/fabric.mod.json index 57107b39f9..a24fc8630b 100644 --- a/fabric-rendering-v1/src/testmod/resources/fabric.mod.json +++ b/fabric-rendering-v1/src/testmod/resources/fabric.mod.json @@ -18,6 +18,7 @@ "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.HudRenderEventsTests", "net.fabricmc.fabric.test.rendering.client.TooltipComponentTests", "net.fabricmc.fabric.test.rendering.client.WorldRenderEventsTests" ] diff --git a/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/HudRenderEventsTests.java b/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/HudRenderEventsTests.java new file mode 100644 index 0000000000..508e97505a --- /dev/null +++ b/fabric-rendering-v1/src/testmodClient/java/net/fabricmc/fabric/test/rendering/client/HudRenderEventsTests.java @@ -0,0 +1,61 @@ +/* + * 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.client.MinecraftClient; +import net.minecraft.util.Colors; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderEvents; + +public class HudRenderEventsTests implements ClientModInitializer { + @Override + public void onInitializeClient() { + // 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 + HudRenderEvents.START.register((context, tickCounter) -> { + 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); + }); + // Render a red square in the center of the screen underneath the crosshair + HudRenderEvents.AFTER_MISC_OVERLAYS.register((context, tickCounter) -> { + 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); + }); + // Render a green rectangle at the bottom of the screen, and it should block the hotbar and status bars + HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> { + 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); + }); + // Render a yellow rectangle at the right of the screen, and it should be above the sleep overlay but below the scoreboard + HudRenderEvents.AFTER_SLEEP_OVERLAY.register((context, tickCounter) -> { + 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); + }); + // Render a blue rectangle at the bottom left of the screen, and it should be blocked by the chat + HudRenderEvents.BEFORE_CHAT.register((context, tickCounter) -> { + 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); + }); + // Render a yellow rectangle at the top of the screen, and it should block the player list + HudRenderEvents.LAST.register((context, tickCounter) -> { + 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); + }); + } +}