Skip to content

Hooks

Open in AI chat

Every callback HBM and Golem Core offer is a dev.golem.core.hook.Hook<T>: one listener type T, any number of listeners, called in a defined order. Use a hook for a small change in behaviour; use substitution to rewrite a block.

public final class Hook<T> implements Iterable<T> {
public static final String DEFAULT_PHASE = "default";
public static <T> Hook<T> of(Function<List<T>, T> combiner);
public static <T> Hook<T> withPhases(Function<List<T>, T> combiner, String... phases);
public static <T> Hook<T> notifying();
public void register(T listener); // default phase
public synchronized void register(String phase, T listener);
public synchronized void addPhaseOrdering(String first, String second);
public T invoker();
public List<T> listeners();
public boolean isEmpty();
}
  • Order. Listeners run phase by phase, and within a phase in registration order. A new phase you register into runs after the existing phases until you order it. addPhaseOrdering(first, second) makes every listener of first run before every listener of second. Phases are namespaced strings ("ntmspace:atmosphere"); a cycle is rejected.
  • Combining. The hook’s combiner decides what several answers mean: first non-PASS wins, first veto wins, or all are notified. A notifying() hook has no invoker; its owner loops over the listeners.
  • Between mods. Registration order between two mods is defined only for listeners registered in declare() (dependencies first, on every loader). Fabric runs client entrypoints in no defined order, so a client listener that must follow another mod’s goes in a phase ordered after the default one.

Verdict is the answer of “may this happen?” hooks:

public enum Verdict {
ALLOW, DENY, PASS;
public static <T> Verdict first(List<T> listeners, Function<T, Verdict> ask); // first non-PASS, or PASS
public boolean orElse(boolean fallback); // PASS -> fallback
}

Running before another mod’s listeners:

ntmprobe: ProbeHooks.java (shortened)
public static final String FIRST = "ntmprobe:first";
hook.addPhaseOrdering(FIRST, Hook.DEFAULT_PHASE);
hook.register(FIRST, listener);

Declaring your own hook:

public static final Hook<CanBurn> CAN_BURN = Hook.of(listeners -> (level, pos) -> {
for (CanBurn listener : listeners) if (!listener.canBurn(level, pos)) return false;
return true;
});
if (!CAN_BURN.invoker().canBurn(level, pos)) return;

com.hbm.api.environment.Environment is what HBM’s machines, blocks, items and entities ask about their surroundings. Every default is the 1.7 rule, so plain HBM behaves as 1.7 on Earth. NTM: Space answers for its planets, orbits and sealed rooms.

com/ntmspace/atmosphere/AtmosphereHooks.java (shortened)
Environment.BREATHING.register((level, pos, amount) -> breatheAir(level, pos, amount) ? Verdict.ALLOW : Verdict.DENY);
Environment.PRESSURE.register(AtmosphereHandler::pressure);
Environment.GRAVITY.register(AtmosphereHooks::gravity);
Environment.SURFACE.register(level -> CelestialBodies.of(level) != null ? Verdict.ALLOW : Verdict.PASS);
Hook Listener Combining Default Query
BREATHING Verdict breathe(Level level, BlockPos pos, int amount) first non-PASS a fire breathes breatheAir(level, pos, amount); breatheAir(level, pos, amount, unanswered)
PRESSURE double pressure(Level level, BlockPos pos) (NaN passes) first non-NaN EARTH_PRESSURE = 1.0 atm pressure(level, pos); isVacuum(level, pos) (in orbit, or below VACUUM_PRESSURE = 0.01)
SURFACE Verdict surface(Level level) first non-PASS only the overworld isSurface(level)
ORBIT Verdict orbit(Level level) first non-PASS not in orbit inOrbit(level)
SUNLIGHT double sunlight(Level level, BlockPos pos) (NaN passes) first non-NaN 1.0 sunlight(level, pos)
GROUNDWATER @Nullable FluidType groundwater(Level level, BlockPos pos) (null passes) first non-null Fluids.WATER; Fluids.NONE in orbit groundwater(level, pos); declaredGroundwater(level, pos)
GRAVITY double gravity(Entity entity), a multiple of Earth’s (NaN passes) first non-NaN 1.0 gravity(entity)
GAS_RELEASE void release(Level level, BlockPos pos, FluidType type, double mB) notifying none releaseGas(level, pos, type, mB) (gases only, server only)
COMPRESSION Verdict compress(Level level, BlockPos pos, int amount) first non-PASS always air to compress compressAir(level, pos, amount)
GROWTH Verdict grow(Level level, BlockPos pos) first non-PASS the grass grows canGrow(level, pos)
LIVING_TICK Consumer<LivingEntity> notifying none livingTick(entity), once per living tick, server side, after HBM’s radiation
AMBIENT_RADIATION double floor(Level level, BlockPos pos, LivingEntity entity) highest answer 0 ambientRadiationFloor(level, pos, entity)
IMPACT void impact(ServerLevel level, BlockPos pos) notifying none impact(level, pos), when Tom hits
LIGHT_ON void lightOn(ServerLevel level, BlockPos pos, BlockState state) notifying none lightOn(level, pos, state), when a spotlight is lit
NUCLEAR_BLAST void blast(ServerLevel level, double x, double z, float radius) notifying none nuclearBlast(level, x, z, radius): an MK5 blast’s first tick, every mini nuke
SATELLITES @Nullable ServerLevel satellites(ServerLevel level, @Nullable BlockPos pos, ItemStack launched) (null passes) first non-null the level itself satelliteLevel(level, pos, launched)

Other constants: EARTH_GRAVITY = 1.6 (blocks per tick per second). An addon keeps its own per-entity state (oxygen, corrosion) in a Golem Core EntityData attachment of its own, never in HBM’s living props.

com.hbm.api.environment.SkyOwnership.OWNERS (boolean ownsSky(Level level), any true wins) tells HBM that your mod draws a dimension’s sky, so HBM’s own sky layers stay off it. Query: SkyOwnership.claimed(level).

com.hbm.api.event.HbmEvents holds observe-only moments. Each is a notifying Hook<Consumer<Payload>>; the payload is a small immutable record, the same one KubeJS scripts see.

Hook Payload record When
EXPLOSION Explosion(Level level, double x, double y, double z, float size, @Nullable Entity exploder, String kind) after an ExplosionVNT (incl. ExplosionNT) or ExplosionLarge blast; kind is vnt or large
BOMB_DETONATED BombDetonated(Level level, BlockPos pos, String block, @Nullable Entity detonator, String result, boolean success) a detonator or a deferred detonation triggered a bomb block
MISSILE_LAUNCHED MissileLaunched(Level level, BlockPos launcher, Entity missile, String missileType, int targetX, int targetZ, @Nullable Entity launchedBy) a launch pad or custom-missile launcher fired
MISSILE_IMPACT MissileImpact(Level level, Entity missile, String missileType, double x, double y, double z, BlockPos hitPos, int targetX, int targetZ, @Nullable LivingEntity thrower) a ballistic missile hit a block, before its warhead goes off
MACHINE_RECIPE_COMPLETED MachineRecipeCompleted(Level level, BlockPos pos, String machine, String table, String recipe) a recipe-module machine finished an operation
ENTITY_IRRADIATED EntityIrradiated(LivingEntity entity, String hazard, String contamination, double amount, double dose) a radiation dose got through contaminate (often every tick: keep it cheap)
HbmEvents.EXPLOSION.register(event -> LOG.info("{} blast of {} at {} {} {}", event.kind(), event.size(), event.x(), event.y(), event.z()));
Hook Listener Does
TileEntityMachineFrackingTower.BLIGHT Verdict blight(ServerLevel level, BlockPos tower, BlockPos deposit, DepositDataMaps.Deposit kind) first non-PASS; DENY replaces 1.7’s oil spot around the tower
BobmazonOfferFactory.OFFERS void addOffers(List<Offer> standard, List<Offer> special) notifying; append Bobmazon offers. Register at mod construction on both sides

Particle effects. com.hbm.particle.helper.HbmEffectNT.register("<ns>:<name>") adds an effect to HBM’s particle payload. The name must be namespaced. On the server, effect.send(level, x, y, z, data) (also with a range, or at a BlockPos) and effect.sendAround(level, x, y, z, data, centreX, centreY, centreZ, range); on the client, effect.setHandler((level, x, y, z, data) -> ...) (EffectHandler.summonParticle).

Golem Core’s hooks for vanilla behaviour work on every loader. Many have an on... registration method beside the hook field.

Hook Listener Notes
world.Furnaces.IGNITION Verdict mayIgnite(Level level, BlockPos pos, ItemStack fuel) vanilla furnaces; DENY means no burn time
world.Evaporation.WATER Verdict evaporates(Level level, BlockPos pos) bucket and dispenser water; nothing deciding keeps vanilla’s answer
world.BlockPlaces.HANDLERS void onPlace(@Nullable Entity placer, ServerLevel level, BlockPos pos, BlockState placed) register with BlockPlaces.onPlace(handler)
world.BlockBreaks.HANDLERS void onBreak(ServerPlayer player, ServerLevel level, BlockPos pos, BlockState state) register with BlockBreaks.onBreak(handler)
world.Spawns.CHECKS boolean deny(Mob mob, ServerLevel level, Spawns.Reason reason) natural and chunk-generation spawns, and patrols, phantoms and traders; register with Spawns.onSpawnCheck
world.Spawns.POTENTIAL void onPotentialSpawns(ServerLevel level, MobCategory category, BlockPos pos) register with Spawns.onPotentialSpawns
world.DamageEvents.ATTACK / HURT Consumer<DamageEvent> (entity(), source(), amount(), setAmount, cancel()) register with DamageEvents.onAttack / onHurt
world.Deaths.ALLOW / AFTER BiPredicate<LivingEntity, DamageSource> / BiConsumer<LivingEntity, DamageSource>
world.Attacks.HANDLERS / INTERCEPTORS BiConsumer<Player, Entity> / BiPredicate<Player, Entity>
world.ItemUses.FINISH BiConsumer<LivingEntity, ItemStack> an item finished being used (food eaten)
world.ItemFirstUse.HANDLERS InteractionResult use(ItemStack stack, UseOnContext context)
world.Sleeps.ATTEMPTS BiConsumer<Player, BlockPos>
world.Weathers.PROVIDERS @Nullable State weather(ServerLevel level) register with Weathers.register
world.ChestLoot.INJECTIONS ChestLoot.Injection add a pool with ChestLoot.inject(String table, String name, int rollsMin, int rollsMax, int emptyWeight, List<ChestLoot.Entry> entries); Entry(Supplier<? extends Item> item, int min, int max, int weight)
world.Gliders.SOURCES / WINGS boolean canGlide(LivingEntity), void wear(LivingEntity) / ItemStack wings(ItemStack chest)
platform.Lifecycle.PLAYER_JOIN void joined(ServerPlayer player, boolean firstTime) register with Lifecycle.onPlayerJoin
platform.Lifecycle.ENTITY_LOAD BiConsumer<Entity, ServerLevel> register with Lifecycle.onEntityLoad

Lifecycle also has onServerTick, onServerStarted, onServerStopping, onLevelUnload, onChunkLoad, onChunkUnload, onPlayerCopy, onDatapackSync and onDatapackReloaded.

world.CompatBlock (Golem Core’s block base class) has two overridable answers: onSustainPlant(BlockGetter level, BlockPos soilPos, BlockState soil, BlockState plant) returns a Verdict (1.7 canSustainPlant), and isLightTransparent(BlockState state) returns true for 1.7 light opacity 0. Both must depend on their arguments only.

Hook Listener Notes
client.CameraHooks.DISTANCE float thirdPersonDistance(Player player, float distance) each listener receives the previous answer
client.CameraHooks.LOWER_THIRD_PERSON boolean lowerThirdPersonCamera(Player player) any true lowers the camera
client.CameraHooks.PITCH float pitchOffset(Entity entity) offsets add up
client.PlayerRenderHooks.POSE void apply(Player player, PoseStack pose, float partialTick) transforms the whole player model; HBM’s listeners are in phase hbm:player, before the default
client.PlayerRenderHooks.MODEL Consumer<HumanoidPose> the humanoid model’s parts after setupAnim
client.Fogs.MODIFIERS Fogs.Modifier fog changes
client.Lightmaps.MODIFIERS Lightmaps.Modifier (active(ClientLevel), add(ClientLevel, int block, int sky, int[] rgb)) light map changes
client.Capes.PROVIDERS @Nullable Identifier cape(UUID id, String name) player capes
client.AttackInput.GUARDS BooleanSupplier
render.BlockOutlines.SUPPRESSORS BooleanSupplier
render.Skyboxes.SKYBOXES Skyboxes.Skybox see Dimensions
world.Skies.MODIFIERS Skies.Modifier see Dimensions

client.Fov.current() returns the current field of view on every node.