Skip to content

Game activity + rules

GameActivity is a scoped bundle of event listeners and GameRule toggles tied to one game instance’s dimension. Every listener you register lives on a child EventNode (on the core ConduitEvents bus) that is detached on close(), so they auto-unregister when the match ends, no manual teardown, no leaks, no double-fires. Rule toggles (no fall damage, no PvP, keep hunger topped up…) are enforced by the engine’s GameRuleEnforcer via Fabric callbacks, so a minigame gets the common behaviours with zero per-game boilerplate.

This is Nucleoid Plasmid’s GameActivity / GameRuleType idea, ported onto Conduit’s event bus.

  • You want listeners that automatically disappear when the round ends.
  • You want the common minigame rule toggles (no fall damage, no hunger, no block break) without writing the Fabric callback plumbing yourself.
  • You want all of a match’s behaviour scoped to its instance dimension, so concurrent lobbies don’t cross-fire.
package me.zlex.conduit.game;
public final class GameActivity implements AutoCloseable {
public GameActivity(ResourceKey<Level> dimension);
public ResourceKey<Level> dimension();
public EventNode node(); // child of the global root
public <E extends ConduitEvent> GameActivity listen(Class<E> type, Consumer<E> handler);
public GameActivity deny(GameRule rule); // turn a rule off
public GameActivity allow(GameRule rule); // turn it back on
public boolean denies(GameRule rule);
@Override public void close(); // detach listeners + rules (idempotent)
}
public enum GameRule {
ALL_DAMAGE, // cancel all damage to players in the dimension
FALL_DAMAGE, // cancel fall damage
PVP, // cancel player-vs-player damage
HUNGER, // keep hunger topped up (no starving)
BLOCK_BREAK // disallow breaking blocks
}
import me.zlex.conduit.event.player.PlayerDamageEvent;
import me.zlex.conduit.game.GameActivity;
import me.zlex.conduit.game.GameRule;
import me.zlex.conduit.instance.GameInstance;
public final class MyRound {
private GameActivity activity;
void start(GameInstance inst) {
activity = new GameActivity(inst.dimension());
activity.deny(GameRule.FALL_DAMAGE)
.deny(GameRule.HUNGER)
.deny(GameRule.BLOCK_BREAK);
activity.listen(PlayerDamageEvent.class, e -> {
if (inLobby(e.player())) e.setCancelled(true);
});
}
void end() {
if (activity != null) {
activity.close(); // detaches every listener + rule for this dimension
activity = null;
}
}
}

Because GameActivity is AutoCloseable, you can also scope it with try-with-resources when the whole match runs inside one block:

try (GameActivity activity = new GameActivity(inst.dimension())) {
activity.deny(GameRule.PVP);
// ... run the match; on exit everything is torn down.
}
  • deny/allow/listen return this, so they chain.
  • close() is idempotent: calling it twice is harmless. It removes the activity’s child node from the global root and unregisters its rules.
  • Listeners registered here only fire for events on this activity’s node; the engine routes per-dimension events (like PlayerDamageEvent) so two concurrent instances don’t see each other’s events.
  • Rules are enforced engine-side; you only declare them. To change a rule mid-match, call deny/allow again on the live activity.