Refactor damage source checking and restructure player management

The source of damage detection for PvP interactions has been updated to improve accuracy and flexibility. A 'Target' sub-class has been implemented in the PlayerManager class, removing the need for a separate DamageCheck class. The PlayerManager class was also moved to the 'data' package. Distribution of feedback messages has been simplified using the new DamageCheckResult class.
This commit is contained in:
2024-02-24 19:23:54 +01:00
parent b9e2aefc40
commit 1aeb245666
16 changed files with 266 additions and 167 deletions
@@ -4,7 +4,8 @@ import me.youhavetrouble.preventstabby.commands.MainCommand;
import me.youhavetrouble.preventstabby.config.ConfigCache;
import me.youhavetrouble.preventstabby.hooks.PlaceholderApiHook;
import me.youhavetrouble.preventstabby.hooks.WorldGuardHook;
import me.youhavetrouble.preventstabby.players.PlayerManager;
import me.youhavetrouble.preventstabby.data.PlayerListener;
import me.youhavetrouble.preventstabby.data.PlayerManager;
import me.youhavetrouble.preventstabby.util.*;
import org.bstats.bukkit.Metrics;
import org.bukkit.command.CommandSender;
@@ -16,7 +17,6 @@ public final class PreventStabby extends JavaPlugin {
private static PreventStabby plugin;
private ConfigCache configCache;
private PlayerManager playerManager;
private DamageCheck damageCheck;
private DatabaseSQLite sqLite;
private static boolean worldGuardHook;
@@ -25,10 +25,9 @@ public final class PreventStabby extends JavaPlugin {
plugin = this;
reloadPluginConfig();
playerManager = new PlayerManager(this);
damageCheck = new DamageCheck(this);
// Register listeners TODO
getServer().getPluginManager().registerEvents(new PlayerListener(), this);
// Register command
PluginCommand pvpCommand = getCommand("pvp");
@@ -83,10 +82,6 @@ public final class PreventStabby extends JavaPlugin {
return playerManager;
}
public DamageCheck getDamageUtil() {
return damageCheck;
}
public DatabaseSQLite getSqLite() {return sqLite;}
public static PreventStabby getPlugin() {
@@ -1,7 +1,9 @@
package me.youhavetrouble.preventstabby.api;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.data.DamageCheckResult;
import me.youhavetrouble.preventstabby.util.PvpState;
import org.bukkit.entity.Entity;
public class PreventStabbyAPI {
@@ -19,5 +21,9 @@ public class PreventStabbyAPI {
PreventStabby.getPlugin().getPlayerManager().setForcedPvpState(newForcedPvpState);
}
public static DamageCheckResult canDamage(Entity attacker, Entity victim) {
return PreventStabby.getPlugin().getPlayerManager().canDamage(attacker, victim);
}
}
@@ -1,7 +1,7 @@
package me.youhavetrouble.preventstabby.api.event;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.players.PlayerData;
import me.youhavetrouble.preventstabby.data.PlayerData;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
@@ -1,7 +1,7 @@
package me.youhavetrouble.preventstabby.api.event;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.players.PlayerData;
import me.youhavetrouble.preventstabby.data.PlayerData;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
@@ -1,7 +1,7 @@
package me.youhavetrouble.preventstabby.api.event;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.players.PlayerData;
import me.youhavetrouble.preventstabby.data.PlayerData;
import org.bukkit.OfflinePlayer;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@@ -3,7 +3,7 @@ package me.youhavetrouble.preventstabby.commands;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.api.event.PlayerTogglePvpEvent;
import me.youhavetrouble.preventstabby.config.PreventStabbyPermission;
import me.youhavetrouble.preventstabby.players.PlayerManager;
import me.youhavetrouble.preventstabby.data.PlayerManager;
import me.youhavetrouble.preventstabby.util.PluginMessages;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
@@ -0,0 +1,41 @@
package me.youhavetrouble.preventstabby.data;
import javax.annotation.Nullable;
import java.util.UUID;
/**
* @param ableToDamage Result of the damage check
* @param feedbackForAttacker Feedback to send to the attacker
* @param feedbackForVictim Feedback to send to the victim
* @param attackerId UUID of attacker player
* @param victimId UUID of victim player
*/
public record DamageCheckResult(
boolean ableToDamage,
@Nullable UUID attackerId,
@Nullable UUID victimId,
@Nullable String feedbackForAttacker,
@Nullable String feedbackForVictim
) {
public static DamageCheckResult positive() {
return new DamageCheckResult(
true,
null,
null,
null,
null
);
}
public static DamageCheckResult positive(UUID attackerId, UUID victimId) {
return new DamageCheckResult(
true,
attackerId,
victimId,
null,
null
);
}
}
@@ -1,4 +1,4 @@
package me.youhavetrouble.preventstabby.players;
package me.youhavetrouble.preventstabby.data;
import me.youhavetrouble.preventstabby.PreventStabby;
@@ -10,16 +10,17 @@ import java.util.UUID;
public class PlayerData {
private final UUID playerUuid;
private long lastAccessTimestamp, combatStartTimestamp, loginTimestamp, teleportTimestamp;
private long lastAccessTimestamp, loginTimestamp;
private Long combatStartTimestamp, teleportTimestamp;
private boolean pvpEnabled, lastCombatCheck;
public PlayerData(UUID playerUuid, boolean pvpEnabled) {
this.playerUuid = playerUuid;
this.pvpEnabled = pvpEnabled;
this.lastCombatCheck = false;
this.combatStartTimestamp = Long.MIN_VALUE;
this.loginTimestamp = Long.MIN_VALUE;
this.teleportTimestamp = Long.MIN_VALUE;
this.combatStartTimestamp = null;
this.loginTimestamp = System.currentTimeMillis();
this.teleportTimestamp = null;
refreshCacheTime();
}
@@ -89,14 +90,14 @@ public class PlayerData {
protected void markNotInCombat() {
refreshCacheTime();
this.combatStartTimestamp = Long.MIN_VALUE;
this.combatStartTimestamp = null;
}
/**
* Sets the login timestamp for the player.
* @param loginTimestamp The login timestamp to set.
*/
public void setLoginTimestamp(long loginTimestamp) {
protected void setLoginTimestamp(long loginTimestamp) {
this.loginTimestamp = loginTimestamp;
}
@@ -130,6 +131,7 @@ public class PlayerData {
* @return The number of seconds left until combat ends. -1 if not in combat
*/
public long getSecondsLeftUntilCombatEnd() {
if (combatStartTimestamp == null) return -1;
long timeSinceCombatStart = System.currentTimeMillis() - combatStartTimestamp;
long combatTimeConfigured = PreventStabby.getPlugin().getConfigCache().combat_time * 1000;
return (timeSinceCombatStart < combatTimeConfigured) ? (combatTimeConfigured - timeSinceCombatStart) / 1000 : -1;
@@ -145,6 +147,7 @@ public class PlayerData {
}
public boolean hasTeleportProtection() {
if (teleportTimestamp == null) return false;
return System.currentTimeMillis() - (teleportTimestamp + (PreventStabby.getPlugin().getConfigCache().teleport_protection_time * 1000)) < 0;
}
@@ -1,8 +1,11 @@
package me.youhavetrouble.preventstabby.players;
package me.youhavetrouble.preventstabby.data;
import com.destroystokyo.paper.event.entity.EntityAddToWorldEvent;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.util.PluginMessages;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Tameable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -55,4 +58,16 @@ public class PlayerListener implements Listener {
playerData.markNotInCombat();
}
/**
* Load data for owners of loaded tameables
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onEntityLoad(EntityAddToWorldEvent event) {
Entity entity = event.getEntity();
if (!(entity instanceof Tameable tameable)) return;
UUID ownerId = tameable.getOwnerUniqueId();
if (ownerId == null) return;
PreventStabby.getPlugin().getPlayerManager().getPlayerData(ownerId);
}
}
@@ -1,14 +1,19 @@
package me.youhavetrouble.preventstabby.players;
package me.youhavetrouble.preventstabby.data;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.api.event.PlayerEnterCombatEvent;
import me.youhavetrouble.preventstabby.api.event.PlayerLeaveCombatEvent;
import me.youhavetrouble.preventstabby.util.DamageCheck;
import me.youhavetrouble.preventstabby.util.PluginMessages;
import me.youhavetrouble.preventstabby.util.PvpState;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Tameable;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -27,6 +32,17 @@ public class PlayerManager {
playerList.values().removeIf(PlayerData::isCacheExpired);
}, 250, 250, TimeUnit.MILLISECONDS);
Bukkit.getScheduler().runTaskTimer(plugin, (task -> {
List<LivingEntity> entities = new ArrayList<>();
Bukkit.getWorlds().forEach((world -> entities.addAll(world.getLivingEntities())));
Bukkit.getAsyncScheduler().runNow(plugin, (asyncTask) -> entities.forEach(livingEntity -> {
if (!(livingEntity instanceof Tameable tameable)) return;
UUID ownerId = tameable.getOwnerUniqueId();
if (ownerId == null) return;
getPlayerData(ownerId);
}));
}), 0, 20 * 15);
Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, (task) -> {
for (PlayerData playerData : playerList.values()) {
if (playerData == null) continue;
@@ -83,17 +99,64 @@ public class PlayerManager {
playerList.put(uuid, data);
}
public void handleDamageCheck(@NotNull DamageCheckResult damageCheckResult) {
PluginMessages.sendOutMessages(damageCheckResult);
PlayerData attacker = getPlayer(damageCheckResult.attackerId());
PlayerData victim = getPlayer(damageCheckResult.victimId());
if (attacker == null || victim == null) return;
if (!damageCheckResult.ableToDamage()) return;
attacker.markInCombat();
victim.markInCombat();
}
/**
* Determines whether the given attacker can damage the victim.
*
* @param attacker The attacking entity.
* @param victim The victim entity.
* @return A {@link DamageCheck.DamageCheckResult} object containing the result of the damage check.
* @return A {@link DamageCheckResult} object containing the result of the damage check.
*/
public DamageCheck.DamageCheckResult canDamage(Entity attacker, Entity victim) {
DamageCheck damageCheck = plugin.getDamageUtil();
return damageCheck.canDamage(attacker, victim);
public DamageCheckResult canDamage(@NotNull Entity attacker, @NotNull Entity victim) {
Target attackerData = Target.getTarget(attacker);
Target victimData = Target.getTarget(victim);
if (attackerData == null || victimData == null) return DamageCheckResult.positive();
return canDamage(attackerData.playerUuid, victimData.playerUuid, victimData.classifier);
}
public DamageCheckResult canDamage(UUID attackerId, UUID victimId, Target.EntityClassifier victimClassifier) {
if (attackerId == null || victimId == null) return DamageCheckResult.positive();
PlayerData attackerPlayerData = getPlayer(attackerId);
PlayerData victimPlayerData = getPlayer(victimId);
if (attackerPlayerData == null || victimPlayerData == null) {
return DamageCheckResult.positive();
}
if (attackerPlayerData.isProtected()) {
String message = switch (victimClassifier) {
case PLAYER -> plugin.getConfigCache().cannotAttackTeleportOrSpawnProtectionAttacker;
case PET -> plugin.getConfigCache().cannotAttackPetsTeleportOrSpawnProtectionAttacker;
case MOUNT -> plugin.getConfigCache().cannotAttackMountsTeleportOrSpawnProtectionAttacker;
default -> null;
};
return new DamageCheckResult(false, attackerId, victimId, message, null);
}
if (victimPlayerData.isProtected()) {
String message = null;
if (victimClassifier == Target.EntityClassifier.PLAYER) {
message = plugin.getConfigCache().cannotAttackTeleportOrSpawnProtectionVictim;
}
return new DamageCheckResult(false, attackerId, victimId, message, null);
}
return switch (getForcedPvpState()) {
case DISABLED -> new DamageCheckResult(false, attackerId, victimId, plugin.getConfigCache().cannotAttackForcedPvpOff, null);
case ENABLED -> DamageCheckResult.positive();
default -> DamageCheckResult.positive();
};
}
/**
@@ -0,0 +1,71 @@
package me.youhavetrouble.preventstabby.data;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Tameable;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.UUID;
public class Target {
/**
* The unique identifier for a player.
*/
public final UUID playerUuid;
/**
* Represents the entity classifier of a target.
* This indicates what type of entity player's id was assumed from
*/
public final EntityClassifier classifier;
private Target(UUID uuid, EntityClassifier classifier) {
this.playerUuid = uuid;
this.classifier = classifier;
}
/**
* Get the UUID of the actual player, being owner of a pet, shooter of a projectile, etc.
*
* @param entity Base entity to get the player UUID from
* @return UUID of the actual player, null if not found
*/
@Nullable
public static Target getTarget(@NotNull Entity entity) {
if (entity instanceof Player) return new Target(entity.getUniqueId(), EntityClassifier.PLAYER);
// Get shooter of projectile
if (entity instanceof Projectile projectile) {
if (projectile.getShooter() instanceof Player shooter) {
return new Target(shooter.getUniqueId(), EntityClassifier.PLAYER);
}
}
// Get player riding mount
if (!entity.getPassengers().isEmpty()) {
Entity passenger = entity.getPassengers().get(0);
if (passenger instanceof Player) {
return new Target(passenger.getUniqueId(), EntityClassifier.MOUNT);
}
}
// Get owner of tamed entity
if (entity instanceof Tameable tameable) {
if (tameable.getOwner() != null) {
return new Target(tameable.getOwner().getUniqueId(), EntityClassifier.PET);
}
}
return null;
}
public enum EntityClassifier {
PLAYER,
PET,
MOUNT,
OTHER
}
}
@@ -0,0 +1,33 @@
package me.youhavetrouble.preventstabby.listeners;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.data.DamageCheckResult;
import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
public class PlayerDamageListener implements Listener {
private final PreventStabby plugin;
PlayerDamageListener(PreventStabby plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityDamageEvent(EntityDamageByEntityEvent event) {
Entity attacker = event.getDamager();
Entity victim = event.getEntity();
DamageCheckResult result = plugin.getPlayerManager().canDamage(attacker, victim);
if (!result.ableToDamage()) {
event.setCancelled(true);
}
plugin.getPlayerManager().handleDamageCheck(result);
}
}
@@ -1,138 +0,0 @@
package me.youhavetrouble.preventstabby.util;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.config.ConfigCache;
import me.youhavetrouble.preventstabby.players.PlayerData;
import me.youhavetrouble.preventstabby.players.PlayerManager;
import org.bukkit.entity.*;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.UUID;
public class DamageCheck {
private final ConfigCache config;
private final PlayerManager playerManager;
public DamageCheck(PreventStabby plugin) {
this.config = plugin.getConfigCache();
this.playerManager = plugin.getPlayerManager();
}
public DamageCheckResult canDamage(@NotNull Entity attacker, @NotNull Entity victim) {
Target attackerData = getUuidOfActualPlayer(attacker);
Target victimData = getUuidOfActualPlayer(victim);
if (attackerData == null || victimData == null) return new DamageCheckResult(true, null, null);
return canDamage(attackerData.playerUuid, victimData.playerUuid, victimData.classifier);
}
public DamageCheckResult canDamage(UUID attackerId, UUID victimId, EntityClassifier victimClassifier) {
if (attackerId == null || victimId == null) return new DamageCheckResult(true, null, null);
PlayerData attackerPlayerData = PreventStabby.getPlugin().getPlayerManager().getPlayer(attackerId);
PlayerData victimPlayerData = PreventStabby.getPlugin().getPlayerManager().getPlayer(victimId);
if (attackerPlayerData.isProtected()) {
String message = switch (victimClassifier) {
case PLAYER -> config.cannotAttackTeleportOrSpawnProtectionAttacker;
case PET -> config.cannotAttackPetsTeleportOrSpawnProtectionAttacker;
case MOUNT -> config.cannotAttackMountsTeleportOrSpawnProtectionAttacker;
default -> null;
};
return new DamageCheckResult(false, message, null);
}
if (victimPlayerData.isProtected()) {
String message = null;
if (victimClassifier == EntityClassifier.PLAYER) {
message = config.cannotAttackTeleportOrSpawnProtectionVictim;
}
return new DamageCheckResult(false, message, null);
}
return switch (playerManager.getForcedPvpState()) {
case DISABLED -> new DamageCheckResult(false, config.cannotAttackForcedPvpOff, null);
case ENABLED -> new DamageCheckResult(true, null, null);
default -> new DamageCheckResult(true, null, null);
};
}
/**
* Get the UUID of the actual player, being owner of a pet, shooter of a projectile, etc.
*
* @param entity Base entity to get the player UUID from
* @return UUID of the actual player, null if not found
*/
@Nullable
public Target getUuidOfActualPlayer(@NotNull Entity entity) {
if (entity instanceof Player) return new Target(entity.getUniqueId(), EntityClassifier.PLAYER);
// Get shooter of projectile
if (entity instanceof Projectile projectile) {
if (projectile.getShooter() instanceof Player shooter) {
return new Target(shooter.getUniqueId(), EntityClassifier.PLAYER);
}
}
// Get player riding mount
if (!entity.getPassengers().isEmpty()) {
Entity passenger = entity.getPassengers().get(0);
if (passenger instanceof Player) {
return new Target(passenger.getUniqueId(), EntityClassifier.MOUNT);
}
}
// Get owner of tamed entity
if (entity instanceof Tameable tameable) {
if (tameable.getOwner() != null) {
return new Target(tameable.getOwner().getUniqueId(), EntityClassifier.PET);
}
}
return null;
}
public static class DamageCheckResult {
public final boolean ableToDamage;
public final String feedbackForAttacker, feedbackForVictim;
private DamageCheckResult(
boolean ableToDamage,
@Nullable String feedbackForAttacker,
@Nullable String feedbackForVictim
) {
this.ableToDamage = ableToDamage;
this.feedbackForAttacker = feedbackForAttacker;
this.feedbackForVictim = feedbackForVictim;
}
}
public static class Target {
/**
* The unique identifier for a player.
*/
public final UUID playerUuid;
/**
* Represents the entity classifier of a target.
* This indicates what type of entity player's id was assumed from
*/
public final EntityClassifier classifier;
private Target(UUID uuid, EntityClassifier classifier) {
this.playerUuid = uuid;
this.classifier = classifier;
}
}
public enum EntityClassifier {
PLAYER,
PET,
MOUNT,
OTHER
}
}
@@ -1,7 +1,7 @@
package me.youhavetrouble.preventstabby.util;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.players.PlayerData;
import me.youhavetrouble.preventstabby.data.PlayerData;
import java.io.File;
import java.sql.*;
@@ -2,6 +2,7 @@ package me.youhavetrouble.preventstabby.util;
import me.clip.placeholderapi.PlaceholderAPI;
import me.youhavetrouble.preventstabby.PreventStabby;
import me.youhavetrouble.preventstabby.data.DamageCheckResult;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@@ -69,6 +70,15 @@ public class PluginMessages {
Bukkit.broadcast(parseMessage(message));
}
public static void sendOutMessages(DamageCheckResult damageCheckResult) {
if (damageCheckResult.victimId() != null && damageCheckResult.feedbackForVictim() != null) {
sendActionBar(damageCheckResult.victimId(), damageCheckResult.feedbackForVictim());
}
if (damageCheckResult.attackerId() != null && damageCheckResult.feedbackForAttacker() != null) {
sendActionBar(damageCheckResult.attackerId(), damageCheckResult.feedbackForAttacker());
}
}
/**
* Swaps most legacy color codes to adventure minimessage tags.
* @param symbol Usually '&'.
+1 -1
View File
@@ -2,7 +2,7 @@ name: PreventStabby
version: ${project.version}
main: me.youhavetrouble.preventstabby.PreventStabby
authors: [YouHaveTrouble]
api-version: 1.13
api-version: 1.20
description: Stop people from getting stabbed!
soft-depend:
- WorldGuard