Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package net.tfminecraft.rpcharacters.pvp;

import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;

/**
* Reports nonlethal knockouts to CoreProtect, which logs every hit between players but can't see
* a knockout itself because the knockout blow is cancelled. CoreProtect is reached by reflection
* so RPCharacters needs no load order against it, and works without it or with older versions.
*/
public final class CoreProtectKnockouts {
private static final Logger LOG = Logger.getLogger("RPCharacters");
private static boolean warned;

private CoreProtectKnockouts() {
}

/** Logs the knockout of {@code victim} by {@code attacker}; true if CoreProtect queued it. */
public static boolean log(Player attacker, Player victim) {
if (attacker == null || victim == null) {
return false;
}
try {
Plugin plugin = Bukkit.getPluginManager().getPlugin("CoreProtect");
if (plugin == null || !plugin.isEnabled()) {
return false;
}
Object api = plugin.getClass().getMethod("getAPI").invoke(plugin);
if (api == null) {
return false;
}
Method logKnockout = api.getClass().getMethod("logPvpKnockout", Player.class, Player.class);
return Boolean.TRUE.equals(logKnockout.invoke(api, attacker, victim));
} catch (ReflectiveOperationException | RuntimeException e) {
if (!warned) {
warned = true;
LOG.log(Level.WARNING, "[RPCharacters] CoreProtect could not log a knockout; it needs a version with logPvpKnockout", e);
}
return false;
}
}

static void resetWarning() {
warned = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ public void onLethalDamage(EntityDamageEvent event) {

event.setCancelled(true);
applyKnockout(player);
PvpStrikeService.handleKnockout(player, attackingPlayer(event));
Player attacker = attackingPlayer(event);
CoreProtectKnockouts.log(attacker, player);
PvpStrikeService.handleKnockout(player, attacker);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package net.tfminecraft.rpcharacters.pvp;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

class CoreProtectKnockoutsTest {
/** Stands in for CoreProtect's main class, which exposes its API through getAPI(). */
public abstract static class CoreProtectPlugin implements Plugin {
public abstract Object getAPI();
}

/** Stands in for a CoreProtect API with knockout logging. */
public static class KnockoutApi {
final List<Player[]> logged = new ArrayList<>();
boolean result = true;

public boolean logPvpKnockout(Player attacker, Player victim) {
logged.add(new Player[] { attacker, victim });
return result;
}
}

/** Stands in for a CoreProtect API from before knockout logging. */
public static class OldApi {
}

private MockedStatic<Bukkit> bukkit;
private PluginManager plugins;
private CoreProtectPlugin coreProtect;
private final Player attacker = mock(Player.class);
private final Player victim = mock(Player.class);
private final List<LogRecord> warnings = new ArrayList<>();
private final Handler capture = new Handler() {
@Override public void publish(LogRecord record) { warnings.add(record); }
@Override public void flush() { }
@Override public void close() { }
};

@BeforeEach
void setUp() {
CoreProtectKnockouts.resetWarning();
plugins = mock(PluginManager.class);
coreProtect = mock(CoreProtectPlugin.class);
when(coreProtect.isEnabled()).thenReturn(true);
when(plugins.getPlugin("CoreProtect")).thenReturn(coreProtect);
bukkit = mockStatic(Bukkit.class);
bukkit.when(Bukkit::getPluginManager).thenReturn(plugins);
Logger.getLogger("RPCharacters").addHandler(capture);
}

@AfterEach
void tearDown() {
Logger.getLogger("RPCharacters").removeHandler(capture);
bukkit.close();
}

@Test
void knockoutIsPassedToCoreProtectsApi() {
KnockoutApi api = new KnockoutApi();
when(coreProtect.getAPI()).thenReturn(api);
assertTrue(CoreProtectKnockouts.log(attacker, victim));
assertEquals(1, api.logged.size());
assertSame(attacker, api.logged.getFirst()[0]);
assertSame(victim, api.logged.getFirst()[1]);
api.result = false;
assertFalse(CoreProtectKnockouts.log(attacker, victim));
assertTrue(warnings.isEmpty());
}

@Test
void missingPlayersOrAMissingDisabledOrUnreadyCoreProtectLogNothing() {
assertFalse(CoreProtectKnockouts.log(null, victim));
assertFalse(CoreProtectKnockouts.log(attacker, null));
when(coreProtect.getAPI()).thenReturn(null);
assertFalse(CoreProtectKnockouts.log(attacker, victim));
when(coreProtect.isEnabled()).thenReturn(false);
assertFalse(CoreProtectKnockouts.log(attacker, victim));
when(plugins.getPlugin("CoreProtect")).thenReturn(null);
assertFalse(CoreProtectKnockouts.log(attacker, victim));
verify(coreProtect, times(1)).getAPI();
assertTrue(warnings.isEmpty());
}

@Test
void olderCoreProtectWarnsOnceAndNeverBreaksTheKnockout() {
when(coreProtect.getAPI()).thenReturn(new OldApi());
assertFalse(CoreProtectKnockouts.log(attacker, victim));
assertFalse(CoreProtectKnockouts.log(attacker, victim));
bukkit.when(Bukkit::getPluginManager).thenThrow(new IllegalStateException("server stopping"));
assertFalse(CoreProtectKnockouts.log(attacker, victim));
assertEquals(1, warnings.size());
assertTrue(warnings.getFirst().getMessage().contains("logPvpKnockout"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ void downedPlayerSurvivesDamageWithoutAttacker(String cause) throws Exception {
when(event.getFinalDamage()).thenReturn(4.0);
var manager = new PvpKnockoutManager();
PlayerData nonlethal = data(false);
try (var players = mockStatic(PlayerManager.class); var strikes = mockStatic(PvpStrikeService.class)) {
try (var players = mockStatic(PlayerManager.class); var strikes = mockStatic(PvpStrikeService.class);
var coreProtect = mockStatic(CoreProtectKnockouts.class)) {
players.when(() -> PlayerManager.get(attacker)).thenReturn(nonlethal);
// SimpleFactions only counts a battle life when the victim really dies.
PermadeathBattleExemption.set(p -> p == victim);
Expand All @@ -121,6 +122,8 @@ void downedPlayerSurvivesDamageWithoutAttacker(String cause) throws Exception {
manager.onLethalDamage(event);
verify(event).setCancelled(true);
strikes.verify(() -> PvpStrikeService.handleKnockout(victim, attacker));
// The cancelled blow never reaches CoreProtect, so the knockout is reported to it directly.
coreProtect.verify(() -> CoreProtectKnockouts.log(attacker, victim));
} finally {
PermadeathBattleExemption.set(null);
}
Expand Down
Loading