Let events modify

This commit is contained in:
Bjorn Beishline 2026-02-04 02:21:27 -07:00
parent a1abcfa8fd
commit 3f4db9f5b4
9 changed files with 208 additions and 143 deletions

View File

@ -1,57 +1,89 @@
package org.patchbukkit.events;
import com.google.protobuf.InvalidProtocolBufferException;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import patchbukkit.common.UUID;
import patchbukkit.events.Event;
import patchbukkit.events.FireEventResponse;
import patchbukkit.events.PlayerJoinEvent;
import java.util.UUID;
import java.util.logging.Logger;
public class PatchBukkitEventFactory {
private static final Logger LOGGER = Logger.getLogger("PatchBukkit");
@Nullable
public static PlayerJoinEvent createPlayerJoinEvent(@NotNull String playerUuid, @NotNull String joinMessage) {
Player player = getPlayer(playerUuid);
if (player == null) return null;
Component joinMessageComponent = Component.translatable("multiplayer.player.joined",
Component.text(joinMessage));
return new PlayerJoinEvent(player, joinMessageComponent);
public static org.bukkit.event.Event createEventFromBytes(byte[] data) {
try {
Event event = Event.parseFrom(data);
return createEvent(event);
} catch (InvalidProtocolBufferException e) {
LOGGER.severe("Failed to parse Event: " + e.getMessage());
return null;
}
}
@Nullable
public static PlayerQuitEvent createPlayerQuitEvent(@NotNull String playerUuid) {
Player player = getPlayer(playerUuid);
if (player == null) return null;
public static org.bukkit.event.Event createEvent(@NotNull Event event) {
Event.DataCase dataCase = event.getDataCase();
Component quitMessage = Component.translatable("multiplayer.player.left",
Component.text(player.getName()));
return switch (dataCase) {
case PLAYER_JOIN -> {
PlayerJoinEvent joinEvent = event.getPlayerJoin();
Player player = getPlayer(joinEvent.getPlayerUuid().getValue());
if (player == null) yield null;
return new PlayerQuitEvent(player, quitMessage, PlayerQuitEvent.QuitReason.DISCONNECTED);
Component joinMessage = GsonComponentSerializer.gson().deserialize(joinEvent.getJoinMessage());
yield new org.bukkit.event.player.PlayerJoinEvent(player, joinMessage);
}
case DATA_NOT_SET -> {
LOGGER.warning("EventFactory: Received Event with no data");
yield null;
}
};
}
/**
* Check if an event implements Cancellable.
*/
public static boolean isCancellable(@NotNull Event event) {
@NotNull
public static byte[] toFireEventResponse(@NotNull org.bukkit.event.Event event) {
boolean cancelled = event instanceof org.bukkit.event.Cancellable c && c.isCancelled();
FireEventResponse.Builder builder = FireEventResponse.newBuilder()
.setCancelled(cancelled);
Event.Builder eventBuilder = Event.newBuilder();
if (event instanceof org.bukkit.event.player.PlayerJoinEvent joinEvent) {
String joinMessage = GsonComponentSerializer.gson().serialize(joinEvent.joinMessage());
eventBuilder.setPlayerJoin(
PlayerJoinEvent.newBuilder()
.setPlayerUuid(UUID.newBuilder()
.setValue(joinEvent.getPlayer().getUniqueId().toString())
.build())
.setJoinMessage(joinMessage)
.build()
);
}
builder.setData(eventBuilder.build());
return builder.build().toByteArray();
}
public static boolean isCancellable(@NotNull org.bukkit.event.Event event) {
return event instanceof org.bukkit.event.Cancellable;
}
/**
* Look up a player by UUID string. Returns null with a warning if not found.
*/
@Nullable
private static Player getPlayer(@NotNull String uuidStr) {
try {
UUID uuid = UUID.fromString(uuidStr);
java.util.UUID uuid = java.util.UUID.fromString(uuidStr);
Player player = Bukkit.getServer().getPlayer(uuid);
if (player == null) {
LOGGER.warning("EventFactory: Player not found for UUID " + uuidStr);

View File

@ -51,10 +51,12 @@ public class PatchBukkitEventManager {
switch (event.getEventName()) {
case "org.bukkit.event.player.PlayerJoinEvent":
var castedEvent = (org.bukkit.event.player.PlayerJoinEvent) event;
request.setPlayerJoin(
PlayerJoinEvent.newBuilder()
request.setEvent(
patchbukkit.events.Event.newBuilder().setPlayerJoin(
PlayerJoinEvent.newBuilder()
.setJoinMessage(castedEvent.joinMessage().toString())
.setPlayerUuid(BridgeUtils.convertUuid(castedEvent.getPlayer().getUniqueId()))
.setPlayerUuid(BridgeUtils.convertUuid(castedEvent.getPlayer().getUniqueId())).build()
).build()
);
break;
}

View File

@ -1,22 +0,0 @@
package org.patchbukkit.events;
import org.bukkit.event.Event;
import org.bukkit.event.player.PlayerJoinEvent;
import com.google.gson.JsonObject;
public class PatchBukkitEventSerializer {
public static String serialize(Event event) {
JsonObject json = new JsonObject();
if (event instanceof PlayerJoinEvent joinEvent) {
var joinMessage = joinEvent.joinMessage();
if (joinMessage != null) {
json.addProperty("playerUuid", joinEvent.getPlayer().getUniqueId().toString());
json.addProperty("joinMessage", joinMessage.toString());
}
}
return json.toString();
}
}

View File

@ -8,6 +8,12 @@ import "patchbukkit/events/player_events.proto";
option java_multiple_files = true;
option java_package = "patchbukkit.events";
message Event {
oneof data {
PlayerJoinEvent player_join = 1;
}
}
message RegisterEventRequest {
string event_type = 1;
string plugin_name = 2;
@ -16,11 +22,14 @@ message RegisterEventRequest {
}
message CallEventRequest {
oneof event_data {
PlayerJoinEvent player_join = 1;
}
Event event = 1;
}
message CallEventResponse {
bool handled = 1;
}
message FireEventResponse {
bool cancelled = 1;
Event data = 2;
}

View File

@ -1,4 +1,5 @@
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use pumpkin::entity::player::Player;
@ -8,37 +9,62 @@ use pumpkin_api_macros::with_runtime;
use tokio::sync::{mpsc, oneshot};
use crate::java::jvm::commands::JvmCommand;
use crate::proto::patchbukkit::common::Uuid;
use crate::proto::patchbukkit::events::event::Data;
use crate::proto::patchbukkit::events::{Event, PlayerJoinEvent};
#[derive(Clone)]
pub enum PatchBukkitEvent {
PlayerJoinEvent {
server: Arc<Server>,
player: Arc<Player>,
join_message: String,
},
pub struct EventContext {
pub server: Arc<Server>,
pub player: Option<Arc<Player>>,
}
pub trait IntoEventData {
fn get_patch_bukkit_event(&self, server: Arc<Server>) -> PatchBukkitEvent;
pub struct JvmEventPayload {
pub event: Event,
pub context: EventContext,
}
impl IntoEventData for pumpkin::plugin::player::player_join::PlayerJoinEvent {
fn get_patch_bukkit_event(&self, server: Arc<Server>) -> PatchBukkitEvent {
PatchBukkitEvent::PlayerJoinEvent {
server,
player: self.player.clone(),
join_message: self.join_message.clone().get_text(),
pub trait PatchBukkitEvent {
fn to_payload(&self, server: Arc<Server>) -> JvmEventPayload;
fn apply_modifications(&mut self, server: &Arc<Server>, data: Data) -> Option<()>;
}
impl PatchBukkitEvent for pumpkin::plugin::player::player_join::PlayerJoinEvent {
fn to_payload(&self, server: Arc<Server>) -> JvmEventPayload {
JvmEventPayload {
event: Event {
data: Some(Data::PlayerJoin(PlayerJoinEvent {
player_uuid: Some(Uuid {
value: self.player.gameprofile.id.to_string(),
}),
join_message: serde_json::to_string(&self.join_message).unwrap(),
})),
},
context: EventContext {
server,
player: Some(self.player.clone()),
},
}
}
fn apply_modifications(&mut self, server: &Arc<Server>, data: Data) -> Option<()> {
match data {
Data::PlayerJoin(event) => {
self.join_message = serde_json::from_str(&event.join_message).ok()?;
server.get_player_by_uuid(uuid::Uuid::from_str(&event.player_uuid?.value).ok()?)?;
}
}
Some(())
}
}
pub struct PatchBukkitEventHandler<E: IntoEventData> {
pub struct PatchBukkitEventHandler<E: PatchBukkitEvent> {
plugin_name: String,
command_tx: mpsc::Sender<JvmCommand>,
_phantom: PhantomData<E>,
}
impl<E: IntoEventData> PatchBukkitEventHandler<E> {
impl<E: PatchBukkitEvent> PatchBukkitEventHandler<E> {
#[must_use]
pub const fn new(plugin_name: String, command_tx: mpsc::Sender<JvmCommand>) -> Self {
Self {
@ -52,7 +78,7 @@ impl<E: IntoEventData> PatchBukkitEventHandler<E> {
#[with_runtime(global)]
impl<E> EventHandler<E> for PatchBukkitEventHandler<E>
where
E: IntoEventData + Payload + Cancellable + 'static,
E: PatchBukkitEvent + Payload + Cancellable + 'static,
{
fn handle_blocking<'a>(
&'a self,
@ -65,7 +91,7 @@ where
let (tx, rx) = oneshot::channel();
if let Err(e) = command_tx
.send(JvmCommand::FireEvent {
patchbukkit_event: event.get_patch_bukkit_event(server.clone()),
payload: event.to_payload(server.clone()),
respond_to: tx,
plugin: self.plugin_name.clone(),
})
@ -76,11 +102,9 @@ where
}
match rx.await {
Ok(cancelled) => {
if cancelled {
log::debug!("Event was cancelled by a Java plugin");
event.set_cancelled(true);
}
Ok(response) => {
event.set_cancelled(response.cancelled);
event.apply_modifications(server, response.data.unwrap().data.unwrap());
}
Err(_) => {
log::warn!("JVM worker dropped response channel for event");

View File

@ -6,7 +6,10 @@ use pumpkin_protocol::java::client::play::CommandSuggestion;
use tokio::sync::{mpsc, oneshot};
use uuid::Uuid;
use crate::{commands::SimpleCommandSender, events::handler::PatchBukkitEvent};
use crate::{
commands::SimpleCommandSender, events::handler::JvmEventPayload,
proto::patchbukkit::events::FireEventResponse,
};
pub enum LoadPluginResult {
SuccessfullyLoadedSpigot,
@ -43,9 +46,9 @@ pub enum JvmCommand {
respond_to: oneshot::Sender<Result<()>>,
},
FireEvent {
patchbukkit_event: PatchBukkitEvent,
payload: JvmEventPayload,
plugin: String,
respond_to: oneshot::Sender<bool>, // true = cancelled
respond_to: oneshot::Sender<FireEventResponse>, // true = cancelled
},
TriggerCommand {
full_command: String,

View File

@ -4,13 +4,16 @@ use j4rs::{InvocationArg, Jvm, JvmBuilder};
use pumpkin::plugin::Context;
use tokio::sync::mpsc;
use crate::java::{
jar::read_configs_from_jar,
jvm::commands::{JvmCommand, LoadPluginResult},
native_callbacks::{init_callback_context, initialize_callbacks},
plugin::{
command_manager::CommandManager, event_manager::EventManager, manager::PluginManager,
use crate::{
java::{
jar::read_configs_from_jar,
jvm::commands::{JvmCommand, LoadPluginResult},
native_callbacks::{init_callback_context, initialize_callbacks},
plugin::{
command_manager::CommandManager, event_manager::EventManager, manager::PluginManager,
},
},
proto::patchbukkit::events::FireEventResponse,
};
pub struct JvmWorker {
@ -138,24 +141,24 @@ impl JvmWorker {
JvmCommand::FireEvent {
respond_to,
plugin,
patchbukkit_event,
payload,
} => {
let jvm = match self.jvm {
Some(ref jvm) => jvm,
None => &Jvm::attach_thread().unwrap(),
};
let cancelled =
match self
.event_manager
.fire_event(jvm, patchbukkit_event, plugin)
{
Ok(c) => c,
Err(e) => {
log::error!("Failed to fire event: {e}");
false
let original_event = payload.event.clone();
let cancelled = match self.event_manager.fire_event(jvm, payload, plugin) {
Ok(c) => c,
Err(e) => {
log::error!("Failed to fire event: {e}");
FireEventResponse {
cancelled: false,
data: Some(original_event),
}
};
}
};
let _ = respond_to.send(cancelled);
}

View File

@ -6,7 +6,7 @@ use pumpkin_util::text::TextComponent;
use crate::events::handler::PatchBukkitEventHandler;
use crate::java::native_callbacks::CALLBACK_CONTEXT;
use crate::proto::patchbukkit::events::call_event_request::EventData;
use crate::proto::patchbukkit::events::event::Data;
use crate::proto::patchbukkit::events::{
CallEventRequest, CallEventResponse, RegisterEventRequest,
};
@ -65,15 +65,15 @@ pub fn ffi_native_bridge_register_event_impl(request: RegisterEventRequest) -> O
pub fn ffi_native_bridge_call_event_impl(request: CallEventRequest) -> Option<CallEventResponse> {
let ctx = CALLBACK_CONTEXT.get()?;
let event_data = request.event_data?;
log::debug!("Java calling event {:?}", event_data);
let event = request.event?;
log::debug!("Java calling event {:?}", event);
let context = ctx.plugin_context.clone();
let handled = tokio::task::block_in_place(|| {
ctx.runtime.block_on(async {
match event_data {
EventData::PlayerJoin(player_join_event_data) => {
match event.data? {
Data::PlayerJoin(player_join_event_data) => {
let uuid =
uuid::Uuid::parse_str(&player_join_event_data.player_uuid?.value).ok()?;
let player = context.server.get_player_by_uuid(uuid)?;

View File

@ -2,9 +2,13 @@ use std::sync::Arc;
use anyhow::Result;
use j4rs::{Instance, InvocationArg, Jvm};
use prost::Message;
use pumpkin::{entity::player::Player, server::Server};
use crate::events::handler::PatchBukkitEvent;
use crate::{
events::handler::JvmEventPayload,
proto::patchbukkit::events::{FireEventResponse, event::Data},
};
pub struct EventManager {}
@ -23,61 +27,71 @@ impl EventManager {
pub fn fire_event(
&self,
jvm: &Jvm,
event: PatchBukkitEvent,
payload: JvmEventPayload,
plugin_name: String,
) -> Result<bool> {
) -> Result<FireEventResponse> {
let server = jvm.invoke_static("org.bukkit.Bukkit", "getServer", InvocationArg::empty())?;
let patch_server = jvm.cast(&server, "org.patchbukkit.PatchBukkitServer")?;
let event_manager = jvm.invoke(&patch_server, "getEventManager", InvocationArg::empty())?;
let j_event = match event {
PatchBukkitEvent::PlayerJoinEvent {
server,
player,
join_message,
} => {
Self::register_player(jvm, &patch_server, &player, &server)?;
jvm.invoke_static(
"org.patchbukkit.events.PatchBukkitEventFactory",
"createPlayerJoinEvent",
&[
InvocationArg::try_from(player.gameprofile.id.to_string())?,
InvocationArg::try_from(join_message)?,
],
)?
}
};
if let Some(ref event) = payload.event.data
&& matches!(event, Data::PlayerJoin(_))
&& let Some(ref player) = payload.context.player
{
Self::register_player(jvm, &patch_server, player, &payload.context.server)?;
}
let bytes = payload.event.encode_to_vec();
let j_bytes = jvm.create_java_array(
"byte",
&bytes
.iter()
.map(|&b| {
InvocationArg::try_from(b as i8)
.unwrap()
.into_primitive()
.unwrap()
})
.collect::<Vec<_>>(),
)?;
let j_event = jvm.invoke_static(
"org.patchbukkit.events.PatchBukkitEventFactory",
"createEventFromBytes",
&[InvocationArg::from(j_bytes)],
)?;
let is_null: bool = jvm.to_rust(jvm.invoke_static(
"java.util.Objects",
"isNull",
&[InvocationArg::from(jvm.clone_instance(&j_event)?)],
)?)?;
if is_null {
return Err(anyhow::anyhow!(
"Failed to create event - factory returned null"
));
}
let j_event_for_fire = jvm.clone_instance(&j_event)?;
jvm.invoke(
&event_manager,
"fireEvent",
&[
InvocationArg::from(j_event_for_fire),
InvocationArg::from(jvm.clone_instance(&j_event)?),
InvocationArg::try_from(plugin_name)?,
],
)?;
let is_cancellable: bool = jvm.to_rust(jvm.invoke_static(
let response_bytes: Vec<i8> = jvm.to_rust(jvm.invoke_static(
"org.patchbukkit.events.PatchBukkitEventFactory",
"isCancellable",
&[InvocationArg::from(jvm.clone_instance(&j_event)?)],
"toFireEventResponse",
&[InvocationArg::from(j_event)],
)?)?;
let cancelled = if is_cancellable {
match jvm.invoke(
&jvm.clone_instance(&j_event)?,
"isCancelled",
InvocationArg::empty(),
) {
Ok(instance) => jvm.to_rust::<bool>(instance).unwrap_or(false),
Err(_) => false,
}
} else {
false
};
let response_bytes: Vec<u8> = response_bytes.iter().map(|&b| b as u8).collect();
let response = FireEventResponse::decode(response_bytes.as_slice())?;
Ok(cancelled)
Ok(response)
}
pub fn register_player(