Resolve direct native damage sources and explicitly observed poison/wither provenance, including hidden layers and effective replacements. Preserve progression and potion effects while reusing identity/session cleanup. Verify native effect application, ticks, tab codecs and plugin registration. No deployment is included.
105 lines
6.2 KiB
Java
105 lines
6.2 KiB
Java
package games.dmg.spigotstealth;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.Mockito.*;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
import net.minecraft.core.RegistryAccess;
|
|
import net.minecraft.core.registries.Registries;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.world.damagesource.DamageSource;
|
|
import net.minecraft.world.damagesource.DamageSources;
|
|
import net.minecraft.world.effect.MobEffectInstance;
|
|
import net.minecraft.world.effect.MobEffects;
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.craftbukkit.CraftRegistry;
|
|
import org.bukkit.craftbukkit.damage.CraftDamageSource;
|
|
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
|
import org.bukkit.event.entity.EntityDamageEvent;
|
|
import org.bukkit.event.entity.EntityPotionEffectEvent;
|
|
import org.bukkit.plugin.PluginManager;
|
|
import org.junit.jupiter.api.BeforeAll;
|
|
|
|
class NativePotionDamageTest {
|
|
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
|
|
|
@org.junit.jupiter.params.ParameterizedTest
|
|
@org.junit.jupiter.params.provider.ValueSource(booleans = {false, true})
|
|
@SuppressWarnings("try")
|
|
void actualNativeApplicationAndDamageTickKeepTheObservedPlayerSource(boolean wither) throws Exception {
|
|
var manager = mock(PluginManager.class);
|
|
try (var platform = mockStatic(Bukkit.class, call -> switch (call.getMethod().getName()) {
|
|
case "getPluginManager" -> manager;
|
|
case "isPrimaryThread" -> true;
|
|
default -> call.callRealMethod();
|
|
})) {
|
|
var victim = mock(CraftPlayer.class);
|
|
var attacker = mock(CraftPlayer.class);
|
|
UUID victimId = UUID.randomUUID(), attackerId = UUID.randomUUID();
|
|
when(victim.getUniqueId()).thenReturn(victimId);
|
|
when(attacker.getUniqueId()).thenReturn(attackerId);
|
|
var nativeVictim = mock(ServerPlayer.class);
|
|
var nativeAttacker = mock(ServerPlayer.class);
|
|
when(victim.getHandle()).thenReturn(nativeVictim);
|
|
when(nativeVictim.getBukkitEntity()).thenReturn(victim);
|
|
when(nativeVictim.getBukkitLivingEntity()).thenReturn(victim);
|
|
when(nativeAttacker.getBukkitEntity()).thenReturn(attacker);
|
|
var active = new HashMap<net.minecraft.core.Holder<net.minecraft.world.effect.MobEffect>, MobEffectInstance>();
|
|
var field = LivingEntity.class.getDeclaredField("activeEffects");
|
|
field.setAccessible(true);
|
|
field.set(nativeVictim, active);
|
|
when(nativeVictim.canBeAffected(any(MobEffectInstance.class))).thenReturn(true);
|
|
when(nativeVictim.getEffect(any())).thenAnswer(call -> active.get(call.getArgument(0)));
|
|
when(nativeVictim.addEffect(any(MobEffectInstance.class), any(net.minecraft.world.entity.Entity.class), any(EntityPotionEffectEvent.Cause.class), anyBoolean())).thenCallRealMethod();
|
|
var failures = new ArrayList<Throwable>();
|
|
var attribution = new PotionDamageListener(id -> id.equals(attackerId) ? attacker : null, failures::add);
|
|
var applications = new ArrayList<EntityPotionEffectEvent>();
|
|
doAnswer(call -> {
|
|
if (call.getArgument(0) instanceof EntityPotionEffectEvent event) {
|
|
applications.add(event);
|
|
attribution.onEffect(event);
|
|
}
|
|
return null;
|
|
}).when(manager).callEvent(any());
|
|
var type = wither ? MobEffects.WITHER : MobEffects.POISON;
|
|
var potion = new MobEffectInstance(type, wither ? 80 : 100, 0);
|
|
assertTrue(nativeVictim.addEffect(potion, nativeAttacker, EntityPotionEffectEvent.Cause.POTION_SPLASH, true));
|
|
assertSame(attacker, applications.getFirst().getSource());
|
|
assertSame(potion, active.get(type));
|
|
var world = mock(ServerLevel.class);
|
|
var config = mock(org.purpurmc.purpur.PurpurWorldConfig.class);
|
|
config.entityMinimalHealthPoison = 1;
|
|
config.entityPoisonDegenerationAmount = 1;
|
|
config.entityWitherDegenerationAmount = 1;
|
|
var worldConfig = net.minecraft.world.level.Level.class.getField("purpurConfig");
|
|
worldConfig.setAccessible(true);
|
|
worldConfig.set(world, config);
|
|
when(nativeVictim.level()).thenReturn(world);
|
|
when(nativeVictim.getHealth()).thenReturn(10f);
|
|
var sources = new DamageSources(new RegistryAccess.ImmutableRegistryAccess(List.of(CraftRegistry.getMinecraftRegistry(Registries.DAMAGE_TYPE))));
|
|
when(nativeVictim.damageSources()).thenReturn(sources);
|
|
var resolved = new ArrayList<UUID>();
|
|
doAnswer(call -> {
|
|
DamageSource raw = call.getArgument(1);
|
|
assertNull(raw.getEntity(), "native delayed damage does not carry the applying player's identity");
|
|
var event = new EntityDamageEvent(victim, wither ? EntityDamageEvent.DamageCause.WITHER : EntityDamageEvent.DamageCause.POISON,
|
|
new CraftDamageSource(raw), ((Float) call.getArgument(2)).doubleValue());
|
|
var responsible = attribution.apply(event);
|
|
assertNotNull(responsible, "observed native potion provenance must resolve the responsible player");
|
|
resolved.add(responsible.getUniqueId());
|
|
return true;
|
|
}).when(nativeVictim).hurtServer(eq(world), any(DamageSource.class), anyFloat());
|
|
assertTrue(potion.tickServer(world, nativeVictim, () -> { }));
|
|
assertEquals(List.of(attackerId), resolved);
|
|
attribution.onQuit(new org.bukkit.event.player.PlayerQuitEvent(victim, net.kyori.adventure.text.Component.empty()));
|
|
assertNull(attribution.apply(new EntityDamageEvent(victim, wither ? EntityDamageEvent.DamageCause.WITHER : EntityDamageEvent.DamageCause.POISON,
|
|
new CraftDamageSource(wither ? sources.wither() : sources.magic()), 1.0)), "disconnect must discard runtime-only victim provenance");
|
|
assertTrue(failures.isEmpty(), failures.toString());
|
|
}
|
|
}
|
|
}
|