Add Hud Render Events ()

* Add HudRenderEvents

* Add HudRenderEventsTests and deprecate HudRenderCallback

* Update tests

* Add client parameter and apply suggestions

* Split HudRenderEvents into separate interfaces

* Fix before chat and last

* Add after sleep overlay event and update after main hud injection point

* Add comments for injection points

* Revert splitting HudRenderEvents into separate interfaces

* Use vanilla layered drawer layer interface

* Cleanup InGameHudMixin

* POC of hud modification

* Implement HudLayerRegistrationCallback

* Delete HudRenderEvents

* Fix sub drawers and add basic documentation

* Fix checkstyle

* Apply suggestions from code review

* Add Javadocs

* Add more unit tests

* Apply suggestions from code review
- Update Javadocs
- Remove vanilla sub drawer flattening
- Improve LayeredDrawerWrapperImpl internals

* Javadoc oddities

* Add client gametests

* Finish client gametests

* Change method and add documentation

* Ensure test environment is correct

* Move test class to same package

* Add render condition for tests

* Fix merge conflicts

* Update javadocs

* Small bug fixes, documentation, and sub drawer tests

* Update javadocs some more

* Add contract and get around return value not used warnings

* Apply suggestions from code review

Co-authored-by: Juuz <6596629+Juuxel@users.noreply.github.com>

* Migrate AtomicBoolean to MutableBoolean

* Update javadocs on render condition

* Use ListIterator#set

---------

Co-authored-by: modmuss50 <modmuss50@gmail.com>
Co-authored-by: Juuz <6596629+Juuxel@users.noreply.github.com>
(cherry picked from commit 44a0820dd2)
This commit is contained in:
Kevin 2025-02-09 21:26:16 +08:00 committed by modmuss50
parent 19c9635598
commit a3630f92b3
24 changed files with 1269 additions and 2 deletions

View file

@ -69,6 +69,17 @@ public interface TestScreenshotComparisonOptions extends TestScreenshotCommonOpt
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

View file

@ -50,6 +50,11 @@ public final class TestScreenshotComparisonOptionsImpl extends TestScreenshotCom
this.templateImage = Either.right(templateImage);
}
@Override
public TestScreenshotComparisonOptions save() {
return saveWithFileName(getTemplateImagePath());
}
@Override
public TestScreenshotComparisonOptions saveWithFileName(String fileName) {
Preconditions.checkNotNull(fileName, "fileName");

View file

@ -6,5 +6,6 @@ moduleDependencies(project, [
])
testDependencies(project, [
':fabric-client-gametest-api-v1',
':fabric-object-builder-api-v1'
])

View file

@ -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.
*
* <p>To register a layer, register a listener to this event and register your layers in the listener.
* For common use cases, see {@link LayeredDrawerWrapper}.
*
* <p>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<HudLayerRegistrationCallback> 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);
}

View file

@ -22,10 +22,14 @@ import net.minecraft.client.render.RenderTickCounter;
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<HudRenderCallback> EVENT = EventFactory.createArrayBacked(HudRenderCallback.class, (listeners) -> (matrixStack, delta) -> {
Event<HudRenderCallback> EVENT = EventFactory.createArrayBacked(HudRenderCallback.class, (listeners) -> (context, tickCounter) -> {
for (HudRenderCallback event : listeners) {
event.onHudRender(matrixStack, delta);
event.onHudRender(context, tickCounter);
}
});

View file

@ -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}.
*
* <p>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.
*
* <p>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.
*
* <p>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);
}
}

View file

@ -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.
*
* <p>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}.
*
* <p>Common places to add layers (as of 1.21.4):
* <table>
* <tr>
* <th>Injection Point</th>
* <th>Use Case</th>
* </tr>
* <tr>
* <td>Before {@link IdentifiedLayer#MISC_OVERLAYS MISC_OVERLAYS}</td>
* <td>Render before everything</td>
* </tr>
* <tr>
* <td>After {@link IdentifiedLayer#MISC_OVERLAYS MISC_OVERLAYS}</td>
* <td>Render after misc overlays (vignette, spyglass, and powder snow) and before the crosshair</td>
* </tr>
* <tr>
* <td>After {@link IdentifiedLayer#EXPERIENCE_LEVEL EXPERIENCE_LEVEL}</td>
* <td>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</td>
* </tr>
* <tr>
* <td>Before {@link IdentifiedLayer#DEMO_TIMER DEMO_TIMER}</td>
* <td>Render after sleep overlay and before the demo timer, debug HUD, scoreboard, overlay message (action bar), and title and subtitle</td>
* </tr>
* <tr>
* <td>Before {@link IdentifiedLayer#CHAT CHAT}</td>
* <td>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</td>
* </tr>
* <tr>
* <td>After {@link IdentifiedLayer#SUBTITLES SUBTITLES}</td>
* <td>Render after everything</td>
* </tr>
* </table>
*
* @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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<IdentifiedLayer, IdentifiedLayer> replacer);
}

View file

@ -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<AbstractInsnNode> nodes) {
List<AbstractInsnNode> targetNodes = new ArrayList<>();
ListIterator<AbstractInsnNode> 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());
}
}

View file

@ -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<LayeredDrawer.Layer> 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<IdentifiedLayer, IdentifiedLayer> 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<LayeredDrawer.Layer> layers, LayerVisitor visitor) {
MutableBoolean modified = new MutableBoolean(false);
ListIterator<LayeredDrawer.Layer> 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<LayeredDrawer.Layer> iterator);
}
}

View file

@ -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}.
*
* <p>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);
}
}
}

View file

@ -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);
}
}

View file

@ -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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>", 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 = "<init>",
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 = "<init>", 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));
}
}

View file

@ -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<LayeredDrawer.Layer> getLayers();
}

View file

@ -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<LayeredDrawer> cir) {
addLayer(new SubLayer(drawer, shouldRender));
cir.setReturnValue((LayeredDrawer) (Object) this);
}
}

View file

@ -14,6 +14,8 @@
"EntityModelsMixin",
"EntityRenderersMixin",
"InGameHudMixin",
"LayeredDrawerAccessor",
"LayeredDrawerMixin",
"LivingEntityRendererAccessor",
"SpecialModelTypesMixin",
"TooltipComponentMixin",

View file

@ -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<String> 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<String> 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);
}
}

View file

@ -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"
]
}
}

View file

@ -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;
}
}

Binary file not shown.

After

(image error) Size: 2.4 KiB

Binary file not shown.

After

(image error) Size: 2.8 KiB

Binary file not shown.

After

(image error) Size: 9.4 KiB