Events
Events are a way to listen to specific actions that happen in the game. They are used to trigger specific actions when something happens in the game.
Listening to events
To listen to an event, you need to register a listener for that event.
This is done by registering an object with the PluginManager Event Bus.
It is recommended to do this in the onEnable method of your plugin.
And unregister the listener in the onDisable method in the entrypoint of
your plugin that inherits from IPlugin.
@Override
public void onEnable() {
PluginManager.getInstance().PLUGIN_EVENT_BUS.register(this);
}
@Override
public void onDisable() {
PluginManager.getInstance().PLUGIN_EVENT_BUS.unregister(this);
}
The way event listening works is that the listener class has methods that are annotated with @Subscribe.
And the parameter of the method is the event class that you want to listen to.
@Subscribe
public void OnEvent(EventClassHere event) {
// Do something
}
Remember to replace EventClassHere with the event class you want to listen to.
ClientTickEvent
The ClientTickEvent is an event that is fired every tick of the client.
This event is useful for doing things that need to be done every tick.
It is located in the com.originmint.plugin.events.ClientTickEvent class.
@Subscribe
public void onClientTick(ClientTickEvent event) {
// Do something every tick
}
It has two phases:
PRE: Fired at the beginning of the tick.POST: Fired at the end of the tick.
@Subscribe
public void onClientTick(ClientTickEvent event) {
if (event.phase == ClientTickEvent.Phase.PRE) {
// Do something at the beginning of the tick
} else if (event.phase == ClientTickEvent.Phase.POST) {
// Do something at the end of the tick
}
}
event.data is empty for this event on every version.
KeyInputEvent
The KeyInputEvent is an event that is fired when a key is pressed or released.
This event is useful for listening to key presses.
It is located in the com.originmint.plugin.events.KeyInputEvent class.
event.data contains the key that triggered the event on every version:
data[0]: the key code as anInteger.data[1]: whether the key was pressed (true) or released (false) as aBoolean.
The key code values differ per Minecraft version:
1.8.9uses LWJGL 2 key codes (e.g.42is left shift). When the key has no LWJGL key code, the event carries the typed character plus256.1.18.2,1.20.4,1.21and26.2use GLFW key codes (e.g.340is left shift).
@Subscribe
public void onKeyInput(KeyInputEvent event) {
int keyCode = (Integer) event.data[0]; // LWJGL 2 key code
boolean pressed = (Boolean) event.data[1];
if (pressed && keyCode == 42) {
// Left shift was pressed
}
}
@Subscribe
public void onKeyInput(KeyInputEvent event) {
int keyCode = (Integer) event.data[0]; // GLFW key code
boolean pressed = (Boolean) event.data[1];
if (pressed && keyCode == 340) {
// Left shift was pressed
}
} RenderTickEvent
The RenderTickEvent is an event that is fired every tick of the render loop.
This event is useful for doing things that need to be done every render tick.
It is located in the com.originmint.plugin.events.RenderTickEvent class.
It also has multiple types:
PRE: Fired at the beginning of the render tick.OVERLAY: Fired when the overlay (HUD) is rendered.WORLD: Fired when the world is rendered.POST: Fired at the end of the render tick.GUI: Reserved. This type exists in the enum but is currently never fired on any version.
This can be useful to render something as a HUD overlay, or something in the
world such as tracers or esp. World drawing must happen during the WORLD
type and overlay drawing during the OVERLAY type — see the
Render Manager page for the drawing methods.
@Subscribe
public void onRenderTick(RenderTickEvent event) {
if (event.type == RenderTickEvent.Type.PRE) {
// Do something at the beginning of the render tick
} else if (event.type == RenderTickEvent.Type.POST) {
// Do something at the end of the render tick
} else if (event.type == RenderTickEvent.Type.OVERLAY) {
// Draw HUD overlay shapes here
} else if (event.type == RenderTickEvent.Type.WORLD) {
// Draw world shapes (esp, tracers, nametags) here
}
}
RenderTickEvent data
event.data always starts with the tick delta, but newer versions attach
extra render state. Verified contents per version:
| Type | 1.8.9 | 1.18.2 / 1.20.4 | 1.21 / 26.2 |
|---|---|---|---|
PRE / POST | Float partialTicks, Long nanoTime | Float tickDelta, Long startTime | Float tickProgress, Long nanoTime |
WORLD | Float partialTicks, Long finishTimeNano | Float tickDelta, Long limitTime, MatrixStack, float[3] cameraPos, float[16] modelView, float[16] projection | same as 1.18.2/1.20.4, but limitTime is always 0 |
OVERLAY | Float partialTicks, Long nanoTime | Float tickDelta, MatrixStack, float[16] modelView, float[16] projection | Float tickDelta, float[16] modelView, float[16] projection |
The event also exposes convenience fields, so you usually do not need to dig
through data yourself:
event.modelViewMatrix/event.projectionMatrix(float[16]): set forWORLDandOVERLAYon1.18.2,1.20.4,1.21and26.2; alwaysnullon1.8.9. On1.21and26.2theOVERLAYmatrices are identity, because from 1.21.5 onwards the HUD renders in plain 2D screen space.event.matrixStack(net.minecraft.client.util.math.MatrixStack): only exists on1.18.2,1.20.4,1.21and26.2. Set forWORLDeverywhere, and forOVERLAYon1.18.2/1.20.4(the 1.21 and 26.2 HUDs no longer use one, so it isnullthere). On26.2the type is Mojang’scom.mojang.blaze3d.vertex.PoseStackrather than Yarn’sMatrixStack. Code that references this field will not compile on1.8.9.
Packet Event
Listening to packets is different than other events, it uses the vanilla
packet classes directly. This is useful for listening to packets that are sent
or received by the client. You have to have two parameters in the method: the
packet class you want to listen to and the PacketCallback class located at
com.originmint.plugin.events.PacketCallback. The packet callback is used to
cancel the packet if needed.
Because the vanilla packet classes are part of Minecraft, their names depend on the version your plugin targets:
| Packet base class | Packet packages | |
|---|---|---|
1.8.9 | net.minecraft.network.Packet | net.minecraft.network.play.client (sent), net.minecraft.network.play.server (received) |
1.18.2 | net.minecraft.network.Packet | net.minecraft.network.packet.c2s.* (sent), net.minecraft.network.packet.s2c.* (received) |
1.20.4 / 1.21 | net.minecraft.network.packet.Packet | same as 1.18.2, but some packets moved to the new c2s.common / s2c.common packages (e.g. KeepAliveS2CPacket) |
26.2 | net.minecraft.network.protocol.Packet | net.minecraft.network.protocol.game.* and friends. 26.2 is unobfuscated, so these are Mojang’s own names and direction is in the class name — ClientboundXPacket (received), ServerboundXPacket (sent) — not the package |
Listening to every packet uses the base class:
import net.minecraft.network.Packet;
@Subscribe
public void onPacket(Packet<?> packet, PacketCallback callback) {
// Listen to all packets
}
import net.minecraft.network.Packet;
@Subscribe
public void onPacket(Packet<?> packet, PacketCallback callback) {
// Listen to all packets
}
import net.minecraft.network.packet.Packet;
@Subscribe
public void onPacket(Packet<?> packet, PacketCallback callback) {
// Listen to all packets
} You can also listen to specific packets by specifying the class, for example listening to chat messages from the server:
import net.minecraft.network.play.server.S02PacketChat;
@Subscribe
public void onPacket(S02PacketChat packet, PacketCallback callback) {
// Cancel received chat packets
callback.cancel();
}
import net.minecraft.network.packet.s2c.play.GameMessageS2CPacket;
@Subscribe
public void onPacket(GameMessageS2CPacket packet, PacketCallback callback) {
// Cancel received chat packets
callback.cancel();
} ReplayEvent
The ReplayEvent is an event that is fired when a replay is started, stopped or paused.
This is a custom event that is fired by the replay system.
It is located in the com.originmint.plugin.events.ReplayEvent class.
The different types of the replay event are:
START: Fired when the replay starts.STOP: Fired when the replay stops.FINISH: Fired when the replay finishes.PAUSE: Fired when the replay is paused.RESUME: Fired when the replay is resumed.
It also has two phases:
PRE: Fired at the beginning of the replay event.POST: Fired at the end of the replay event.
This can be useful to do something when the replay starts, stops, or pauses. And you can also cancel starting, stopping, or pausing the replay.
@Subscribe
public void onReplay(ReplayEvent event) {
if (event.type == ReplayEvent.Type.START) {
// Do something when the replay starts
} else if (event.type == ReplayEvent.Type.STOP) {
// Do something when the replay stops
} else if (event.type == ReplayEvent.Type.FINISH) {
// Do something when the replay finishes
} else if (event.type == ReplayEvent.Type.PAUSE) {
// Do something when the replay is paused
} else if (event.type == ReplayEvent.Type.RESUME) {
// Do something when the replay is resumed
}
}
For all replay event types, event.data[0] is the replay id as a String.
@Subscribe
public void onReplay(ReplayEvent event) {
if (event.data == null || event.data.length == 0) {
return;
}
String replayId = (String) event.data[0];
if (event.type == ReplayEvent.Type.START && event.phase == ReplayEvent.Phase.POST) {
LoggingManager.getInstance().log("Started replay " + replayId, LoggingManager.LOG_LEVEL.INFO);
}
}
Example of cancelling the replay start:
@Subscribe
public void onReplay(ReplayEvent event) {
if (event.type == ReplayEvent.Type.START && event.phase == ReplayEvent.Phase.PRE) {
// Cancel starting the replay
event.cancelEvent();
}
}
SyncEvent
The SyncEvent is an event that is fired when the replay goes out of sync.
This is a custom event that is fired by the replay system.
It is located in the com.originmint.plugin.events.SyncEvent class.
This can be used to do something when the replay goes out of sync.
Or cancel the replay from going out of sync.
There are different types of the sync event:
CAMERA: Fired when the camera goes out of sync.POSITION: Fired when the position goes out of sync.LOCK_POSITION: Fired when the client gets moved by the server.INVENTORY: Fired when the inventory goes out of sync.INVENTORY_ERROR: Fired when an error occurs with the inventory.INVENTORY_OPEN: Fired when the inventory is opened.INVENTORY_OPEN_CONTAINER: Fired when the container is opened.INVENTORY_HOTBAR: Fired when the hotbar goes out of sync.INVENTORY_ARMOR: Fired when the armor goes out of sync.INVENTORY_SLOT: Fired when a slot goes out of sync.PLAYER_NEARBY: Fired when a watched player is too close during a replay.ENTITY_NEARBY: Fired when a watched living entity is too close during a replay.AREA: Fired when a locked area block changes near the player.
It also has two phases:
PRE: Fired at the beginning of the sync event.POST: Fired at the end of the sync event.
@Subscribe
public void onSync(SyncEvent event) {
if (event.type == SyncEvent.Type.CAMERA) {
// Do something when the camera goes out of sync
} else if (event.type == SyncEvent.Type.POSITION) {
// Do something when the position goes out of sync
} else if (event.type == SyncEvent.Type.LOCK_POSITION) {
// Do something when the client gets moved by the server
} else if (event.type == SyncEvent.Type.INVENTORY) {
// Do something when the inventory goes out of sync
} else if (event.type == SyncEvent.Type.INVENTORY_ERROR) {
// Do something when an error occurs with the inventory
} else if (event.type == SyncEvent.Type.INVENTORY_OPEN) {
// Do something when the inventory is opened
} else if (event.type == SyncEvent.Type.INVENTORY_OPEN_CONTAINER) {
// Do something when the container is opened
} else if (event.type == SyncEvent.Type.INVENTORY_HOTBAR) {
// Do something when the hotbar goes out of sync
} else if (event.type == SyncEvent.Type.INVENTORY_ARMOR) {
// Do something when the armor goes out of sync
} else if (event.type == SyncEvent.Type.INVENTORY_SLOT) {
// Do something when a slot goes out of sync
} else if (event.type == SyncEvent.Type.PLAYER_NEARBY) {
// Do something when a watched player is nearby
} else if (event.type == SyncEvent.Type.ENTITY_NEARBY) {
// Do something when a watched entity is nearby
} else if (event.type == SyncEvent.Type.AREA) {
// Do something when the locked area changes
}
}
SyncEvent.data is an Object[] with type-specific values:
CAMERA:Float yawDifference,Float pitchDifference— all versions.POSITIONandLOCK_POSITION:Double xDifference,Double yDifference,Double zDifference— all versions.- Inventory types:
ItemStack savedItemStack,ItemStack newItemStack. Either value can benull— 1.8.9 only. PLAYER_NEARBYandENTITY_NEARBY: the nearbyEntity— 1.8.9 only.AREA:Integer x,Integer y,Integer zfor the changed block position — 1.8.9 only.
data is the legacy payload. Its lengths, indices, and version-specific behavior
are kept unchanged so existing compiled plugins remain compatible. New plugins
should use event.getDetails() when they need a complete cross-version payload.
SyncEvent.getDetails() returns an additive Object[] with the same schema on
every supported Minecraft version:
CAMERA:[Float yawDifference, Float pitchDifference, Object rawContext]POSITIONandLOCK_POSITION:[Double xDifference, Double yDifference, Double zDifference, Object rawContext]- All inventory types:
[Object savedItemStack, Object currentItemStack, Integer primaryIndex, Integer secondaryIndex, Integer expectedSize, Integer actualSize, Object rawContext] PLAYER_NEARBYandENTITY_NEARBY:[Object rawEntity, String name, String kind, Double x, Double y, Double z, Double distance]AREA:[Integer x, Integer y, Integer z, String changeKind, Object rawContext]
The stable values are Java numbers and strings. A value of -1 for an inventory
index or size means that field does not apply to that event. Inventory index
fields have these meanings:
- Item mismatch:
primaryIndexis the mismatched slot andsecondaryIndexis-1. - Server-selected slot change:
primaryIndexis the current hotbar slot andsecondaryIndexis the slot requested by the server. - Inventory size error:
expectedSizeandactualSizedescribe the compared arrays.
rawContext, rawEntity, and item-stack values use Minecraft’s class for the
current game version. Treat them as optional and only cast them in version-specific
plugin code. A raw context is normally the packet that caused a server correction,
the opened inventory/container screen, or null for a check that did not originate
from a raw Minecraft object. Area changeKind is one of chunk_data,
multi_block_change, block_change, or block_action.
On 1.18.2, 1.20.4 and 1.21 the inventory, entity and area sync events
still fire (and can still be cancelled), but their data array is currently
empty. Always check event.data.length before indexing.
The following data example targets 1.8.9 (the only version that currently
fills the inventory, entity and area data). The camera and position branches
work the same on every version.
For version-independent code, read getDetails() and check the array before
indexing:
private Object detail(SyncEvent event, int index) {
Object[] details = event.getDetails();
return details != null && index >= 0 && index < details.length
? details[index]
: null;
}
@Subscribe
public void onSyncDetails(SyncEvent event) {
switch (event.type) {
case CAMERA: {
Float yaw = (Float) detail(event, 0);
Float pitch = (Float) detail(event, 1);
LoggingManager.getInstance().log(
"Camera difference: yaw=" + yaw + ", pitch=" + pitch,
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
case POSITION:
case LOCK_POSITION: {
Double x = (Double) detail(event, 0);
Double y = (Double) detail(event, 1);
Double z = (Double) detail(event, 2);
LoggingManager.getInstance().log(
"Position difference: " + x + ", " + y + ", " + z,
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
case PLAYER_NEARBY:
case ENTITY_NEARBY: {
String name = (String) detail(event, 1);
Double distance = (Double) detail(event, 6);
LoggingManager.getInstance().log(
name + " is " + distance + " blocks away",
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
case AREA: {
Integer x = (Integer) detail(event, 0);
Integer y = (Integer) detail(event, 1);
Integer z = (Integer) detail(event, 2);
String changeKind = (String) detail(event, 3);
LoggingManager.getInstance().log(
changeKind + " at " + x + ", " + y + ", " + z,
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
default: {
// Inventory events use indices 2-5 for portable slot and size details.
Integer primaryIndex = (Integer) detail(event, 2);
Integer secondaryIndex = (Integer) detail(event, 3);
LoggingManager.getInstance().log(
"Inventory indices: " + primaryIndex + ", " + secondaryIndex,
LoggingManager.LOG_LEVEL.ERROR
);
}
}
}
import net.minecraft.entity.Entity;
import net.minecraft.item.ItemStack;
private Object data(SyncEvent event, int index) {
if (event.data == null || event.data.length <= index) {
return null;
}
return event.data[index];
}
@Subscribe
public void onSyncData(SyncEvent event) {
switch (event.type) {
case CAMERA: {
Float yawDifference = (Float) data(event, 0);
Float pitchDifference = (Float) data(event, 1);
if (yawDifference == null || pitchDifference == null) {
return;
}
LoggingManager.getInstance().log(
"Camera out of sync: yaw=" + yawDifference + ", pitch=" + pitchDifference,
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
case POSITION:
case LOCK_POSITION: {
Double xDifference = (Double) data(event, 0);
Double yDifference = (Double) data(event, 1);
Double zDifference = (Double) data(event, 2);
if (xDifference == null || yDifference == null || zDifference == null) {
return;
}
LoggingManager.getInstance().log(
"Position out of sync: x=" + xDifference + ", y=" + yDifference + ", z=" + zDifference,
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
case INVENTORY:
case INVENTORY_ERROR:
case INVENTORY_OPEN:
case INVENTORY_OPEN_CONTAINER:
case INVENTORY_HOTBAR:
case INVENTORY_ARMOR:
case INVENTORY_SLOT: {
ItemStack savedItemStack = (ItemStack) data(event, 0);
ItemStack newItemStack = (ItemStack) data(event, 1);
String savedName = savedItemStack == null ? "empty" : savedItemStack.getDisplayName();
String newName = newItemStack == null ? "empty" : newItemStack.getDisplayName();
LoggingManager.getInstance().log(
"Inventory out of sync: saved=" + savedName + ", new=" + newName,
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
case PLAYER_NEARBY:
case ENTITY_NEARBY: {
Entity entity = (Entity) data(event, 0);
if (entity == null) {
return;
}
LoggingManager.getInstance().log(
"Nearby entity caused sync event: " + entity.getName(),
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
case AREA: {
Integer x = (Integer) data(event, 0);
Integer y = (Integer) data(event, 1);
Integer z = (Integer) data(event, 2);
if (x == null || y == null || z == null) {
return;
}
LoggingManager.getInstance().log(
"Area out of sync at " + x + ", " + y + ", " + z,
LoggingManager.LOG_LEVEL.ERROR
);
break;
}
}
}
Example of cancelling the camera going out of sync:
@Subscribe
public void onSync(SyncEvent event) {
if (event.type == SyncEvent.Type.CAMERA && event.phase == SyncEvent.Phase.PRE) {
// Cancel the camera from going out of sync
event.cancelEvent();
}
}
Cancelling a PRE event prevents Mint from stopping the replay for that occurrence.
If the same out-of-sync condition is checked again and is still present, another
event can be delivered, so a listener should cancel only conditions it intentionally
knows how to tolerate.
Player interaction events
Three small events fire when the player interacts with the world. They are
available and behave the same on every version, carry no data, and can all
be cancelled with event.cancel() — cancelling prevents the vanilla action:
OnAttackEvent: fired when the player attacks. Located incom.originmint.plugin.events.OnAttackEvent.OnUseItemEvent: fired when the player uses an item (right click). Located incom.originmint.plugin.events.OnUseItemEvent.OnBlockBreakEvent: fired when the player breaks a block. Located incom.originmint.plugin.events.OnBlockBreakEvent. It has a publicleftClickfield that tells you whether the break came from a left click.
@Subscribe
public void onAttack(OnAttackEvent event) {
// Cancel the attack
event.cancel();
}
@Subscribe
public void onUseItem(OnUseItemEvent event) {
// Right click / item use
}
@Subscribe
public void onBlockBreak(OnBlockBreakEvent event) {
if (event.leftClick) {
// Block break started from a left click
}
}