Skip to content

Commit

Permalink
Adds ability to listen to cancelled events (#6207)
Browse files Browse the repository at this point in the history
* Hey! Listen!

Adds functionality to allow events to listen to cancelled events only, uncancelled only (previous behavior), and both.
Deprecates listenCancelled (breaking change for EvtResurrect)

* Apply suggestions from code review

Co-authored-by: _tud <98935832+UnderscoreTud@users.noreply.github.com>

* Allow events to have specific default listening behavior

Removes breaking change from EvtResurrect.
Refactors some of SkriptEventHandler
Errors for events that don't support cancelling
Bug fix for data leak via EventData

* revert incorrect java doc change

* Streamline setting default behavior

* indentation

* Prevent any sort of listening behavior on events that can't be cancelled. Reduce local complexity in SkriptEventHandler

* Fix merge mistake

* Apply suggestions from code review

Co-authored-by: Patrick Miller <apickledwalrus@gmail.com>

* Requested Changes

* Cancelled Event Test

* Fix merge mistakes

I need to stop using the browser for merge conflicts

* Update SkriptEventHandler.java

---------

Co-authored-by: _tud <98935832+UnderscoreTud@users.noreply.github.com>
Co-authored-by: Moderocky <admin@moderocky.com>
Co-authored-by: Patrick Miller <apickledwalrus@gmail.com>
  • Loading branch information
4 people authored Apr 7, 2024
1 parent e454a10 commit 33d0fb1
Show file tree
Hide file tree
Showing 8 changed files with 361 additions and 86 deletions.
175 changes: 111 additions & 64 deletions src/main/java/ch/njol/skript/SkriptEventHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@
*/
package ch.njol.skript;

import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import ch.njol.skript.lang.SkriptEvent;
import ch.njol.skript.lang.Trigger;
import ch.njol.skript.timings.SkriptTimings;
import ch.njol.skript.util.Task;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.bukkit.Bukkit;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
Expand All @@ -42,13 +37,16 @@
import org.bukkit.plugin.RegisteredListener;
import org.eclipse.jdt.annotation.Nullable;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

import ch.njol.skript.lang.SkriptEvent;
import ch.njol.skript.lang.Trigger;
import ch.njol.skript.timings.SkriptTimings;
import ch.njol.skript.util.Task;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

public final class SkriptEventHandler {

Expand Down Expand Up @@ -112,66 +110,94 @@ private static List<Trigger> getTriggers(Class<? extends Event> event) {
* @param priority The priority of the Event.
*/
private static void check(Event event, EventPriority priority) {
// get all triggers for this event, return if none
List<Trigger> triggers = getTriggers(event.getClass());
if (triggers.isEmpty())
return;

if (Skript.logVeryHigh()) {
boolean hasTrigger = false;
for (Trigger trigger : triggers) {
SkriptEvent triggerEvent = trigger.getEvent();
if (
triggerEvent.getEventPriority() == priority
&& triggerEvent.canExecuteAsynchronously() ? triggerEvent.check(event) : Boolean.TRUE.equals(Task.callSync(() -> triggerEvent.check(event)))
) {
hasTrigger = true;
break;
}
}
if (!hasTrigger)
return;
// Check if this event should be treated as cancelled
boolean isCancelled = isCancelled(event);

logEventStart(event);
}

boolean isCancelled = event instanceof Cancellable && ((Cancellable) event).isCancelled() && !listenCancelled.contains(event.getClass());
boolean isResultDeny = !(event instanceof PlayerInteractEvent && (((PlayerInteractEvent) event).getAction() == Action.LEFT_CLICK_AIR || ((PlayerInteractEvent) event).getAction() == Action.RIGHT_CLICK_AIR) && ((PlayerInteractEvent) event).useItemInHand() != Result.DENY);

if (isCancelled && isResultDeny) {
if (Skript.logVeryHigh())
Skript.info(" -x- was cancelled");
return;
}
// This logs events even if there isn't a trigger that's going to run at that priority.
// However, there should only be a priority listener IF there's a trigger at that priority.
// So the time will be logged even if no triggers pass check(), which is still useful information.
logEventStart(event, priority);

for (Trigger trigger : triggers) {
SkriptEvent triggerEvent = trigger.getEvent();

// check if the trigger is at the right priority
if (triggerEvent.getEventPriority() != priority)
continue;

// these methods need to be run on whatever thread the trigger is
Runnable execute = () -> {
logTriggerStart(trigger);
Object timing = SkriptTimings.start(trigger.getDebugLabel());
trigger.execute(event);
SkriptTimings.stop(timing);
logTriggerEnd(trigger);
};

if (trigger.getEvent().canExecuteAsynchronously()) {
if (triggerEvent.check(event))
execute.run();
} else { // Ensure main thread
Task.callSync(() -> {
if (triggerEvent.check(event))
execute.run();
return null; // we don't care about a return value
});
}
// check if the cancel state of the event is correct
if (!triggerEvent.getListeningBehavior().matches(isCancelled))
continue;

// execute the trigger
execute(trigger, event);
}

logEventEnd();
}

/**
* Helper method to check if we should treat the provided Event as cancelled.
*
* @param event The event to check.
* @return Whether the event should be treated as cancelled.
*/
private static boolean isCancelled(Event event) {
return event instanceof Cancellable &&
(((Cancellable) event).isCancelled() && isResultDeny(event)) &&
// TODO: listenCancelled is deprecated and should be removed in 2.10
!listenCancelled.contains(event.getClass());
}

/**
* Helper method for when the provided Event is a {@link PlayerInteractEvent}.
* These events are special in that they are called as cancelled when the player is left/right clicking on air.
* We don't want to treat those as cancelled, so we need to check if the {@link PlayerInteractEvent#useItemInHand()} result is DENY.
* That means the event was purposefully cancelled, and we should treat it as cancelled.
*
* @param event The event to check.
* @return Whether the event was a PlayerInteractEvent with air and the result was DENY.
*/
private static boolean isResultDeny(Event event) {
return !(event instanceof PlayerInteractEvent &&
(((PlayerInteractEvent) event).getAction() == Action.LEFT_CLICK_AIR || ((PlayerInteractEvent) event).getAction() == Action.RIGHT_CLICK_AIR) &&
((PlayerInteractEvent) event).useItemInHand() != Result.DENY);
}

/**
* Executes the provided Trigger with the provided Event as context.
*
* @param trigger The Trigger to execute.
* @param event The Event to execute the Trigger with.
*/
private static void execute(Trigger trigger, Event event) {
// these methods need to be run on whatever thread the trigger is
Runnable execute = () -> {
logTriggerStart(trigger);
Object timing = SkriptTimings.start(trigger.getDebugLabel());
trigger.execute(event);
SkriptTimings.stop(timing);
logTriggerEnd(trigger);
};

if (trigger.getEvent().canExecuteAsynchronously()) {
if (trigger.getEvent().check(event))
execute.run();
} else { // Ensure main thread
Task.callSync(() -> {
if (trigger.getEvent().check(event))
execute.run();
return null; // we don't care about a return value
});
}
}


private static long startEvent;

/**
Expand All @@ -180,11 +206,30 @@ private static void check(Event event, EventPriority priority) {
* @param event The Event that started.
*/
public static void logEventStart(Event event) {
logEventStart(event, null);
}

/**
* Logs that the provided Event has started with a priority.
* Requires {@link Skript#logVeryHigh()} to be true to log anything.
* @param event The Event that started.
* @param priority The priority of the Event.
*/
public static void logEventStart(Event event, @Nullable EventPriority priority) {
startEvent = System.nanoTime();
if (!Skript.logVeryHigh())
return;
Skript.info("");
Skript.info("== " + event.getClass().getName() + " ==");

String message = "== " + event.getClass().getName();

if (priority != null)
message += " with priority " + priority;

if (event instanceof Cancellable && ((Cancellable) event).isCancelled())
message += " (cancelled)";

Skript.info(message + " ==");
}

/**
Expand Down Expand Up @@ -307,8 +352,10 @@ public static void unregisterBukkitEvents(Trigger trigger) {
}

/**
* Events which are listened even if they are cancelled.
* Events which are listened even if they are cancelled. This should no longer be used.
* @deprecated Users should specify the listening behavior in the event declaration. "on any %event%:", "on cancelled %event%:".
*/
@Deprecated
public static final Set<Class<? extends Event>> listenCancelled = new HashSet<>();

/**
Expand Down
27 changes: 13 additions & 14 deletions src/main/java/ch/njol/skript/events/SimpleEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@
*/
package ch.njol.skript.events;

import ch.njol.skript.Skript;
import ch.njol.skript.lang.SkriptEvent.ListeningBehavior;
import ch.njol.skript.lang.util.SimpleEvent;
import com.destroystokyo.paper.event.block.AnvilDamagedEvent;
import com.destroystokyo.paper.event.entity.EntityJumpEvent;
import com.destroystokyo.paper.event.entity.ProjectileCollideEvent;
import com.destroystokyo.paper.event.player.PlayerArmorChangeEvent;
import com.destroystokyo.paper.event.player.PlayerJumpEvent;
import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import com.destroystokyo.paper.event.player.PlayerReadyArrowEvent;
import io.papermc.paper.event.player.PlayerStopUsingItemEvent;
import io.papermc.paper.event.player.PlayerDeepSleepEvent;
import io.papermc.paper.event.player.PlayerInventorySlotChangeEvent;
import io.papermc.paper.event.player.PlayerTradeEvent;
import org.bukkit.event.block.BlockCanBuildEvent;
import org.bukkit.event.block.BlockDamageEvent;
import org.bukkit.event.block.BlockFertilizeEvent;
Expand All @@ -35,9 +44,9 @@
import org.bukkit.event.block.BlockSpreadEvent;
import org.bukkit.event.block.LeavesDecayEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.block.SpongeAbsorbEvent;
import org.bukkit.event.enchantment.EnchantItemEvent;
import org.bukkit.event.enchantment.PrepareItemEnchantEvent;
import org.bukkit.event.block.SpongeAbsorbEvent;
import org.bukkit.event.entity.AreaEffectCloudApplyEvent;
import org.bukkit.event.entity.CreeperPowerEvent;
import org.bukkit.event.entity.EntityBreakDoorEvent;
Expand Down Expand Up @@ -108,16 +117,6 @@
import org.spigotmc.event.entity.EntityDismountEvent;
import org.spigotmc.event.entity.EntityMountEvent;

import com.destroystokyo.paper.event.entity.ProjectileCollideEvent;
import com.destroystokyo.paper.event.player.PlayerArmorChangeEvent;
import com.destroystokyo.paper.event.player.PlayerJumpEvent;
import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import com.destroystokyo.paper.event.entity.EntityJumpEvent;
import io.papermc.paper.event.player.PlayerTradeEvent;
import ch.njol.skript.Skript;
import ch.njol.skript.SkriptEventHandler;
import ch.njol.skript.lang.util.SimpleEvent;

/**
* @author Peter Güttinger
*/
Expand Down Expand Up @@ -453,13 +452,13 @@ public class SimpleEvents {
Skript.registerEvent("Resurrect Attempt", SimpleEvent.class, EntityResurrectEvent.class, "[entity] resurrect[ion] [attempt]")
.description("Called when an entity dies, always. If they are not holding a totem, this is cancelled - you can, however, uncancel it.")
.examples(
"on resurrect attempt:",
"on resurrect attempt:",
"\tentity is player",
"\tentity has permission \"admin.undying\"",
"\tuncancel the event"
)
.since("2.2-dev28");
SkriptEventHandler.listenCancelled.add(EntityResurrectEvent.class); // Listen this even when cancelled
.since("2.2-dev28")
.listeningBehavior(ListeningBehavior.ANY);
Skript.registerEvent("Player World Change", SimpleEvent.class, PlayerChangedWorldEvent.class, "[player] world chang(ing|e[d])")
.description("Called when a player enters a world. Does not work with other entities!")
.examples("on player world change:",
Expand Down
Loading

0 comments on commit 33d0fb1

Please sign in to comment.