feat(events): record Guardian Bond damage outcomes
Release / release (push) Successful in 13m31s

This commit is contained in:
dmg committed 2026-09-25 02:18:00 -04:00
1 parent 08d13d2192
commit 3163a491a0
8 files changed
+236 -23

No files matched your search

@@ -713,6 +713,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
message -> getLogger().warning(message));
eventCapture = new games.dmg.fantasystick.events.EventCaptureListener(eventLog, sessions, Clock.systemUTC());
eventCapture.excludeOwnedDamage(fireball::ownsAcceptedDamage);
eventCapture.excludeOwnedDamage(guardianBond::ownsAcceptedRedirect);
downedEvents = new games.dmg.fantasystick.events.DownedEventBridge(eventLog::emit, Clock.systemUTC());
rareEvents = new games.dmg.fantasystick.events.RareEventBridge(eventLog::emit, Clock.systemUTC());rareShop.attachEvents(rareEvents);
bookEvents = new games.dmg.fantasystick.events.BookEventBridge(eventLog::emit, Clock.systemUTC());books.attachEvents(bookEvents);
@@ -722,6 +723,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
abilityEngine.enableCastEventsFor("fireball");
abilityEngine.enableCastEventsFor("mending_light");
abilityEngine.enableCastEventsFor("purify");
abilityEngine.enableCastEventsFor("guardian_bond");
admin.attachAbilityConfigEvents(new games.dmg.fantasystick.events.AbilityConfigEventBridge(eventLog::emit, Clock.systemUTC()));
passiveEvents = new games.dmg.fantasystick.events.PassiveEventBridge(eventLog::emit, Clock.systemUTC());books.attachPassiveEvents(passiveEvents);admin.attachPassiveEvents(passiveEvents);
recoveryEvents = new games.dmg.fantasystick.events.RecoveryEventBridge(eventLog::emit, Clock.systemUTC());outOfCombatRecovery.attachEvents(recoveryEvents,record->record.passives().contains(games.dmg.fantasystick.passive.QuickerRegenerationI.ID)?quickerRegeneration.bonus():java.math.BigInteger.ZERO);
@@ -432,6 +432,13 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
if (castEvents != null && audit != null)
castEvents.cleansed(audit, target, aided.character(), aided.session(), removed);
}
@Override public void acceptedPrevention(EquipmentSessions.Active aided, Player ally, Entity attacker,
double incoming, double actual, double remaining,
UUID bondApplication) {
if (castEvents != null && audit != null)
castEvents.prevented(audit, ally, aided.character(), aided.session(), attacker,
incoming, actual, remaining, bondApplication);
}
@Override public void effectEnded(UUID application, String reason) {
Objects.requireNonNull(application); Objects.requireNonNull(reason);
var effectId = observedEffects.remove(application);
@@ -36,6 +36,10 @@ public interface AbilityExecution extends AutoCloseable {
/** Record only harmful native effects actually removed from this target. */
void acceptedCleanse(games.dmg.fantasystick.session.EquipmentSessions.Active aided,
org.bukkit.entity.Player target, List<String> removedEffects);
/** Report only the final uncancelled native ally reduction, not a proposed redirect. */
void acceptedPrevention(games.dmg.fantasystick.session.EquipmentSessions.Active aided,
org.bukkit.entity.Player ally, Entity attacker, double incoming,
double actual, double remaining, UUID bondApplication);
/** End an owned observed effect once; ordinary close ends any remaining observed effects. */
void effectEnded(UUID applicationId, String reason);
/** An owner may specify a truthful terminal outcome before finishing. */
@@ -56,6 +56,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
final Player ally;
final EquipmentSessions.Active aided;
final Settings settings;
UUID application;
String endReason = "interrupted";
boolean closed;
State(AbilityExecution execution, Player guardian, Player ally,
EquipmentSessions.Active aided, Settings settings) {
@@ -67,14 +69,16 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
closed = true;
byGuardian.remove(guardian.getUniqueId(), this);
byAlly.remove(ally.getUniqueId(), this);
if (application != null) execution.effectEnded(application, endReason);
}
}
private static final class NativeTransfer {
final Player guardian; final Entity attacker; final BigDecimal requested; final BigDecimal available;
final State owner; final Player guardian; final Entity attacker; final BigDecimal requested; final BigDecimal available;
BigDecimal accepted = BigDecimal.ZERO;
NativeTransfer(Player guardian, Entity attacker, BigDecimal requested, BigDecimal available) {
this.guardian = guardian; this.attacker = attacker; this.requested = requested; this.available = available;
NativeTransfer(State owner, Player guardian, Entity attacker, BigDecimal requested, BigDecimal available) {
this.owner = owner; this.guardian = guardian; this.attacker = attacker;
this.requested = requested; this.available = available;
}
}
@@ -84,11 +88,14 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
private final EquipmentSessions sessions;
private final PartyRoster parties;
private final Damage damage;
private final boolean nativeDamageMode;
private final Consumer<Support> supports;
private final Map<UUID, State> byGuardian = new java.util.HashMap<>();
private final Map<UUID, State> byAlly = new java.util.HashMap<>();
private int transferring;
private NativeTransfer nativeTransfer;
private record Reduction(State state, Entity attacker, double incoming, double accepted) { }
private final Map<EntityDamageByEntityEvent, Reduction> pendingReductions = new java.util.IdentityHashMap<>();
private org.bukkit.scheduler.BukkitTask sweepTask;
private boolean closed;
@@ -102,7 +109,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
this.settings = Objects.requireNonNull(settings);
this.sessions = Objects.requireNonNull(sessions);
this.parties = Objects.requireNonNull(parties);
this.damage = damage == null ? this::nativeDamage : damage;
this.nativeDamageMode = damage == null;
this.damage = nativeDamageMode ? this::nativeDamage : damage;
this.supports = Objects.requireNonNull(supports);
}
@@ -153,7 +161,14 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
}
var state = new State(execution, guardian, ally, aided, captured);
byGuardian.put(guardian.getUniqueId(), state); byAlly.put(ally.getUniqueId(), state);
execution.own(state); execution.later(captured.duration(), execution::finish);
execution.own(state);
state.application = execution.effectApplied("guardian_bond", ally,
Map.of("redirect_percent", captured.redirectPercent().toPlainString(),
"range", Double.toString(captured.range()),
"duration_seconds", seconds(captured.duration())));
execution.later(captured.duration(), () -> {
state.endReason = "expired"; execution.resolve("expired"); execution.finish();
});
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@@ -184,8 +199,30 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
double remaining = BigDecimal.valueOf(incoming).subtract(accepted).max(BigDecimal.ZERO).doubleValue();
if (!Double.isFinite(remaining)) return;
event.setDamage(remaining);
state.execution.report(AbilityReport.Type.PREVENTION, ally, accepted);
supports.accept(new Support(attacker.getUniqueId(), state.execution.input().session().character(), state.aided, accepted));
pendingReductions.put(event, new Reduction(state, attacker, incoming, accepted.doubleValue()));
if (!nativeDamageMode) {
// Injected test adapters return their final accepted result directly.
state.execution.report(AbilityReport.Type.PREVENTION, ally, accepted);
supports.accept(new Support(attacker.getUniqueId(), state.execution.input().session().character(), state.aided, accepted));
}
}
/** Only an uncancelled original hit can claim actual prevention. */
@EventHandler(priority = EventPriority.MONITOR)
public void acceptedPrevention(EntityDamageByEntityEvent event) {
var reduction = pendingReductions.remove(event);
if (reduction == null || event.isCancelled() || !Double.isFinite(event.getFinalDamage())) return;
double actual = Math.min(reduction.accepted(), Math.max(0, reduction.incoming() - event.getFinalDamage()));
if (actual <= 0) return;
var state = reduction.state();
state.execution.acceptedPrevention(state.aided, state.ally, reduction.attacker(),
reduction.incoming(), actual, event.getFinalDamage(), state.application);
if (nativeDamageMode) {
var amount = BigDecimal.valueOf(actual);
state.execution.report(AbilityReport.Type.PREVENTION, state.ally, amount);
supports.accept(new Support(reduction.attacker().getUniqueId(),
state.execution.input().session().character(), state.aided, amount));
}
}
/** Capture accepted marked damage before synchronous fantasy-defeat restoration can replace native health. */
@@ -193,7 +230,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
Objects.requireNonNull(guardian); Objects.requireNonNull(attacker); Objects.requireNonNull(requested);
double amount = requested.doubleValue(); double health = effectiveHealth(guardian);
if (!Double.isFinite(amount) || amount <= 0 || !Double.isFinite(health) || health <= 0) return BigDecimal.ZERO;
var transfer = new NativeTransfer(guardian, attacker, requested, BigDecimal.valueOf(health));
var transfer = new NativeTransfer(byGuardian.get(guardian.getUniqueId()), guardian, attacker,
requested, BigDecimal.valueOf(health));
nativeTransfer = transfer;
try { guardian.damage(amount, attacker); }
finally { nativeTransfer = null; }
@@ -206,6 +244,17 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
if (transfer == null || event.getEntity() != transfer.guardian || attacker(event) != transfer.attacker
|| event.isCancelled() || !Double.isFinite(event.getFinalDamage()) || event.getFinalDamage() <= 0) return;
transfer.accepted = BigDecimal.valueOf(event.getFinalDamage()).min(transfer.requested).min(transfer.available);
if (transfer.owner != null && transfer.accepted.signum() > 0)
transfer.owner.execution.acceptedDamage(transfer.guardian, transfer.requested.doubleValue(),
transfer.accepted.doubleValue(), transfer.attacker,
Map.of("damage_kind", "guardian_bond_redirection"));
}
/** The guardian's accepted native hit has one richer bond-owned damage record. */
public boolean ownsAcceptedRedirect(org.bukkit.event.entity.EntityDamageEvent event) {
var transfer = nativeTransfer;
return transfer != null && event instanceof EntityDamageByEntityEvent hit
&& hit.getEntity() == transfer.guardian && attacker(hit) == transfer.attacker;
}
/** True only during synchronous dispatch of Guardian Bond's marked redirected damage. */
@@ -230,7 +279,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
}
private void breakBond(State state) {
state.close(); state.execution.finish();
state.endReason = "eligibility_changed";
state.close(); state.execution.resolve("interrupted"); state.execution.finish();
}
private static boolean eligible(Player guardian, Player ally, double range) {
@@ -268,7 +318,9 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
@Override public void close() {
if (closed) return; closed = true;
if (sweepTask != null) sweepTask.cancel();
for (var state : List.copyOf(byGuardian.values())) state.execution.finish();
byGuardian.clear(); byAlly.clear();
for (var state : List.copyOf(byGuardian.values())) {
state.endReason = "shutdown"; state.execution.resolve("interrupted"); state.execution.finish();
}
byGuardian.clear(); byAlly.clear(); pendingReductions.clear();
}
}
@@ -87,7 +87,7 @@ public final class AbilityCastEventBridge {
data.put("application_id", application.toString());
if (target != null) {
data.put("target_entity_id", target.getUniqueId().toString());
data.put("target_type", target.getType().name());
data.put("target_type", entityType(target));
var place = target.getLocation();
if (place != null) data.put("target_location", locationData(place));
}
@@ -108,12 +108,12 @@ public final class AbilityCastEventBridge {
data.put("requested_amount", requested);
data.put("actual_amount", actual);
data.put("target_entity_id", target.getUniqueId().toString());
data.put("target_type", target.getType().name());
data.put("target_type", entityType(target));
var targetAtApplication = target.getLocation();
if (targetAtApplication != null) data.put("target_location", locationData(targetAtApplication));
if (target instanceof org.bukkit.entity.Player player) data.put("target_player_id", player.getUniqueId().toString());
data.put("direct_entity_id", direct.getUniqueId().toString());
data.put("direct_entity_type", direct.getType().name());
data.put("direct_entity_type", entityType(direct));
emit(cast, "combat.damage", "accepted", data, application);
} catch (RuntimeException ignored) { /* Native damage remains authoritative even when auditing fails. */ }
}
@@ -165,6 +165,35 @@ public final class AbilityCastEventBridge {
} catch (RuntimeException ignored) { /* Native cleanse remains authoritative. */ }
}
public void prevented(Cast cast, org.bukkit.entity.Player ally, UUID aidedCharacter,
UUID aidedSession, Entity attacker, double incoming, double actual,
double remaining, UUID bondApplication) {
if (cast == null || !Double.isFinite(actual) || actual <= 0) return;
try {
var data = new LinkedHashMap<String, Object>();
var application = UUID.randomUUID();
data.put("application_id", application.toString());
data.put("causation_id", cast.actionId().toString());
if (bondApplication != null) data.put("bond_application_id", bondApplication.toString());
data.put("target_player_id", ally.getUniqueId().toString());
data.put("target_character_id", aidedCharacter.toString());
data.put("target_session_id", aidedSession.toString());
data.put("target_location", locationData(ally.getLocation()));
data.put("attacker_entity_id", attacker.getUniqueId().toString());
data.put("attacker_entity_type", entityType(attacker));
data.put("incoming_amount", decimal(incoming));
data.put("actual_amount", decimal(actual));
data.put("remaining_amount", decimal(remaining));
emit(cast, "combat.prevented", "accepted", data, application);
} catch (RuntimeException ignored) { /* Prevention authority belongs to the native damage event. */ }
}
private static String entityType(Entity entity) {
if (entity instanceof org.bukkit.entity.Player) return "PLAYER";
var type = entity.getType();
return type == null ? "unavailable" : type.name();
}
public void effectEnded(Cast cast, UUID application, String effectId, String reason) {
if (cast == null) return;
emit(cast, "effect.ended", "ended", Map.of("application_id", application.toString(),
@@ -70,10 +70,11 @@ public final class GuardianBondSettings implements AutoCloseable {
public String value(String key) { return effectiveValues.get(key); }
public GuardianBond.Settings settings() { return parse(effectiveValues); }
public synchronized void publish(Update update) {
public synchronized boolean publish(Update update) {
Objects.requireNonNull(update);
if (update.sequence() <= publishedSequence) return;
if (update.sequence() <= publishedSequence) return false;
parse(update.values()); effectiveValues = update.values(); publishedSequence = update.sequence();
return true;
}
public CompletionStage<Update> set(String key, String value) {
@@ -1327,8 +1327,11 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
private void guardianBondConfiguration(CommandSender sender, String[] args) {
String key = args[1];
var actor = abilityConfigEvents == null ? null : abilityConfigEvents.capture(sender);
String before = guardianBondSettings.value(key);
if (args.length == 2) {
sender.sendMessage(key + "=" + guardianBondSettings.value(key) + " (applies to future Guardian Bond casts only)"); return;
if (abilityConfigEvents != null) abilityConfigEvents.read(actor, "guardian_bond", key, before);
sender.sendMessage(key + "=" + before + " (applies to future Guardian Bond casts only)"); return;
}
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.GuardianBondSettings.Update> operation;
try { operation = guardianBondSettings.set(key, args[2]); }
@@ -1340,8 +1343,9 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
if (closed) return;
try { nextTick.accept(() -> {
if (closed) return;
if (failure == null) {
guardianBond.configure(saved.settings()); abilities.replace(guardianBond.definition()); guardianBondSettings.publish(saved);
if (failure == null && guardianBondSettings.publish(saved)) {
guardianBond.configure(saved.settings()); abilities.replace(guardianBond.definition());
if (abilityConfigEvents != null) abilityConfigEvents.changed(actor, "guardian_bond", key, before, saved.value());
}
if (!sender.hasPermission(PERMISSION)
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
@@ -68,6 +68,65 @@ class GuardianBondNativeTest {
var expired = hit(enemy, ally.player, 10); fixture.dispatcher.accept(expired);
assertEquals(10, expired.getDamage(), 1.0e-9);
}
var records = events(data);
for (var type : List.of("ability.learned", "ability.assigned", "ability.cast", "ability.resolved",
"effect.applied", "effect.ended", "combat.prevented", "combat.damage"))
assertEquals(1, records.stream().filter(event -> event.get("type").getAsString().endsWith(type)
&& event.getAsJsonObject("data").has("ability_id")
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).count(), type);
var cast = records.stream().filter(event -> event.get("type").getAsString().endsWith("ability.cast")
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).findFirst().orElseThrow();
var prevented = records.stream().filter(event -> event.get("type").getAsString().endsWith("combat.prevented")
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).findFirst().orElseThrow();
assertEquals("3", prevented.getAsJsonObject("data").get("actual_amount").getAsString());
assertEquals(cast.getAsJsonObject("data").get("action_id").getAsString(),
prevented.getAsJsonObject("data").get("action_id").getAsString());
}
private static List<com.google.gson.JsonObject> events(Path root) throws Exception {
var values = new ArrayList<com.google.gson.JsonObject>();
try (var files = java.nio.file.Files.walk(root.resolve("events"))) {
for (var file : files.filter(path -> path.toString().endsWith(".jsonl")).toList())
for (var line : java.nio.file.Files.readAllLines(file))
if (!line.isBlank()) values.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
}
return values;
}
@Test void laterCancelledOriginalHitCannotClaimAcceptedPrevention() throws Exception {
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
fixture.plugin.setEnabled(true);
var guardian = new FantasyStickLifecycleNativeTest.Actor(data);
var ally = new FantasyStickLifecycleNativeTest.Actor(data);
prepareGuardian(data, guardian, true);
fixture.players.put(guardian.core.id, guardian.player);
fixture.players.put(ally.core.id, ally.player);
fixture.activate(guardian); fixture.activate(ally);
party(fixture, guardian, ally); aim(guardian, ally, 8, true);
nativeDamage(fixture, guardian);
class Veto implements org.bukkit.event.Listener {
@org.bukkit.event.EventHandler(priority = org.bukkit.event.EventPriority.HIGHEST)
public void cancel(EntityDamageByEntityEvent event) {
if (event.getEntity() == ally.player) event.setCancelled(true);
}
}
fixture.dispatcher.register(new Veto(), fixture.plugin);
use(fixture, guardian); drain(fixture, 2);
var enemy = mock(org.bukkit.entity.Zombie.class);
when(enemy.getUniqueId()).thenReturn(UUID.randomUUID());
when(enemy.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);
var original = hit(enemy, ally.player, 10); fixture.dispatcher.accept(original);
assertTrue(original.isCancelled());
assertEquals(7, guardian.player.getHealth(), 1.0e-9);
}
var observed = events(data);
assertEquals(0, observed.stream().filter(event -> event.get("type").getAsString().endsWith("combat.prevented")
&& event.getAsJsonObject("data").has("ability_id")
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).count());
assertEquals(1, observed.stream().filter(event -> event.get("type").getAsString().endsWith("combat.damage")
&& event.getAsJsonObject("data").has("ability_id")
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).count(),
"The guardian's separately accepted native damage remains truthful");
}
@Test void lethalRedirectUsesNormalDefeatAndOnlyRemovesGuardianAvailableHealth() throws Exception {
@@ -92,7 +151,10 @@ class GuardianBondNativeTest {
}
};
use(fixture, guardian); drain(fixture, 2); guardian.core.health = 1; ally.core.health = 9;
var hit = hit(mock(org.bukkit.entity.Zombie.class), ally.player, 10); fixture.dispatcher.accept(hit);
var attacker = mock(org.bukkit.entity.Zombie.class);
when(attacker.getUniqueId()).thenReturn(UUID.randomUUID());
when(attacker.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);
var hit = hit(attacker, ally.player, 10); fixture.dispatcher.accept(hit);
assertEquals(List.of("guardian"), defeatOrder,
"Redirected damage reaches normal guardian defeat before original damage resumes");
assertEquals(9, hit.getDamage(), 1.0e-9, "Overkill cannot delete damage the guardian could not accept");
@@ -107,6 +169,11 @@ class GuardianBondNativeTest {
}
assertEquals(List.of("guardian", "ally"), defeatOrder, "Both characters may be defeated in the approved order");
}
var ownerDamage = events(data).stream().filter(event -> event.get("type").getAsString().endsWith("combat.damage")
&& event.getAsJsonObject("data").has("ability_id")
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).toList();
assertEquals(1, ownerDamage.size(), "A lethal redirect still has one accepted guardian damage record");
assertEquals(1.0, ownerDamage.getFirst().getAsJsonObject("data").get("actual_amount").getAsDouble());
}
@Test void actualProtectionCreatesOnePersistedSupportShareForTheAidedDirectContributor() throws Exception {
@@ -132,6 +199,25 @@ class GuardianBondNativeTest {
}
}
@Test void cancelledOriginalNativeHitCannotCreateEffectiveSupport() {
var context = new Context(true);
when(context.guardian.getHealth()).thenReturn(10.0);
doAnswer(call -> {
var redirected = hit(call.getArgument(1), context.guardian, (Double) call.getArgument(0));
context.bond.acceptedRedirect(redirected);
return null;
}).when(context.guardian).damage(anyDouble(), any(Entity.class));
context.activate();
var cancelled = hit(context.enemy, context.ally, 10);
context.bond.redirect(cancelled); cancelled.setCancelled(true);
context.bond.acceptedPrevention(cancelled);
assertTrue(context.supports.isEmpty(), "A later-vetoed original hit cannot create support credit");
var accepted = hit(context.enemy, context.ally, 10);
context.bond.redirect(accepted); context.bond.acceptedPrevention(accepted);
assertEquals(1, context.supports.size());
assertEquals(0, new BigDecimal("3").compareTo(context.supports.getFirst().amount()));
}
@Test void conservedRoutingUsesPostRecipientAndPostGuardianMitigationCancellationAndLethalCaps() {
var context = new Context(); var requested = new ArrayList<BigDecimal>();
var mitigation = new games.dmg.fantasystick.ability.CombatMitigation();
@@ -268,6 +354,19 @@ class GuardianBondNativeTest {
nativeDamage(fixture, guardian); use(fixture, guardian); drain(fixture, 2);
var future = hit(mock(org.bukkit.entity.Zombie.class), ally.player, 10); fixture.dispatcher.accept(future);
assertEquals(5, future.getDamage(), 1.0e-9); assertEquals(180L, fixture.delayedTicks.getLast());
try (var channel = java.nio.channels.FileChannel.open(data.resolve(".config.lock"),
java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.WRITE);
var lock = channel.lock()) {
assertTrue(lock.isValid());
fixture.command.execute(sender.port, "fstickadmin",
new String[]{"config", "guardian-bond-range-blocks", "17"});
var failed = fixture.tasks.poll(5, TimeUnit.SECONDS);
assertNotNull(failed); failed.run();
}
assertEquals("16", ((games.dmg.fantasystick.persistence.GuardianBondSettings)
FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "guardianBondSettings"))
.value("guardian-bond-range-blocks"));
assertTrue(sender.messages.getLast().contains("save failed"));
fixture.command.execute(sender.port, "fantasystick", new String[]{"config", "guardian-bond-duration-seconds", "0"});
assertTrue(sender.messages.getLast().contains("invalid"));
fixture.command.execute(sender.port, "fantasystick", new String[]{"config", "guardian-bond-range-blocks", "16", "extra"});
@@ -281,6 +380,18 @@ class GuardianBondNativeTest {
assertEquals(0, new BigDecimal("9").compareTo(afterRestart.parameters().get("duration-seconds")));
assertEquals(new BigDecimal("50"), afterRestart.parameters().get("redirect-percent"));
}
var config = events(data).stream().filter(event -> event.get("type").getAsString().endsWith("config.read")
|| event.get("type").getAsString().endsWith("config.changed"))
.filter(event -> event.getAsJsonObject("data").has("key")
&& event.getAsJsonObject("data").get("key").getAsString().startsWith("guardian-bond-")).toList();
assertEquals(5, config.size());
assertEquals("14", config.getFirst().getAsJsonObject("data").get("after").getAsString());
assertEquals("console", config.getFirst().getAsJsonObject("data").get("actor_kind").getAsString());
var range = config.stream().filter(event -> event.get("type").getAsString().endsWith("config.changed")
&& event.getAsJsonObject("data").get("key").getAsString().equals("guardian-bond-range-blocks"))
.findFirst().orElseThrow();
assertEquals("14", range.getAsJsonObject("data").get("before").getAsString());
assertEquals("16", range.getAsJsonObject("data").get("after").getAsString());
for (String invalid : List.of("guardian-bond-cooldown-seconds: -1\n", "guardian-bond-range-blocks: 0\n",
"guardian-bond-duration-seconds: 0\n", "guardian-bond-redirect-percent: 101\n")) {
var directory = data.resolve(Integer.toHexString(invalid.hashCode())); java.nio.file.Files.createDirectories(directory);
@@ -299,7 +410,8 @@ class GuardianBondNativeTest {
GuardianBond.Damage damage = (target, attacker, amount) -> amount; final GuardianBond bond;
final EquipmentSessions.Active guardianActive; final EquipmentSessions.Active allyActive;
final PartyRoster.Party party;
Context() {
Context() { this(false); }
Context(boolean nativeAdapter) {
var guardianId = UUID.randomUUID(); var allyId = UUID.randomUUID();
when(guardian.getUniqueId()).thenReturn(guardianId); when(ally.getUniqueId()).thenReturn(allyId);
when(enemy.getUniqueId()).thenReturn(UUID.randomUUID());
@@ -314,8 +426,10 @@ class GuardianBondNativeTest {
when(parties.partyOf(guardianId)).thenReturn(java.util.Optional.of(party)); when(parties.partyOf(allyId)).thenReturn(java.util.Optional.of(party));
when(input.player()).thenReturn(guardian); when(input.target()).thenReturn(ally); when(input.session()).thenReturn(guardianActive);
when(execution.input()).thenReturn(input); when(execution.stillCurrent()).thenReturn(true);
bond = new GuardianBond(GuardianBond.Settings.defaults(), sessions, parties,
(target, attacker, amount) -> damage.apply(target, attacker, amount), supports::add);
bond = nativeAdapter
? new GuardianBond(GuardianBond.Settings.defaults(), sessions, parties, supports::add)
: new GuardianBond(GuardianBond.Settings.defaults(), sessions, parties,
(target, attacker, amount) -> damage.apply(target, attacker, amount), supports::add);
when(execution.definition()).thenReturn(bond.definition());
}
void activate() { bond.activate(execution); }