Skip to content

Instantly share code, notes, and snippets.

@isXander
Last active May 25, 2025 11:15
Show Gist options
  • Save isXander/c969700ee7206dc068f69cbe91dd155f to your computer and use it in GitHub Desktop.
Save isXander/c969700ee7206dc068f69cbe91dd155f to your computer and use it in GitHub Desktop.
package dev.isxander.controlify.platform.client.fabric.hudapi;
import net.minecraft.resources.ResourceLocation;
public interface HudLayerRegistry {
void renderFirst(ResourceLocation thisElement, HudLayerRenderer renderer);
void renderLast(ResourceLocation thisElement, HudLayerRenderer renderer);
/**
* Renders the given element before the specified element.
* If that element does not exist by the end of registration, an exception will be thrown.
* @param before the identifier of the element to render before
* @param thisElement the identifier of this element to render
* @param renderer the renderer to use for this element
*/
void renderBefore(ResourceLocation before, ResourceLocation thisElement, HudLayerRenderer renderer);
/**
* Renders the given element before the specified element, if that element exists.
* If that element does not exist, the given element will not be rendered without warning or error.
* @param before the identifier of the element to render before
* @param thisElement the identifier of this element to render
* @param renderer the renderer to use for this element
*/
void renderBeforeMaybe(ResourceLocation before, ResourceLocation thisElement, HudLayerRenderer renderer);
/**
* Renders the given element after the specified element.
* If that element does not exist by the end of registration, an exception will be thrown.
* @param after the identifier of the element to render after
* @param thisElement the identifier of this element to render
* @param renderer the renderer to use for this element
*/
void renderAfter(ResourceLocation after, ResourceLocation thisElement, HudLayerRenderer renderer);
/**
* Renders the given element after the specified element.
* If that element does not exist, the given element will not be rendered without warning or error.
* @param after the identifier of the element to render after
* @param thisElement the identifier of this element to render
* @param renderer the renderer to use for this element
*/
void renderAfterMaybe(ResourceLocation after, ResourceLocation thisElement, HudLayerRenderer renderer);
}
package dev.isxander.controlify.platform.client.fabric.hudapi;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.gui.GuiGraphics;
@FunctionalInterface
public interface HudLayerRenderer {
void render(GuiGraphics graphics, DeltaTracker deltaTracker);
}
package dev.isxander.controlify.platform.client.fabric.hudapi;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
@FunctionalInterface
public interface HudRegistrationCallback {
Event<HudRegistrationCallback> EVENT = EventFactory.createArrayBacked(HudRegistrationCallback.class,
listeners -> registry -> {
for (HudRegistrationCallback listener : listeners) {
listener.register(registry);
}
});
void register(HudLayerRegistry registry);
}
package dev.isxander.controlify.platform.client.fabric.hudapi;
import net.minecraft.resources.ResourceLocation;
public class HudRegistrationException extends RuntimeException {
private final ResourceLocation layerId;
public HudRegistrationException(ResourceLocation layerId, String message) {
super(message);
this.layerId = layerId;
}
public ResourceLocation getLayerId() {
return layerId;
}
}
package dev.isxander.controlify.platform.client.fabric.hudapi;
import net.minecraft.resources.ResourceLocation;
public final class VanillaHudLayers {
private VanillaHudLayers() {}
public static final ResourceLocation FIRST = ResourceLocation.withDefaultNamespace("first");
public static final ResourceLocation HEALTH = ResourceLocation.withDefaultNamespace("health");
// etc...
public static final ResourceLocation LAST = ResourceLocation.withDefaultNamespace("last");
}
package dev.isxander.controlify.platform.client.fabric.hudapi.impl;
import dev.isxander.controlify.platform.client.fabric.hudapi.HudLayerRenderer;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.resources.ResourceLocation;
public class HudLayer {
private final ResourceLocation id;
private final Event<HudLayerRenderer> beforeEvent;
private final Event<HudLayerRenderer> afterEvent;
private boolean hasBeforeInvoker, hasAfterInvoker;
private boolean lenient;
public HudLayer(ResourceLocation id, boolean lenient) {
this.id = id;
this.beforeEvent = createEvent();
this.afterEvent = createEvent();
this.lenient = lenient;
}
public ResourceLocation getId() {
return this.id;
}
public Event<HudLayerRenderer> getBeforeEvent() {
return this.beforeEvent;
}
public Event<HudLayerRenderer> getAfterEvent() {
return this.afterEvent;
}
public void markHasBeforeInvoker() {
this.hasBeforeInvoker = true;
}
public void markHasAfterInvoker() {
this.hasAfterInvoker = true;
}
public boolean hasBeforeInvoker() {
return this.hasBeforeInvoker;
}
public boolean hasAfterInvoker() {
return this.hasAfterInvoker;
}
public boolean isLenient() {
return this.lenient;
}
public void setLenient(boolean lenient) {
this.lenient = lenient;
}
private static Event<HudLayerRenderer> createEvent() {
return EventFactory.createArrayBacked(HudLayerRenderer.class, listeners -> (graphics, deltaTracker) -> {
for (HudLayerRenderer listener : listeners) {
listener.render(graphics, deltaTracker);
}
});
}
}
package dev.isxander.controlify.platform.client.fabric.hudapi.impl;
import dev.isxander.controlify.platform.client.fabric.hudapi.*;
import net.fabricmc.fabric.api.event.Event;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.resources.ResourceLocation;
import java.util.HashMap;
import java.util.Map;
public class HudLayerRegistryImpl implements HudLayerRegistry {
private final HudLayer firstLayer, lastLayer;
private final Map<ResourceLocation, HudLayer> layers = new HashMap<>();
private boolean registrationContext = false;
public HudLayerRegistryImpl() {
this.firstLayer = new HudLayer(VanillaHudLayers.FIRST, false);
this.firstLayer.markHasBeforeInvoker();
this.lastLayer = new HudLayer(VanillaHudLayers.LAST, false);
this.lastLayer.markHasAfterInvoker();
this.layers.put(firstLayer.getId(), firstLayer);
this.layers.put(lastLayer.getId(), lastLayer);
}
@Override
public void renderFirst(ResourceLocation thisElement, HudLayerRenderer renderer) {
this.guardRegistrationContext(thisElement);
this.createLayerAtLayer(thisElement, renderer, this.firstLayer.getBeforeEvent(), false);
}
@Override
public void renderLast(ResourceLocation thisElement, HudLayerRenderer renderer) {
this.guardRegistrationContext(thisElement);
this.createLayerAtLayer(thisElement, renderer, this.lastLayer.getAfterEvent(), false);
}
@Override
public void renderBefore(ResourceLocation before, ResourceLocation thisElement, HudLayerRenderer renderer) {
this.guardRegistrationContext(thisElement);
this.guardFirstOrLast(before, thisElement, "renderBefore");
this.createLayerAtLayer(thisElement, renderer, this.getOrCreateLayer(before).getBeforeEvent(), false);
}
@Override
public void renderBeforeMaybe(ResourceLocation before, ResourceLocation thisElement, HudLayerRenderer renderer) {
this.guardRegistrationContext(thisElement);
this.guardFirstOrLast(before, thisElement, "renderBeforeMaybe");
this.createLayerAtLayer(thisElement, renderer, this.getOrCreateLayer(before).getBeforeEvent(), true);
}
@Override
public void renderAfter(ResourceLocation after, ResourceLocation thisElement, HudLayerRenderer renderer) {
this.guardRegistrationContext(thisElement);
this.guardFirstOrLast(after, thisElement, "renderAfter");
this.createLayerAtLayer(thisElement, renderer, this.getOrCreateLayer(after).getAfterEvent(), false);
}
@Override
public void renderAfterMaybe(ResourceLocation after, ResourceLocation thisElement, HudLayerRenderer renderer) {
this.guardRegistrationContext(thisElement);
this.guardFirstOrLast(after, thisElement, "renderAfterMaybe");
this.createLayerAtLayer(thisElement, renderer, this.getOrCreateLayer(after).getAfterEvent(), true);
}
// Called from a mixin
public void wrapRenderVanillaLayer(ResourceLocation id, GuiGraphics graphics, DeltaTracker deltaTracker, Runnable vanillaRenderCall) {
this.getOrCreateLayer(id).getBeforeEvent().invoker().render(graphics, deltaTracker);
vanillaRenderCall.run();
this.getOrCreateLayer(id).getAfterEvent().invoker().render(graphics, deltaTracker);
}
private void createLayerAtLayer(ResourceLocation id, HudLayerRenderer renderer, Event<HudLayerRenderer> atLayer, boolean lenient) {
var layer = new HudLayer(id, lenient);
var oldLayer = this.layers.put(id, layer);
if (oldLayer != null) {
if (oldLayer.hasAfterInvoker() || oldLayer.hasBeforeInvoker()) {
throw new HudRegistrationException(id, "Layer with ID " + id + " has already been registered.");
} else {
layer = oldLayer;
layer.setLenient(lenient);
}
}
HudLayer finalLayer = layer;
atLayer.register((graphics, deltaTracker) -> {
finalLayer.getBeforeEvent().invoker().render(graphics, deltaTracker);
finalLayer.markHasBeforeInvoker();
renderer.render(graphics, deltaTracker);
finalLayer.getAfterEvent().invoker().render(graphics, deltaTracker);
finalLayer.markHasAfterInvoker();
});
}
public void doRegistration() {
try {
this.registrationContext = true;
HudRegistrationCallback.EVENT.invoker().register(this);
} catch (HudRegistrationException e) {
throw new IllegalStateException("Exception during HUD layer registration at layer " + e.getLayerId(), e);
} finally {
this.registrationContext = false;
}
// Ensure that all layers that have been registered have an invoker
// e.g. someone may have set a layer to render before/after another layer,
// but by the end of registration, that layer may not have been registered
for (HudLayer layer : this.layers.values()) {
if (!layer.isLenient() && (!layer.hasBeforeInvoker() && layer != this.lastLayer) && (!layer.hasAfterInvoker() && layer != this.firstLayer)) {
if (layer.getId().getNamespace().equals("minecraft")) {
// If the layer is a vanilla layer, we can just skip it, since it's not registered at registration time.
continue;
}
throw new IllegalStateException("Layer " + layer.getId() + " has no invoker registered for it, but it is not lenient. " +
"This means that the layer was not registered properly, or it was registered after the registration context ended.");
}
}
}
private void guardRegistrationContext(ResourceLocation id) {
if (!registrationContext) {
throw new HudRegistrationException(id, "Cannot register HUD layers outside of the registration context");
}
}
private void guardFirstOrLast(ResourceLocation target, ResourceLocation callerLayer, String callerMethod) {
boolean isFirst = target.equals(firstLayer.getId());
boolean isLast = target.equals(lastLayer.getId());
if (isFirst || isLast) {
// e.g.: "Cannot use renderBefore to target the first layer: my_mod:custom_layer. Use the renderFirst method instead."
throw new HudRegistrationException(callerLayer, "Cannot use " + callerMethod + " to target the " + (isFirst ? "first" : "last") + " layer. " +
"Use the render" + (isFirst ? "First" : "Last") + " method instead.");
}
}
private HudLayer getOrCreateLayer(ResourceLocation id) {
return layers.computeIfAbsent(id, key -> new HudLayer(key, false));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment