Events, Keybinds, Tooltips & Chat¶
All of these are exposed through PlatformBridge — you never touch a loader's event API directly from shared code.
Tick and startup events¶
Corim.bridge().onClientTick(mc -> {
// runs every client tick
});
Corim.bridge().onClientStarted(mc -> {
// runs once, after the client has fully finished starting - Options/etc. exist here.
// This is where you should call ConfigManager.load(...) - see Config System.
});
Keybinds¶
CorimKeybinds¶
CorimLib defines one shared keybind category every registered KeyMapping should use:
public final class CorimKeybinds {
public static final KeyMapping.Category CATEGORY =
KeyMapping.Category.register(Identifier.fromNamespaceAndPath("crunch", "menu"));
public static KeyMapping openMenu; // set by CrunchFeatures.bootstrap()
}
CorimKeybinds.CATEGORY is namespaced "crunch" — for your own mod's keybinds, define your own KeyMapping.Category under your own namespace rather than reusing this one, since it will group your keybind under a category literally labeled "crunch" in the vanilla Controls screen.
Registering a keybind¶
KeyMapping myKey = new KeyMapping("key.mymod.do_thing", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_G, myCategory);
Corim.bridge().registerKeybind(myKey);
Corim.bridge().onClientTick(mc -> {
while (myKey.consumeClick()) {
// handle the press
}
});
This is a real, production pattern for opening CrunchMainScreen:
CorimKeybinds.openMenu = new KeyMapping("key.crunch.open_menu", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_K, CorimKeybinds.CATEGORY);
Corim.bridge().registerKeybind(CorimKeybinds.openMenu);
Corim.bridge().onClientTick(mc -> {
while (CorimKeybinds.openMenu.consumeClick()) {
if (mc.canInterruptScreen()) {
mc.setScreenAndShow(new CrunchMainScreen());
}
}
});
Tooltips¶
@FunctionalInterface
public interface TooltipHandler {
void onTooltip(ItemStack stack, Item.TooltipContext context, TooltipFlag flag, List<Component> lines);
}
Corim.bridge().onItemTooltip((stack, context, flag, lines) -> {
lines.add(Component.literal("Extra tooltip line"));
});
context is loader-dependent: Fabric and NeoForge both pass a real Item.TooltipContext; Forge always passes null here, because Forge's own ItemTooltipEvent has no equivalent parameter (its signature is (ItemStack, Player, List<Component>, TooltipFlag)). If your tooltip logic reads context, guard against null or accept that the feature degrades on Forge.
Chat¶
Two separate hooks, run in order:
@FunctionalInterface
public interface ChatFilter {
/** Return false to hide the message. */
boolean test(Component message);
}
// Runs first - can hide the message.
Corim.bridge().onChatReceived(message -> {
return !message.getString().contains("spam"); // false = hidden
});
// Runs after onChatReceived - read-only, for reactions like sounds or logging the last message.
// The message may already be hidden by the time this fires.
Corim.bridge().onChatObserved(message -> {
// e.g. play a sound if the message mentions the player's name
});
Forge's cancellation mechanism is different under the hood
On Forge, ClientChatReceivedEvent.BUS is a CancellableEventBus — there's no event.setCanceled(boolean) to call from a plain listener. A correct ForgePlatformBridge implementation registers via the addListener(Predicate<T>) overload instead, where returning true cancels the event, and casts the lambda explicitly ((Predicate<ClientChatReceivedEvent>) event -> !filter.test(event.getMessage())) because the ambiguous overload won't otherwise compile. You don't need to know this to call ChatFilter — it's already handled inside the bridge implementation — but it explains why Forge's chat handling code looks different from Fabric's/NeoForge's if you go reading the source.
HUD elements¶
Covered in full on the HUD System page — Corim.bridge().registerHudElement(Identifier id, HudRenderer renderer) is the entry point.