feat(events): record Sanctuary area and healing outcomes
Release / release (push) Successful in 7m19s
Release / release (push) Successful in 7m19s
This commit is contained in:
1 parent
21877f6820
commit
ff28f2e98d
7 files changed
+181
-21
No files matched your search
@@ -729,6 +729,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
abilityEngine.enableCastEventsFor("rallying_cry");
|
||||
abilityEngine.enableCastEventsFor("frost_nova");
|
||||
abilityEngine.enableCastEventsFor("chain_lightning");
|
||||
abilityEngine.enableCastEventsFor("sanctuary");
|
||||
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);
|
||||
|
||||
@@ -18,9 +18,12 @@ import org.bukkit.event.entity.EntityDamageEvent;
|
||||
/** Internal strongest-reduction stage shared by Fantasy Stick protection effects. */
|
||||
public final class CombatMitigation implements Listener, AutoCloseable {
|
||||
private final class Grant implements AutoCloseable {
|
||||
final AbilityExecution execution; final Player player; final BigDecimal percent; final BooleanSupplier current; boolean closed;
|
||||
Grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current) {
|
||||
final AbilityExecution execution; final Player player; final BigDecimal percent; final BooleanSupplier current;
|
||||
final java.util.function.BiConsumer<EntityDamageEvent, Double> acceptedObservation; boolean closed;
|
||||
Grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current,
|
||||
java.util.function.BiConsumer<EntityDamageEvent, Double> acceptedObservation) {
|
||||
this.execution = execution; this.player = player; this.percent = percent; this.current = current;
|
||||
this.acceptedObservation = acceptedObservation;
|
||||
}
|
||||
boolean valid() {
|
||||
try { return !closed && execution.stillCurrent() && current.getAsBoolean(); }
|
||||
@@ -42,10 +45,16 @@ public final class CombatMitigation implements Listener, AutoCloseable {
|
||||
}
|
||||
|
||||
public AutoCloseable grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current) {
|
||||
return grant(execution, player, percent, current, null);
|
||||
}
|
||||
|
||||
/** An owner may observe only the selected, positive native prevention after final cancellation. */
|
||||
public AutoCloseable grant(AbilityExecution execution, Player player, BigDecimal percent, BooleanSupplier current,
|
||||
java.util.function.BiConsumer<EntityDamageEvent, Double> acceptedObservation) {
|
||||
Objects.requireNonNull(execution); Objects.requireNonNull(player); Objects.requireNonNull(percent); Objects.requireNonNull(current);
|
||||
if (closed || percent.signum() < 0 || percent.compareTo(BigDecimal.valueOf(100)) > 0)
|
||||
throw new IllegalArgumentException("Invalid combat mitigation grant");
|
||||
var grant = new Grant(execution, player, percent, current);
|
||||
var grant = new Grant(execution, player, percent, current, acceptedObservation);
|
||||
grants.computeIfAbsent(player.getUniqueId(), ignored -> new ArrayList<>()).add(grant);
|
||||
return grant;
|
||||
}
|
||||
@@ -76,6 +85,9 @@ public final class CombatMitigation implements Listener, AutoCloseable {
|
||||
if (prevention == null || event.isCancelled() || event.getFinalDamage() <= 0
|
||||
|| !prevention.grant.valid()) return;
|
||||
prevention.grant.execution.report(AbilityReport.Type.PREVENTION, event.getEntity(), prevention.amount);
|
||||
if (prevention.grant.acceptedObservation != null) try {
|
||||
prevention.grant.acceptedObservation.accept(event, prevention.amount.doubleValue());
|
||||
} catch (RuntimeException ignored) { /* Audit cannot alter the accepted native hit. */ }
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
|
||||
@@ -53,23 +53,30 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
|
||||
|
||||
private final class State implements AutoCloseable {
|
||||
final AbilityExecution execution; final Player source;
|
||||
final Location origin; final Settings captured; final Map<UUID, AutoCloseable> mitigation = new LinkedHashMap<>();
|
||||
final Location origin; final Settings captured; final Map<UUID, Benefit> mitigation = new LinkedHashMap<>();
|
||||
Duration elapsed = Duration.ZERO; Duration nextPulse; final Duration scanPeriod;
|
||||
int pending; boolean expired; boolean closed;
|
||||
UUID areaApplication; int pulseOrdinal;
|
||||
String endReason = "interrupted"; int pending; boolean expired; boolean closed;
|
||||
State(AbilityExecution execution, Settings captured) {
|
||||
this.execution = execution; this.source = execution.input().player(); this.origin = source.getLocation().clone();
|
||||
this.captured = captured; this.nextPulse = captured.pulseInterval();
|
||||
long period = Math.max(50_000_000L, (captured.duration().toNanos() + 59) / 60);
|
||||
this.scanPeriod = Duration.ofNanos(period);
|
||||
}
|
||||
void start() { states.add(this); execution.own(this); reconcile(); visual(); schedule(); }
|
||||
void start() {
|
||||
states.add(this); execution.own(this);
|
||||
areaApplication = execution.effectApplied("sanctuary_area", null,
|
||||
Map.of("radius", captured.radius(), "vertical_range", captured.verticalRange(),
|
||||
"duration_seconds", seconds(captured.duration()), "area_location", locationData(origin)));
|
||||
reconcile(); visual(); schedule();
|
||||
}
|
||||
void schedule() {
|
||||
if (closed || expired) return;
|
||||
Duration remaining = captured.duration().minus(elapsed);
|
||||
execution.later(remaining.compareTo(scanPeriod) < 0 ? remaining : scanPeriod, this::step);
|
||||
}
|
||||
void step() {
|
||||
if (closed || !execution.stillCurrent()) { execution.finish(); return; }
|
||||
if (closed || !execution.stillCurrent()) { execution.resolve("interrupted"); execution.finish(); return; }
|
||||
Duration remaining = captured.duration().minus(elapsed);
|
||||
elapsed = elapsed.plus(remaining.compareTo(scanPeriod) < 0 ? remaining : scanPeriod);
|
||||
reconcile(); visual();
|
||||
@@ -77,20 +84,22 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
|
||||
pulse(); nextPulse = nextPulse.plus(captured.pulseInterval());
|
||||
}
|
||||
if (elapsed.compareTo(captured.duration()) >= 0) {
|
||||
expired = true; closeMitigation(); if (pending == 0) execution.finish();
|
||||
expired = true; endReason = "expired"; closeMitigation();
|
||||
execution.resolve("expired", Map.of("pulse_count", pulseOrdinal));
|
||||
if (pending == 0) execution.finish();
|
||||
} else schedule();
|
||||
}
|
||||
void reconcile() {
|
||||
var eligible = recipients();
|
||||
for (var entry : List.copyOf(mitigation.entrySet())) {
|
||||
if (eligible.stream().noneMatch(player -> player.getUniqueId().equals(entry.getKey()))) {
|
||||
try { entry.getValue().close(); } catch (Exception ignored) { } mitigation.remove(entry.getKey());
|
||||
entry.getValue().close(); mitigation.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
for (var player : eligible) mitigation.computeIfAbsent(player.getUniqueId(), ignored ->
|
||||
combatMitigation.grant(execution, player, captured.damageReductionPercent(), () -> recipientCurrent(player)));
|
||||
for (var player : eligible) mitigation.computeIfAbsent(player.getUniqueId(), ignored -> new Benefit(this, player));
|
||||
}
|
||||
void pulse() {
|
||||
pulseOrdinal++;
|
||||
for (var player : recipients()) if (owner(player) == this) heal(player);
|
||||
}
|
||||
void heal(Player player) {
|
||||
@@ -141,11 +150,46 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
|
||||
captured.radius() / 2, 0.1, captured.radius() / 2, 0); } catch (RuntimeException ignored) { }
|
||||
}
|
||||
void closeMitigation() {
|
||||
for (var grant : List.copyOf(mitigation.values())) try { grant.close(); } catch (Exception ignored) { }
|
||||
for (var grant : List.copyOf(mitigation.values())) grant.close();
|
||||
mitigation.clear();
|
||||
}
|
||||
@Override public void close() {
|
||||
if (closed) return; closed = true; expired = true; states.remove(this); closeMitigation();
|
||||
if (areaApplication != null) execution.effectEnded(areaApplication, endReason);
|
||||
}
|
||||
}
|
||||
|
||||
private final class Benefit implements AutoCloseable {
|
||||
final State state; final Player player; final AutoCloseable grant;
|
||||
UUID application; boolean closed;
|
||||
Benefit(State state, Player player) {
|
||||
this.state = state; this.player = player;
|
||||
grant = combatMitigation.grant(state.execution, player, state.captured.damageReductionPercent(),
|
||||
() -> !closed && state.recipientCurrent(player), (event, amount) -> {
|
||||
var active = sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);
|
||||
var attacker = event.getDamageSource().getCausingEntity();
|
||||
if (attacker == null && event instanceof org.bukkit.event.entity.EntityDamageByEntityEvent byEntity)
|
||||
attacker = byEntity.getDamager();
|
||||
if (active != null && attacker != null && amount > 0)
|
||||
state.execution.acceptedPrevention(active, player, attacker,
|
||||
event.getDamage() + amount, amount, event.getFinalDamage(), application);
|
||||
});
|
||||
try {
|
||||
var active = sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);
|
||||
var evidence = new LinkedHashMap<String, Object>();
|
||||
evidence.put("damage_reduction_percent", state.captured.damageReductionPercent().toPlainString());
|
||||
if (active != null) {
|
||||
evidence.put("recipient_character_id", active.character().toString());
|
||||
evidence.put("recipient_session_id", active.session().toString());
|
||||
}
|
||||
application = state.execution.effectApplied("sanctuary_benefit", player, evidence);
|
||||
} catch (RuntimeException ignored) { /* Auditing cannot refuse a native mitigation grant. */ }
|
||||
}
|
||||
@Override public void close() {
|
||||
if (closed) return; closed = true;
|
||||
try { grant.close(); } catch (Exception ignored) { }
|
||||
if (application != null) state.execution.effectEnded(application,
|
||||
state.closed || state.expired ? state.endReason : "recipient_eligibility_changed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,10 +253,15 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
|
||||
double desired = Math.min(maximum, persisted.health().doubleValue()); double restored = Math.min(accepted, desired - current);
|
||||
if (!Double.isFinite(current) || !Double.isFinite(maximum) || !Double.isFinite(restored) || restored <= 0) return;
|
||||
target.player().setHealth(Math.min(maximum, current + restored));
|
||||
double after = target.player().getHealth();
|
||||
double actual = after - current;
|
||||
if (!Double.isFinite(actual) || actual <= 0) return;
|
||||
state.execution.acceptedHeal(target.active(), target.player(), persisted,
|
||||
accepted, actual, current, after, maximum);
|
||||
state.execution.report(AbilityReport.Type.HEALING, target.player(),
|
||||
BigDecimal.valueOf(restored).stripTrailingZeros());
|
||||
BigDecimal.valueOf(actual).stripTrailingZeros());
|
||||
if (target.player() != state.source) supports.accept(new MendingLight.Support(
|
||||
state.execution.input().session().character(), target.active(), BigDecimal.valueOf(restored).stripTrailingZeros()));
|
||||
state.execution.input().session().character(), target.active(), BigDecimal.valueOf(actual).stripTrailingZeros()));
|
||||
} finally { completePending(state); }
|
||||
}
|
||||
private static void completePending(State state) {
|
||||
@@ -240,6 +289,13 @@ public final class Sanctuary implements AbilityEffect, AutoCloseable {
|
||||
return world.rayTraceBlocks(origin, offset.multiply(1 / distance), distance, org.bukkit.FluidCollisionMode.NEVER, true) == null;
|
||||
} catch (RuntimeException invalid) { return false; }
|
||||
}
|
||||
private static Map<String, Object> locationData(Location location) {
|
||||
var world = location == null ? null : location.getWorld();
|
||||
if (world == null || world.getUID() == null || world.getName() == null)
|
||||
return Map.of("location_status", "unavailable");
|
||||
return Map.of("world_id", world.getUID().toString(), "world_name", world.getName(),
|
||||
"x", location.getX(), "y", location.getY(), "z", location.getZ());
|
||||
}
|
||||
private static double distance(Location origin, Player player) {
|
||||
try { var at = player.getLocation(); if (origin.getWorld() != at.getWorld()) return Double.POSITIVE_INFINITY;
|
||||
double dx = at.getX() - origin.getX(), dz = at.getZ() - origin.getZ(); return dx * dx + dz * dz;
|
||||
|
||||
@@ -73,10 +73,11 @@ public final class SanctuarySettings implements AutoCloseable {
|
||||
public String value(String key) { return effectiveValues.get(key); }
|
||||
public Sanctuary.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) {
|
||||
|
||||
@@ -1453,8 +1453,11 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
|
||||
private void sanctuaryConfiguration(CommandSender sender, String[] args) {
|
||||
String key = args[1];
|
||||
var actor = abilityConfigEvents == null ? null : abilityConfigEvents.capture(sender);
|
||||
String before = sanctuarySettings.value(key);
|
||||
if (args.length == 2) {
|
||||
sender.sendMessage(key + "=" + sanctuarySettings.value(key) + " (applies to future Sanctuary casts only)"); return;
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.read(actor, "sanctuary", key, before);
|
||||
sender.sendMessage(key + "=" + before + " (applies to future Sanctuary casts only)"); return;
|
||||
}
|
||||
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.SanctuarySettings.Update> operation;
|
||||
try { operation = sanctuarySettings.set(key, args[2]); }
|
||||
@@ -1466,12 +1469,14 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
if (closed) return;
|
||||
try { nextTick.accept(() -> {
|
||||
if (closed) return;
|
||||
if (failure == null) {
|
||||
sanctuary.configure(saved.settings()); abilities.replace(sanctuary.definition()); sanctuarySettings.publish(saved);
|
||||
boolean published = failure == null && sanctuarySettings.publish(saved);
|
||||
if (published) {
|
||||
sanctuary.configure(saved.settings()); abilities.replace(sanctuary.definition());
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.changed(actor, "sanctuary", key, before, saved.value());
|
||||
}
|
||||
if (!sender.hasPermission(PERMISSION)
|
||||
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
|
||||
sender.sendMessage(failure == null
|
||||
sender.sendMessage(published
|
||||
? "Saved " + key + "=" + saved.value() + " for future Sanctuary casts; active areas and deadlines are unchanged."
|
||||
: "Sanctuary configuration save failed; the effective setting is unchanged.");
|
||||
}); } catch (RuntimeException rejected) { report(null, "config-response", rejected); }
|
||||
|
||||
@@ -43,6 +43,19 @@ class SanctuaryNativeTest {
|
||||
var viewer=new FantasyStickLifecycleNativeTest.Actor(data);viewer.core.inventory.setItemInOffHand(null);viewer.core.location.setWorld(caster.world);fixture.players.put(viewer.core.id,viewer.player);
|
||||
PartyBookNativeTest.disableAllParticles(fixture,caster);caster.particleCalls.clear();viewer.particleCalls.clear();
|
||||
use(fixture,caster);drain(fixture,2);assertEquals(2L,fixture.delayedTicks.getLast());
|
||||
var incomingAttacker=mock(org.bukkit.entity.LivingEntity.class);when(incomingAttacker.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
var incomingSource=DamageSource.builder(org.bukkit.damage.DamageType.MOB_ATTACK)
|
||||
.withCausingEntity(incomingAttacker).withDirectEntity(incomingAttacker).build();
|
||||
var incoming=new EntityDamageByEntityEvent(incomingAttacker,ally.player,EntityDamageEvent.DamageCause.ENTITY_ATTACK,incomingSource,10);
|
||||
fixture.dispatcher.accept(incoming);assertEquals(8,incoming.getFinalDamage(),1.0e-9);
|
||||
class LateVeto implements org.bukkit.event.Listener {
|
||||
@org.bukkit.event.EventHandler(priority=org.bukkit.event.EventPriority.HIGHEST)
|
||||
public void cancel(EntityDamageByEntityEvent event){if(event.getDamager()==incomingAttacker)event.setCancelled(true);}
|
||||
}
|
||||
fixture.dispatcher.register(new LateVeto(),fixture.plugin);
|
||||
var vetoed=new EntityDamageByEntityEvent(incomingAttacker,ally.player,
|
||||
EntityDamageEvent.DamageCause.ENTITY_ATTACK,incomingSource,10);
|
||||
fixture.dispatcher.accept(vetoed);assertTrue(vetoed.isCancelled());
|
||||
for(int i=0;i<10;i++)fixture.delayed.removeFirst().run();drain(fixture,4);assertEquals(6,ally.core.health,0);
|
||||
assertTrue(caster.particleCalls.isEmpty(),"The opted-out caster receives no Sanctuary particles");assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==org.bukkit.Particle.END_ROD&&call[2].equals(12)),"An enabled nearby viewer retains Sanctuary visuals");
|
||||
var enemy=mock(org.bukkit.entity.LivingEntity.class);when(enemy.getUniqueId()).thenReturn(UUID.randomUUID());when(enemy.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);
|
||||
@@ -54,6 +67,61 @@ class SanctuaryNativeTest {
|
||||
assertEquals(BigInteger.valueOf(5),storage.find(caster.core.record.id()).get().orElseThrow().experience());
|
||||
assertEquals(BigInteger.valueOf(5),storage.find(ally.core.record.id()).get().orElseThrow().experience());
|
||||
}
|
||||
var records=events(data);
|
||||
for(var type:List.of("ability.learned","ability.assigned","ability.cast","ability.resolved","combat.prevented"))
|
||||
assertEquals(1,abilityEvents(records,type).size(),type);
|
||||
var heals=abilityEvents(records,"combat.healed");
|
||||
assertTrue(heals.size()>=1,"Actual acknowledged ally-health pulses are individually recorded");
|
||||
assertEquals(heals.size(),heals.stream().map(event->event.getAsJsonObject("data").get("application_id").getAsString()).distinct().count());
|
||||
assertEquals(3,abilityEvents(records,"effect.applied").size(),"One fixed area plus caster/ally benefits");
|
||||
assertEquals(3,abilityEvents(records,"effect.ended").size(),"No per-tick duplicate grants");
|
||||
var heal=heals.stream().map(event->event.getAsJsonObject("data"))
|
||||
.filter(value->value.get("health_before").getAsString().equals("4")).findFirst().orElseThrow();
|
||||
assertEquals("2",heal.get("actual_amount").getAsString());
|
||||
assertEquals("6",heal.get("health_after").getAsString());
|
||||
assertTrue(heal.has("target_location"));
|
||||
assertEquals("2",abilityEvents(records,"combat.prevented").getFirst().getAsJsonObject("data")
|
||||
.get("actual_amount").getAsString());
|
||||
}
|
||||
|
||||
@Test void cancelledNativeAllyHealCannotClaimSavedOrEffectiveHealth()throws Exception{
|
||||
UUID allyId;
|
||||
try(var fixture=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
fixture.plugin.setEnabled(true);
|
||||
var caster=new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var ally=new FantasyStickLifecycleNativeTest.Actor(data);allyId=ally.core.id;
|
||||
prepareCaster(data,caster,true);setHealth(data,ally,BigDecimal.valueOf(4));
|
||||
fixture.players.put(caster.core.id,caster.player);fixture.players.put(allyId,ally.player);
|
||||
fixture.activate(caster);fixture.activate(ally);party(fixture,caster,ally);
|
||||
caster.core.location=new Location(caster.world,0,64,0);ally.core.location=new Location(caster.world,2,64,0);
|
||||
doReturn(List.of(ally.player)).when(caster.world).getNearbyEntities(any(Location.class),anyDouble(),anyDouble(),anyDouble());
|
||||
class Veto implements org.bukkit.event.Listener{
|
||||
@org.bukkit.event.EventHandler(priority=org.bukkit.event.EventPriority.HIGH)
|
||||
public void cancel(EntityRegainHealthEvent event){if(event.getEntity()==ally.player)event.setCancelled(true);}
|
||||
}
|
||||
fixture.dispatcher.register(new Veto(),fixture.plugin);
|
||||
use(fixture,caster);drain(fixture,2);
|
||||
for(int i=0;i<10;i++)fixture.delayed.removeFirst().run();drain(fixture,4);
|
||||
assertEquals(4,ally.core.health,0);
|
||||
}
|
||||
assertTrue(abilityEvents(events(data),"combat.healed").stream()
|
||||
.noneMatch(event->event.getAsJsonObject("data").get("target_player_id").getAsString().equals(allyId.toString())),
|
||||
"Cancelled ally healing must not become a saved-health success or a JSONL healing claim");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
private static List<com.google.gson.JsonObject> abilityEvents(List<com.google.gson.JsonObject> events,String suffix){
|
||||
return events.stream().filter(event->event.get("type").getAsString().endsWith(suffix)
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("sanctuary")).toList();
|
||||
}
|
||||
|
||||
@Test void fixedCylinderHasNoImmediatePulseThenSixOwnedHealingAndMitigationPulses() {
|
||||
@@ -145,6 +213,16 @@ class SanctuaryNativeTest {
|
||||
plugin.command.execute(sender.port,"fantasystick",new String[]{"config",update[0],update[1]});var response=plugin.tasks.poll(5,TimeUnit.SECONDS);assertNotNull(response);response.run();}
|
||||
var configured=registry.find("sanctuary").orElseThrow();assertEquals(new BigDecimal("4"),configured.parameters().get("healing-per-pulse"));
|
||||
assertEquals(new BigDecimal("30"),configured.parameters().get("damage-reduction-percent"));assertEquals(java.time.Duration.ofSeconds(20),configured.cooldown());
|
||||
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());
|
||||
plugin.command.execute(sender.port,"fstickadmin",new String[]{"config","sanctuary-radius-blocks","9"});
|
||||
var failed=plugin.tasks.poll(5,TimeUnit.SECONDS);assertNotNull(failed);failed.run();
|
||||
}
|
||||
assertEquals("8",((games.dmg.fantasystick.persistence.SanctuarySettings)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(plugin.plugin,"sanctuarySettings")).value("sanctuary-radius-blocks"));
|
||||
assertTrue(sender.messages.getLast().contains("save failed"));
|
||||
plugin.command.execute(sender.port,"fantasystick",new String[]{"config","sanctuary-radius-blocks","0"});assertTrue(sender.messages.getLast().contains("invalid"));
|
||||
plugin.command.execute(sender.port,"fantasystick",new String[]{"config","sanctuary-radius-blocks","8","extra"});assertTrue(sender.messages.getLast().contains("Usage"));
|
||||
plugin.plugin.setEnabled(false);plugin.plugin.setEnabled(true);
|
||||
@@ -152,6 +230,11 @@ class SanctuaryNativeTest {
|
||||
var saved=reloaded.find("sanctuary").orElseThrow();assertEquals(new BigDecimal("4"),saved.parameters().get("healing-per-pulse"));
|
||||
assertEquals(new BigDecimal("30"),saved.parameters().get("damage-reduction-percent"));assertEquals(java.time.Duration.ofSeconds(20),saved.cooldown());
|
||||
}
|
||||
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("sanctuary-")).toList();
|
||||
assertEquals(8,config.size(),"Authorized read and seven saved settings only");
|
||||
}
|
||||
|
||||
private static void use(FantasyStickLifecycleNativeTest.Fixture fixture,FantasyStickLifecycleNativeTest.Actor actor){actor.core.inventory.getInventory().setSelectedSlot(2);fixture.dispatcher.accept(new org.bukkit.event.player.PlayerInteractEvent(actor.player,org.bukkit.event.block.Action.RIGHT_CLICK_AIR,actor.core.inventory.getItem(2),null,org.bukkit.block.BlockFace.SELF,org.bukkit.inventory.EquipmentSlot.HAND));}
|
||||
|
||||
@@ -22,7 +22,9 @@ class SanctuarySettingsTest {
|
||||
var radius = settings.set(SanctuarySettings.RADIUS, "7").toCompletableFuture();
|
||||
var cooldown = settings.set(SanctuarySettings.COOLDOWN, "30").toCompletableFuture();
|
||||
var savedRadius = radius.get(5, TimeUnit.SECONDS); var savedCooldown = cooldown.get(5, TimeUnit.SECONDS);
|
||||
assertEquals(6, settings.settings().radius(), 0); settings.publish(savedCooldown); settings.publish(savedRadius);
|
||||
assertEquals(6, settings.settings().radius(), 0);
|
||||
assertTrue(settings.publish(savedCooldown));
|
||||
assertFalse(settings.publish(savedRadius), "A stale save callback cannot republish policy or claim a config event");
|
||||
assertEquals(7, settings.settings().radius(), 0); assertEquals(java.time.Duration.ofSeconds(30), settings.settings().cooldown());
|
||||
var yaml = (Map<?, ?>) new org.yaml.snakeyaml.Yaml().load(Files.readString(data.resolve("config.yml")));
|
||||
assertEquals("retained", yaml.get("unrelated")); assertEquals("7", String.valueOf(yaml.get(SanctuarySettings.RADIUS)));
|
||||
|
||||
Reference in new issue
Block a user