ProjectAres/Util/bukkit/src/main/java/tc/oc/commons/bukkit/listeners/PlayerMovementListener.java

266 lines
10 KiB
Java

package tc.oc.commons.bukkit.listeners;
import java.util.Map;
import java.util.WeakHashMap;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.bukkit.EntityLocation;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.EventBus;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerAnimationEvent;
import org.bukkit.event.player.PlayerAnimationType;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.util.RayBlockIntersection;
import org.bukkit.util.Vector;
import tc.oc.commons.bukkit.chat.Audiences;
import tc.oc.commons.bukkit.event.BlockPunchEvent;
import tc.oc.commons.bukkit.event.BlockTrampleEvent;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.Materials;
import tc.oc.commons.core.plugin.PluginFacet;
/**
* Translates standard Bukkit events into a few extra events:
* {@link CoarsePlayerMoveEvent}
* {@link BlockPunchEvent}
* {@link BlockTrampleEvent}
*/
@Singleton
public class PlayerMovementListener implements PluginFacet, Listener {
protected final EventBus eventBus;
protected final Audiences audiences;
// The last location of a player that has been used to generate
// coarse movement events. If a player is not in this list, then
// the next movement event they generate can be assumed valid
// on its own.
private final Map<Player, EntityLocation> lastToLocation = new WeakHashMap<>();
@Inject PlayerMovementListener(EventBus eventBus, Audiences audiences) {
this.eventBus = eventBus;
this.audiences = audiences;
}
/**
* Update the last known location of a player to account for the
* given movement event
*/
private void updateLastToLocation(final PlayerMoveEvent event) {
if(event.isCancelled()) {
this.lastToLocation.put(event.getPlayer(), event.getEntityFrom());
} else {
this.lastToLocation.put(event.getPlayer(), event.getEntityTo());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
lastToLocation.remove(event.getPlayer());
}
// -------------------------
// ---- Player movement ----
// -------------------------
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerMoveHigh(final PlayerMoveEvent event) {
this.handleMovementHigh(event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerTeleportHigh(final PlayerTeleportEvent event) {
this.handleMovementHigh(event);
}
private final void handleMovementHigh(final PlayerMoveEvent event) {
Player player = event.getPlayer();
EntityLocation originalFrom = event.getEntityFrom();
EntityLocation originalTo = event.getEntityTo();
EntityLocation oldTo = this.lastToLocation.get(player);
if(oldTo != null && !oldTo.equals(originalFrom)) {
// If this movement does not start where the last known movement ended,
// we have to make up the missing movement. We do that by (potentially) firing
// two coarse events for this one event, a "fake" one for the missing movement
// and a "real" one for the current movement.
// First, modify this event to look like the missing event, and fire
// a coarse event that wraps it.
event.setFrom(oldTo);
event.setTo(originalFrom);
this.updateLastToLocation(event);
if(this.callCoarsePlayerMove(event)) {
// If the fake coarse event was cancelled, we don't need to fire
// the real one, so just return. Note that the wrapped event won't
// actually be cancelled, rather its to location will be modified
// to return the player to the oldTo location. Also note that if
// the original event was already cancelled before the coarse event
// fired, then we will never get here, and both the fake and real
// events will go through.
this.updateLastToLocation(event);
return;
}
// Restore the event to its real state
event.setFrom(originalFrom);
event.setTo(originalTo);
}
this.updateLastToLocation(event);
if(this.callCoarsePlayerMove(event)) {
this.updateLastToLocation(event);
}
}
/**
* Fire a CoarsePlayerMoveEvent that wraps the given event, only if it crosses
* a block boundary, or the PoseFlags change.
* @param event The movement event to potentially wrap
* @return True if the original event was not cancelled, and a coarse event was fired,
* and that coarse event was cancelled. In this case, the wrapped event won't
* actually be cancelled, but callers should treat it like it is.
*/
private boolean callCoarsePlayerMove(final PlayerMoveEvent event) {
// Don't fire coarse events for teleports that are not "in-game"
// e.g. /jumpto command
if(event instanceof PlayerTeleportEvent) {
PlayerTeleportEvent teleportEvent = (PlayerTeleportEvent) event;
if(teleportEvent.getCause() != TeleportCause.ENDER_PEARL &&
teleportEvent.getCause() != TeleportCause.UNKNOWN) {
return false;
}
}
// If the movement does not cross a block boundary, and no PoseFlags changed, we don't care about it
final EntityLocation from = event.getEntityFrom();
final EntityLocation to = event.getEntityTo();
if(from.position().coarseEquals(to.position()) && from.poseFlags().equals(to.poseFlags())) {
return false;
}
// Remember whether the original event was already cancelled
boolean wasCancelled = event.isCancelled();
CoarsePlayerMoveEvent generalEvent = new CoarsePlayerMoveEvent(event, event.getPlayer(), from, to);
this.eventBus.callEvent(generalEvent);
if(!wasCancelled && generalEvent.isCancelled()) {
// When a coarse event is cancelled, we have our own logic for resetting the
// player's position, so we un-cancel the event and instead modify its
// to location to put the player where we want them.
resetPosition(event);
return true;
} else {
return false;
}
}
/**
* Modify the to location of the given event to prevent the movement and
* move the player so they are standing on the center of the block at the
* from location.
*/
private static void resetPosition(final PlayerMoveEvent event) {
Location newLoc;
double yValue = event.getFrom().getY();
if(yValue <= 0 || event instanceof PlayerTeleportEvent) {
newLoc = event.getFrom();
} else {
newLoc = BlockUtils.center(event.getFrom()).subtract(new Vector(0, 0.5, 0));
if(newLoc.getBlock() != null) {
switch(newLoc.getBlock().getType()) {
case STEP:
case WOOD_STEP:
newLoc.add(new Vector(0, 0.5, 0));
break;
default: break;
}
}
}
newLoc.setPitch(event.getTo().getPitch());
newLoc.setYaw(event.getTo().getYaw());
event.setCancelled(false);
event.setTo(newLoc);
}
// reset the last location on death
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(final PlayerRespawnEvent event) {
this.lastToLocation.remove(event.getPlayer());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerMoveMonitor(final PlayerMoveEvent event) {
this.handleMovementMonitor(event);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerTeleportMonitor(final PlayerTeleportEvent event) {
this.handleMovementMonitor(event);
}
private void handleMovementMonitor(PlayerMoveEvent event) {
// It's possible for a PlayerMoveEvent to be modified by another
// HIGHEST handler after we handle it, so we also check it at MONITOR
this.updateLastToLocation(event);
}
// ------------------------------------------
// ---- Adventure mode block interaction ----
// ------------------------------------------
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void detectBlockPunch(PlayerAnimationEvent event) {
if(event.getAnimationType() != PlayerAnimationType.ARM_SWING) return;
if(event.getPlayer().getGameMode() != GameMode.ADVENTURE) return;
// Client will not punch blocks in adventure mode, so we detect it ourselves and fire a BlockPunchEvent.
// We do this in the kit module only because its the one that is responsible for putting players in adventure mode.
// A few other modules rely on this, including StaminaModule and BlockDropsModule.
RayBlockIntersection hit = event.getPlayer().getTargetedBlock(true, false);
if(hit == null) return;
eventBus.callEvent(new BlockPunchEvent(event.getPlayer(), hit));
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void detectBlockTrample(CoarsePlayerMoveEvent event) {
if(!event.getPlayer().isOnGround()) return;
Block block = event.getBlockTo().getBlock();
if(!Materials.isColliding(block.getType())) {
block = block.getRelative(BlockFace.DOWN);
if(!Materials.isColliding(block.getType())) return;
}
eventBus.callEvent(new BlockTrampleEvent(event.getPlayer(), block));
}
// -------------------------
// ---- Cancel messages ----
// -------------------------
@EventHandler(priority = EventPriority.MONITOR)
public void processCancelMessage(final CoarsePlayerMoveEvent event) {
if(event.isCancelled() && event.getCancelMessage() != null) {
audiences.get(event.getPlayer()).sendWarning(event.getCancelMessage(), false);
}
}
}