added reflection on bluemap removal and /nopvp tweaks so its harder to abuse

This commit is contained in:
Martin Dvořák 2026-09-27 17:01:00 +00:00
commit 3d5ebd8a9b
5 changed files with 93 additions and 8 deletions

View file

@ -5,7 +5,7 @@ A quality-of-life SMP plugin for Paper/Folia.
## Features
- **Join messages** — a "Welcome to the SMP" title on every join, friendly welcome messages (including a first-join message that lists the server's plugins and a warning that `/nopvp` abuse is bannable and that combat logging is a bad idea), and a special announcement when the owner logs in.
- **`/nopvp`** — toggle no-PvP mode. While active you cannot damage other players and other players cannot damage you (direct hits and projectiles). While enabled you also glow so everyone can see you're protected. Turning no-PvP *off* starts a **1-hour cooldown** before you can enable it again, and the state persists across restarts so relogging can't bypass it.
- **`/nopvp`** — toggle no-PvP mode. While active you cannot damage other players and other players cannot damage you (direct hits, projectiles, and player-deployed explosives such as TNT, TNT minecarts, end crystals and bed bombs). While enabled you also glow so everyone can see you're protected. Turning no-PvP *off* starts a **1-hour cooldown** before you can enable it again, and the state persists across restarts so relogging can't bypass it.
- **Anti-combat-logging** — whenever you hit another player or get hit by one, you're tagged as being in combat for **15 seconds** with a chat warning. Logging off while tagged kills you on the spot and announces it to the server.
## Commands

View file

@ -9,7 +9,7 @@ repositories {
}
dependencies {
compileOnly("io.papermc.paper:paper-api:26.2.build.+")
compileOnly("io.papermc.paper:paper-api:26.1.2.build.+")
}
java {
@ -21,7 +21,7 @@ tasks {
// Configure the Minecraft version for our task.
// This is the only required configuration besides applying the plugin.
// Your plugin's jar (or shadowJar if present) will be used automatically.
minecraftVersion("26.2")
minecraftVersion("26.1.2")
jvmArgs("-Xms2G", "-Xmx2G")
}

View file

@ -15,7 +15,6 @@ public class JoinListener implements Listener {
if (!player.hasPlayedBefore()) {
player.sendMessage("§aWelcome to the SMP, " + player.getName() + "!");
player.sendMessage("This is your first time here, so let me tell you whats on here:");
player.sendMessage("§7- a 3d map of the server at https://bluemap.dvorakmortonxd.dev");
player.sendMessage("§7- a voicechat you can optionally install as a mod (Simple Voice Chat)");
player.sendMessage("§7- a tree chopper plugin you can toggle with /atc");
player.sendMessage("§7- veinminer, which you can also toggle with /veinminer toggle");

View file

@ -38,7 +38,7 @@ public final class NoPvPCommand implements CommandExecutor {
}
manager.enable(player);
player.sendMessage("§aYou enabled no-PvP. Other players can no longer damage you, and you can no longer damage other players.");
player.sendMessage("§aYou enabled no-PvP. Other players can no longer damage you (including TNT, TNT minecarts, crystals and bed bombs), and you can no longer damage other players.");
return true;
}

View file

@ -1,12 +1,18 @@
package com.dvorakmortonxd.qoltweaks.nopvp;
import org.bukkit.damage.DamageSource;
import org.bukkit.entity.EnderCrystal;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.entity.minecart.ExplosiveMinecart;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
@ -28,22 +34,74 @@ public final class NoPvPListener implements Listener {
manager.applyGlow(event.getPlayer());
}
/**
* Catches explosion damage that has no damager entity attached, most notably
* bed bombs and respawn anchors in the Nether/End ({@code BLOCK_EXPLOSION}).
* Entity-caused damage is handled by {@link #onEntityDamageByEntity(EntityDamageByEntityEvent)}.
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityDamage(EntityDamageEvent event) {
if (event instanceof EntityDamageByEntityEvent) {
return;
}
if (!(event.getEntity() instanceof Player victim) || !manager.isEnabled(victim)) {
return;
}
if (isPvpExplosion(event)) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
if (!(event.getEntity() instanceof Player victim)) {
return;
}
Player attacker = resolveAttacker(event.getDamager());
if (attacker == null) {
Player attacker = resolveAttacker(event);
boolean attackerIsOther = attacker != null
&& !attacker.getUniqueId().equals(victim.getUniqueId());
// A no-PvP player can never hurt another player.
if (attackerIsOther && manager.isEnabled(attacker)) {
event.setCancelled(true);
return;
}
if (manager.isEnabled(attacker) || manager.isEnabled(victim)) {
// A no-PvP player can't be hurt by another player (direct hits or projectiles).
if (attackerIsOther && manager.isEnabled(victim)) {
event.setCancelled(true);
return;
}
// ...or by player-deployable explosives (TNT, TNT minecarts, end crystals)
// even when no player can be attributed as the attacker.
if (manager.isEnabled(victim) && isPvpExplosion(event)) {
event.setCancelled(true);
}
}
/**
* Resolves the player ultimately responsible for a damage event, preferring
* the modern {@link DamageSource} attribution (which knows who lit the TNT,
* who detonated a crystal, etc.) and falling back to the raw damager.
*/
private Player resolveAttacker(EntityDamageEvent event) {
DamageSource source = event.getDamageSource();
Player responsible = asPlayer(source.getCausingEntity());
if (responsible != null) {
return responsible;
}
responsible = asPlayer(source.getDirectEntity());
if (responsible != null) {
return responsible;
}
if (event instanceof EntityDamageByEntityEvent byEntity) {
return resolveAttacker(byEntity.getDamager());
}
return null;
}
private Player resolveAttacker(Entity damager) {
if (damager instanceof Player player) {
return player;
@ -51,6 +109,34 @@ public final class NoPvPListener implements Listener {
if (damager instanceof Projectile projectile && projectile.getShooter() instanceof Player player) {
return player;
}
// Primed TNT remembers the entity that set it off.
if (damager instanceof TNTPrimed tnt && tnt.getSource() instanceof Player player) {
return player;
}
return null;
}
private Player asPlayer(Entity entity) {
return entity instanceof Player player ? player : null;
}
/**
* Whether this damage comes from an explosive that players can deploy against
* each other: TNT blocks, TNT minecarts ("carting"), end crystals and
* bed/respawn-anchor explosions.
*/
private boolean isPvpExplosion(EntityDamageEvent event) {
DamageCause cause = event.getCause();
if (cause == DamageCause.BLOCK_EXPLOSION) {
// Bed bombs and respawn anchors (also TNT blocks) are block explosions.
return true;
}
if (cause != DamageCause.ENTITY_EXPLOSION) {
return false;
}
Entity direct = event.getDamageSource().getDirectEntity();
return direct instanceof EnderCrystal
|| direct instanceof ExplosiveMinecart
|| direct instanceof TNTPrimed;
}
}