diff --git a/Server/src/main/content/global/handlers/PvPLogoutHandler.kt b/Server/src/main/content/global/handlers/PvPLogoutHandler.kt deleted file mode 100644 index 4e0d26f5c..000000000 --- a/Server/src/main/content/global/handlers/PvPLogoutHandler.kt +++ /dev/null @@ -1,33 +0,0 @@ -package content.global.handlers - -import core.api.LogoutListener -import core.game.node.entity.player.Player -import core.game.node.entity.player.link.PvPStateAudit - -/** - * Global logout handler that ensures PvP state is always cleaned up on logout. - * This is a safety net to prevent PvP state leakage (Falador Massacre-style bugs). - * - * Even if a minigame's leave() or logout() method fails to clean up properly, - * this handler ensures the player's PvP flags are reset before they rejoin. - * - * This works alongside PvPStateAudit (which runs periodically) to provide - * comprehensive protection against PvP state leaking out of designated areas. - * - * @see core.game.node.entity.player.link.PvPStateAudit - * @see core.api.PvPZone - */ -class PvPLogoutHandler : LogoutListener { - - /** - * Called when a player logs out. - * Always cleans up PvP state as a final safety measure. - */ - override fun logout(player: Player) { - // Always clean up PvP state on logout as final safety net. - // This ensures that even if a minigame's leave() method fails, - // the player won't have PvP flags enabled when they log back in. - PvPStateAudit.cleanupPvPState(player) - } -} - diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt index 0d1ed3d76..5743682d1 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt @@ -51,10 +51,6 @@ abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener { // Remove teleblock removeTimer(player, "teleblock") - // TODO: PvPStateAudit blows, has gotta go and actual safe handling of PvP generalized to cover current inauthentic deep wildy, - // future authentic minigames and that wont result in 7000 exploits that ruin the server (yes. thats a good idea. who said that) - // PvPStateAudit.cleanupPvPState(player) - player.equipment.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) player.inventory.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) (player.familiarManager.familiar as? BurdenBeast)?.container?.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt index 3db8b03f2..e037c5240 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt @@ -7,7 +7,9 @@ import core.game.component.Component import core.game.event.EventHook import core.game.event.InterfaceCloseEvent import core.game.interaction.Option +import core.game.node.Node import core.game.node.entity.Entity +import core.game.node.entity.combat.CombatStyle import core.game.node.entity.player.Player import core.game.node.item.Item import core.game.world.map.Location @@ -20,7 +22,7 @@ import org.rs09.consts.Components /** * Handles the Castle Wars game map */ -class CastleWarsGameArea : CastleWarsArea(), TickListener, PvPZone { +class CastleWarsGameArea : CastleWarsArea(), TickListener { companion object { private val saradominStandardFloor: ZoneBorders = @@ -342,46 +344,21 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, PvPZone { return handleDeath(entity, killer) } - // ==================== PvPZone Implementation ==================== TODO: PvPZone general review... it might be good but need to triple-check - - /** - * Check if attacker can attack victim in this zone. - * Only allows attacks between players on opposite teams. - * Uses self-healing team detection based on equipped cloak to ensure - * players are properly tracked even if they weren't added to sets correctly. - * @param attacker The player initiating the attack - * @param victim The player being attacked - * @return true if players are on opposite teams, false otherwise - */ - override fun canAttackPlayer(attacker: Player, victim: Player): Boolean { - if (ticksLeftInGame <= 0) return false + override fun isPvpAllowed(entity: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean? { + val attacker = entity as? Player ?: return null + val victim = target as? Player ?: return null + if (ticksLeftInGame <= 0) { + return false + } val attackerTeam = CastleWarsGameState.getPlayerTeam(attacker) ?: return false val victimTeam = CastleWarsGameState.getPlayerTeam(victim) ?: return false - return attackerTeam != victimTeam - } - - /** - * Check combat level restrictions. - * Castle Wars has no combat level restrictions. - */ - override fun checkCombatLevel(attacker: Player, victim: Player): Boolean { + if (attackerTeam == victimTeam) { + if (message) { + sendMessage(attacker, "You can only attack players on the opposite team!") + } + return false + } return true } - /** - * Get the message to display when attack is blocked. - * @return The message string - */ - override fun getBlockedAttackMessage(): String { - return "You can only attack players on the opposite team!" // TODO: pretty sure this is inauthentic. seen the real one in vids, needs correction here (also does this even get called huh) - } - - /** - * Get the message to display when attack is blocked by combat level check. - * @return The message string - */ - override fun getCombatLevelBlockedMessage(): String { - return PvPZone.DEFAULT_COMBAT_LEVEL_MESSAGE - } - } diff --git a/Server/src/main/core/api/MapArea.kt b/Server/src/main/core/api/MapArea.kt index 28631f54d..e138d975c 100644 --- a/Server/src/main/core/api/MapArea.kt +++ b/Server/src/main/core/api/MapArea.kt @@ -1,6 +1,8 @@ package core.api +import core.game.node.Node import core.game.node.entity.Entity +import core.game.node.entity.combat.CombatStyle import core.game.world.map.Location import core.game.world.map.zone.MapZone import core.game.world.map.zone.RegionZone @@ -25,6 +27,18 @@ interface MapArea : ContentInterface { fun areaEnter(entity: Entity) {} fun areaLeave(entity: Entity, logout: Boolean) {} fun entityStep(entity: Entity, location: Location, lastLocation: Location) {} + /** + * Extends MapZone to cater to pvp, team games and all other unique targeting systems. + * Return true/false if you want to override the controls. + * Return null if you want the default check in MapZone. (default) + **/ + fun isPvpAllowed(entity: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean? { return null } + /** + * Extends MapZone to cater to pvp, team games and all other unique targeting systems. + * Return true/false if you want to allow the deaths, especially false for "safe" areas. + * Return null if you want the default check in MapZone. (default) + **/ + fun canStartDeath(entity: Entity, killer: Entity): Boolean? { return null } companion object { val zoneMaps = HashMap() diff --git a/Server/src/main/core/api/PvPCombatUtils.kt b/Server/src/main/core/api/PvPCombatUtils.kt deleted file mode 100644 index ea904442f..000000000 --- a/Server/src/main/core/api/PvPCombatUtils.kt +++ /dev/null @@ -1,76 +0,0 @@ -package core.api - -import core.game.node.entity.player.Player -import kotlin.math.abs - -/** - * Utility object providing combat level range checking for PvP zones. - * Used by zones that implement the PvPZone interface to determine - * if two players are within the allowed combat level range for an area. - * - * @see PvPZone - */ -object PvPCombatUtils { - /** - * Check if two players are within a combat level range. - * Used for wilderness-style level restrictions where the allowed - * combat level difference is determined by the area's level. - * - * @param attacker The player initiating the attack - * @param victim The player being attacked - * @param range The maximum allowed combat level difference - * @return true if within allowed range, false otherwise - */ - @JvmStatic - fun withinCombatRange(attacker: Player, victim: Player, range: Int): Boolean { - val attackerLevel = attacker.properties.currentCombatLevel - val victimLevel = victim.properties.currentCombatLevel - return abs(attackerLevel - victimLevel) <= range - } - - /** - * Calculate wilderness-style combat range based on wilderness level. - * In the wilderness, the combat level range equals the wilderness level. - * For example, in level 5 wilderness, you can attack players within - * 5 combat levels of your own level. - * - * @param wildernessLevel The wilderness level of the area - * @return The combat level range (same as wilderness level) - */ - @JvmStatic - fun getWildernessStyleRange(wildernessLevel: Int): Int { - return wildernessLevel - } - - /** - * Check if combat between two players is allowed based on wilderness-style rules. - * Combines the wilderness level lookup with combat range checking. - * - * @param attacker The player initiating the attack - * @param victim The player being attacked - * @param wildernessLevel The wilderness level of the area - * @return true if combat is allowed, false otherwise - */ - @JvmStatic - fun checkWildernessStyleCombat(attacker: Player, victim: Player, wildernessLevel: Int): Boolean { - val range = getWildernessStyleRange(wildernessLevel) - return withinCombatRange(attacker, victim, range) - } - - /** - * Get a formatted message explaining why combat was blocked due to level difference. - * - * @param attacker The attacking player - * @param victim The victim player - * @param allowedRange The maximum allowed combat level difference - * @return A descriptive message explaining the level restriction - */ - @JvmStatic - fun getCombatLevelBlockedMessage(attacker: Player, victim: Player, allowedRange: Int): String { - val attackerLevel = attacker.properties.currentCombatLevel - val victimLevel = victim.properties.currentCombatLevel - val actualDifference = abs(attackerLevel - victimLevel) - return "The combat level difference ($actualDifference) exceeds the allowed range ($allowedRange)." - } -} - diff --git a/Server/src/main/core/api/PvPZone.kt b/Server/src/main/core/api/PvPZone.kt deleted file mode 100644 index 628e1b5d2..000000000 --- a/Server/src/main/core/api/PvPZone.kt +++ /dev/null @@ -1,51 +0,0 @@ -package core.api - -import core.game.node.entity.player.Player - -/** - * Interface for zones that allow player-vs-player combat. - * Zones implementing this interface must explicitly define PvP rules. - * This provides a centralized, explicit mechanism for PvP permission checking - * to prevent combat state leakage (e.g., Falador Massacre-style bugs). - */ -interface PvPZone { - /** - * Check if attacker can attack victim in this zone. - * This is the primary permission check for PvP combat. - * @param attacker The player initiating the attack - * @param victim The player being attacked - * @return true if attack is permitted, false otherwise - */ - fun canAttackPlayer(attacker: Player, victim: Player): Boolean - - /** - * Check if combat level restrictions apply between two players. - * Override this for zones with level-based restrictions (e.g., wilderness level range). - * @param attacker The player initiating the attack - * @param victim The player being attacked - * @return true if within allowed combat level range, false otherwise - */ - fun checkCombatLevel(attacker: Player, victim: Player): Boolean - - /** - * Get the message to display when attack is blocked by canAttackPlayer(). - * @return The message string to send to the attacking player - */ - fun getBlockedAttackMessage(): String - - /** - * Get the message to display when attack is blocked by combat level check. - * @return The message string to send to the attacking player - */ - fun getCombatLevelBlockedMessage(): String - - /** - * Companion object providing default values for the interface methods. - * Use these in Java implementations to get default behavior. - */ - companion object { - const val DEFAULT_BLOCKED_MESSAGE = "You cannot attack that player here." // TODO: correct - const val DEFAULT_COMBAT_LEVEL_MESSAGE = "The level difference between you and your opponent is too great." - } -} - diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index 3381df361..23abb9281 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -47,11 +47,8 @@ import core.game.system.task.Pulse; import core.game.world.map.*; import core.game.world.map.build.DynamicRegion; import core.game.world.map.path.Pathfinder; -import core.game.world.map.zone.RegionZone; import core.game.world.map.zone.ZoneRestriction; import core.game.world.map.zone.ZoneType; -import core.api.MapArea; -import core.api.PvPZone; import core.game.node.entity.combat.DeathContext; import core.game.world.update.flag.PlayerFlags; import core.game.world.update.flag.*; @@ -614,7 +611,6 @@ public class Player extends Entity { @Override public void commenceDeath(Entity killer) { if (!isPlaying()) return; - // TODO: review DeathContext, PvPStateAudit, PvPZone and related components. theyre all cool but also... cant this break a lot of shit? 100%. does it? uhhh... lemme check (later DeathContext context = new DeathContext( getLocation(), getZoneMonitor().getType(), @@ -794,26 +790,6 @@ public class Player extends Entity { } if (entity instanceof Player) { Player attacker = (Player) entity; - - // NEW: Check for PvPZone-based permission (minigames, etc.) - PvPZone pvpZone = findCommonPvPZone(attacker, this); - if (pvpZone != null) { - if (!pvpZone.canAttackPlayer(attacker, this)) { - if (message) { - attacker.getPacketDispatch().sendMessage(pvpZone.getBlockedAttackMessage()); - } - return false; - } - if (!pvpZone.checkCombatLevel(attacker, this)) { - if (message) { - attacker.getPacketDispatch().sendMessage(pvpZone.getCombatLevelBlockedMessage()); - } - return false; - } - return true; - } - - // LEGACY: Wilderness fallback (kept for backward compatibility) if (attacker.getSkullManager().isWilderness() && skullManager.isWilderness()) { if (!GameWorld.getSettings().getWild_pvp_enabled()) return false; @@ -821,77 +797,13 @@ public class Player extends Entity { return false; if (skullManager.hasWildernessProtection()) return false; - return super.isAttackable(entity, style, message); - } else return false; + } else if (!getZoneMonitor().isPvPable(entity, this, style, message)) { + return false; + } } return super.isAttackable(entity, style, message); } - /** - * Find a PvPZone that both the attacker and this player (victim) are in. - * @param attacker The attacking player - * @param victim The player being attacked (this) - * @return The common PvPZone, or null if no common zone exists - */ - private PvPZone findCommonPvPZone(Player attacker, Player victim) { - // First check: Direct PvPZone implementations (e.g., ActivityPlugin subclasses) - for (RegionZone zone : attacker.getZoneMonitor().getZones()) { - if (zone.getZone() instanceof PvPZone) { - for (RegionZone victimZone : victim.getZoneMonitor().getZones()) { - if (zone.getZone() == victimZone.getZone()) { - return (PvPZone) zone.getZone(); - } - } - } - } - - // Second check: MapArea implementations that implement PvPZone - // ClassScanner wraps MapAreas in anonymous MapZone classes, so we need to - // look up the original MapArea from MapArea.zoneMaps - for (RegionZone zone : attacker.getZoneMonitor().getZones()) { - String zoneName = zone.getZone().getName(); - // MapArea zones are named with "MapArea" suffix by ClassScanner - if (zoneName != null && zoneName.endsWith("MapArea")) { - // Look up the original MapArea class that implements PvPZone - for (java.util.Map.Entry entry : MapArea.Companion.getZoneMaps().entrySet()) { - if (entry.getKey().equals(zoneName) && entry.getValue() == zone.getZone()) { - // Find the MapArea instance - we need to check all loaded ContentInterfaces - // The MapArea interface stores zones but not the instances themselves - // We need to iterate GameWorld's content to find the matching MapArea - PvPZone pvpZone = findMapAreaPvPZone(zoneName, attacker, victim); - if (pvpZone != null) { - return pvpZone; - } - } - } - } - } - - return null; - } - - /** - * Find a MapArea that implements PvPZone and matches the zone name. - * This is needed because ClassScanner wraps MapAreas in anonymous classes. - */ - private PvPZone findMapAreaPvPZone(String zoneName, Player attacker, Player victim) { - // Check TickListeners - MapAreas often implement TickListener - for (Object listener : GameWorld.getTickListeners()) { - if (listener instanceof MapArea && listener instanceof PvPZone) { - String mapAreaZoneName = listener.getClass().getSimpleName() + "MapArea"; - if (mapAreaZoneName.equals(zoneName)) { - // Verify victim is also in this zone - for (RegionZone victimZone : victim.getZoneMonitor().getZones()) { - if (victimZone.getZone().getName().equals(zoneName)) { - return (PvPZone) listener; - } - } - } - } - } - return null; - } - @Override public boolean continueAttack(Entity target, CombatStyle style, boolean message) { if (target instanceof NPC) { @@ -913,7 +825,7 @@ public class Player extends Entity { return false; } return !skullManager.hasWildernessProtection(); - } else { + } else if (!getZoneMonitor().isPvPable(this, target, style, message)) { return false; } } diff --git a/Server/src/main/core/game/node/entity/player/link/PvPStateAudit.kt b/Server/src/main/core/game/node/entity/player/link/PvPStateAudit.kt deleted file mode 100644 index ffc711add..000000000 --- a/Server/src/main/core/game/node/entity/player/link/PvPStateAudit.kt +++ /dev/null @@ -1,67 +0,0 @@ -package core.game.node.entity.player.link - -import core.api.PvPZone -import core.api.TickListener -import core.api.log -import core.game.interaction.Option -import core.game.node.entity.player.Player -import core.game.world.map.zone.ZoneRestriction -import core.game.world.map.zone.impl.WildernessZone -import core.game.world.repository.Repository -import core.tools.Log - -/** - * Periodic audit that detects and fixes PvP state leakage. - * Runs every 10 ticks (6 seconds) to catch players who have - * PvP flags enabled outside valid PvP zones. - */ -class PvPStateAudit : TickListener { - private var tickCounter = 0 - - override fun tick() { - tickCounter++ - if (tickCounter < 10) return - tickCounter = 0 - - for (player in Repository.players) { - if (player == null || !player.isActive || player.isArtificial) continue - auditPlayer(player) - } - } - - private fun auditPlayer(player: Player) { - val hasPvPEnabled = player.skullManager.isWilderness - val inPvPZone = isInValidPvPZone(player) - if (hasPvPEnabled && !inPvPZone) { - log(this::class.java, Log.WARN, - "Player ${player.username} has PvP enabled but is not in a PvP zone! " + - "Location: ${player.location}, Cleaning up state.") - cleanupPvPState(player) - } - } - - private fun isInValidPvPZone(player: Player): Boolean { - for (zone in player.zoneMonitor.zones) { - if (zone.zone is PvPZone) return true - } - if (player.zoneMonitor.isRestricted(ZoneRestriction.PVP_ZONE)) return true - return WildernessZone.isInZone(player) - } - - companion object { - /** - * Clean up all PvP-related state from a player. - * Called when a player is found to have PvP flags enabled outside a PvP zone. - */ - @JvmStatic - fun cleanupPvPState(player: Player) { - player.skullManager.isWilderness = false - player.skullManager.isSkullCheckDisabled = false - player.skullManager.level = 0 - player.properties.isSafeZone = false - player.properties.isMultiZone = false - player.interaction.remove(Option._P_ATTACK) - } - } -} - diff --git a/Server/src/main/core/game/world/map/zone/MapZone.java b/Server/src/main/core/game/world/map/zone/MapZone.java index 7adb16dfc..752598f6f 100644 --- a/Server/src/main/core/game/world/map/zone/MapZone.java +++ b/Server/src/main/core/game/world/map/zone/MapZone.java @@ -128,7 +128,21 @@ public abstract class MapZone implements Zone { public boolean actionButton(Player player, int interfaceId, int buttonId, int slot, int itemId, int opcode) { return false; } - + + /** + * Checks if the entity is able to continue attacking another player within this zone. + * This is added like continueAttack for player vs player fighting. + * + * @param e the attacking entity + * @param target the target node being attacked + * @param style the combat style being used + * @param message whether to send a message to the player explaining why the attack cannot continue + * @return {@code true} if the zone allows the attack to continue + */ + public boolean continuePvp(Entity e, Node target, CombatStyle style, boolean message) { + return false; + } + /** * Checks if the entity is able to continue attacking the target within this zone. * diff --git a/Server/src/main/core/game/world/map/zone/ZoneMonitor.java b/Server/src/main/core/game/world/map/zone/ZoneMonitor.java index d3e7de92e..4fabe5995 100644 --- a/Server/src/main/core/game/world/map/zone/ZoneMonitor.java +++ b/Server/src/main/core/game/world/map/zone/ZoneMonitor.java @@ -1,7 +1,5 @@ package core.game.world.map.zone; -import core.api.MapArea; -import core.api.PvPZone; import core.game.interaction.Option; import core.game.node.Node; import core.game.node.entity.Entity; @@ -10,7 +8,6 @@ import core.game.node.entity.player.Player; import core.game.node.entity.player.link.music.MusicZone; import core.game.node.entity.player.link.request.RequestType; import core.game.node.item.Item; -import core.game.world.GameWorld; import core.game.world.map.Location; import core.game.world.map.Region; import org.rs09.consts.Items; @@ -150,6 +147,23 @@ public final class ZoneMonitor { return false; } + /** + * Checks whether any current zone explicitly allows PvP against the target. + * + * @param player The attacking entity. + * @param target The target. + * @param style The combat style used. + * @return {@code True} if so. + */ + public boolean isPvPable(Entity player, Node target, CombatStyle style, boolean message) { + for (RegionZone z : zones) { + if (z.getZone().continuePvp(player, target, style, message)) { + return true; + } + } + return false; + } + /** * Checks if the entity is able to continue attacking the target. * @@ -168,72 +182,12 @@ public final class ZoneMonitor { return false; } } - if (entity instanceof Player && target instanceof Player) { - Player attacker = (Player) entity; - Player victim = (Player) target; - // Skip wilderness check if both players are in a PvPZone (e.g., Castle Wars, Duel Arena) - // PvPZone handles its own attack permissions via canAttackPlayer() - if (!isInCommonPvPZone(attacker, victim)) { - if (!attacker.getSkullManager().isWilderness() || !victim.getSkullManager().isWilderness()) { - if (message) { - attacker.getPacketDispatch().sendMessage("You can only attack other players in the wilderness."); - } - return false; - } - } - } if (target instanceof Entity && !MapZone.checkMulti(entity, (Entity) target, message)) { return false; } return true; } - /** - * Check if both players are in a common PvPZone. - * Used to bypass the wilderness check for minigames like Castle Wars and Duel Arena. - * - * @param attacker The attacking player - * @param victim The player being attacked - * @return true if both players share a PvPZone - */ - private boolean isInCommonPvPZone(Player attacker, Player victim) { - // Check for direct PvPZone implementations (e.g., ActivityPlugin subclasses) - for (RegionZone zone : attacker.getZoneMonitor().getZones()) { - if (zone.getZone() instanceof PvPZone) { - for (RegionZone victimZone : victim.getZoneMonitor().getZones()) { - if (zone.getZone() == victimZone.getZone()) { - return true; - } - } - } - } - // Check for MapArea-based PvPZones (ClassScanner wraps MapAreas in anonymous MapZone classes) - for (RegionZone zone : attacker.getZoneMonitor().getZones()) { - String zoneName = zone.getZone().getName(); - if (zoneName != null && zoneName.endsWith("MapArea")) { - // Look up the original MapArea instance from TickListeners - for (Object listener : GameWorld.getTickListeners()) { - if (listener instanceof MapArea && listener instanceof PvPZone) { - String mapAreaZoneName = listener.getClass().getSimpleName() + "MapArea"; - if (mapAreaZoneName.equals(zoneName)) { - for (RegionZone victimZone : victim.getZoneMonitor().getZones()) { - if (victimZone.getZone().getName().equals(zoneName)) { - return true; - } - } - } - } - } - } - } - // Check for PVP_ZONE restriction flag - if (attacker.getZoneMonitor().isRestricted(ZoneRestriction.PVP_ZONE) && - victim.getZoneMonitor().isRestricted(ZoneRestriction.PVP_ZONE)) { - return true; - } - return false; - } - /** * Checks if the entity can interact with the target. * diff --git a/Server/src/main/core/game/world/map/zone/ZoneRestriction.java b/Server/src/main/core/game/world/map/zone/ZoneRestriction.java index 0d34313f1..64dd971db 100644 --- a/Server/src/main/core/game/world/map/zone/ZoneRestriction.java +++ b/Server/src/main/core/game/world/map/zone/ZoneRestriction.java @@ -47,20 +47,6 @@ public enum ZoneRestriction { * Dynamic regions are implicitly off-map and do not require this attribute. */ OFF_MAP, - - /** - * This zone allows player-vs-player combat. - * Used to mark zones where PvP is explicitly permitted. - * Zones with this restriction should implement the PvPZone interface - * to define specific combat rules (who can attack whom, combat level ranges, etc.). - * - * This flag is used by PvPStateAudit as an additional validation mechanism - * to ensure PvP combat state doesn't leak out of designated areas. - * - * @see core.api.PvPZone - * @see core.game.node.entity.player.link.PvPStateAudit - */ - PVP_ZONE, ; /** diff --git a/Server/src/main/core/plugin/ClassScanner.kt b/Server/src/main/core/plugin/ClassScanner.kt index 2ad6e5f4b..725a5427c 100644 --- a/Server/src/main/core/plugin/ClassScanner.kt +++ b/Server/src/main/core/plugin/ClassScanner.kt @@ -7,7 +7,9 @@ import core.game.activity.ActivityPlugin import core.game.bots.PlayerScripts import core.game.interaction.InteractionListener import core.game.interaction.InterfaceListener +import core.game.node.Node import core.game.node.entity.Entity +import core.game.node.entity.combat.CombatStyle import core.game.node.entity.npc.NPCBehavior import core.game.node.entity.player.info.login.LoginConfiguration import core.game.node.entity.player.info.login.PlayerSaveParser @@ -128,6 +130,29 @@ object ClassScanner { if(e != null && from != null && to != null) clazz.entityStep(e, to, from) return super.move(e, from, to) } + + // Additional hook for continueAttack for PVP area controls + override fun continuePvp(e: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean { + val clazzAttack = clazz.isPvpAllowed(e, target, style, message) + if (clazzAttack == null) { // If clazz returns null, continue the usual check. + return super.continuePvp(e, target, style, message) + } else { + return clazzAttack // Otherwise return whatever function. + } + } + + // Additional hook for startDeath for PVP area controls + override fun startDeath(e: Entity?, killer: Entity?): Boolean { + if (e == null) { + return super.startDeath(null, killer) + } + val clazzAttack = clazz.canStartDeath(e, killer) + if (clazzAttack == null) { // If clazz returns null, continue the usual check. + return super.startDeath(e, killer) + } else { + return clazzAttack // Otherwise return whatever function. + } + } } for(border in clazz.defineAreaBorders()) zone.register(border) ZoneBuilder.configure(zone)