This commit is contained in:
1 parent
1d0cdc8f0f
commit
08d13d2192
8 files changed
+109
-10
No files matched your search
@@ -721,6 +721,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
abilityEngine.attachCastEvents(new games.dmg.fantasystick.events.AbilityCastEventBridge(eventLog::emit, Clock.systemUTC()));
|
||||
abilityEngine.enableCastEventsFor("fireball");
|
||||
abilityEngine.enableCastEventsFor("mending_light");
|
||||
abilityEngine.enableCastEventsFor("purify");
|
||||
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);
|
||||
|
||||
@@ -428,6 +428,10 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
|
||||
castEvents.healed(audit, target, aided.character(), aided.session(), saved,
|
||||
requested, actual, before, after, maximum);
|
||||
}
|
||||
@Override public void acceptedCleanse(EquipmentSessions.Active aided, Player target, List<String> removed) {
|
||||
if (castEvents != null && audit != null)
|
||||
castEvents.cleansed(audit, target, aided.character(), aided.session(), removed);
|
||||
}
|
||||
@Override public void effectEnded(UUID application, String reason) {
|
||||
Objects.requireNonNull(application); Objects.requireNonNull(reason);
|
||||
var effectId = observedEffects.remove(application);
|
||||
|
||||
@@ -33,6 +33,9 @@ public interface AbilityExecution extends AutoCloseable {
|
||||
org.bukkit.entity.Player target,
|
||||
games.dmg.fantasystick.persistence.CharacterRecord saved,
|
||||
double requested, double actual, double before, double after, double maximum);
|
||||
/** 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);
|
||||
/** 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. */
|
||||
|
||||
@@ -72,16 +72,22 @@ public final class Purify implements AbilityEffect {
|
||||
@Override public void activate(AbilityExecution execution) {
|
||||
if (!execution.stillCurrent()) return;
|
||||
var configured = from(execution.definition()); var target = target(execution.input(), configured.range());
|
||||
if (target == null) return;
|
||||
int removed = 0;
|
||||
if (target == null) { execution.resolve("miss"); return; }
|
||||
var removed = new java.util.ArrayList<String>();
|
||||
int attempted = 0;
|
||||
for (var type : HARMFUL) {
|
||||
if (!execution.stillCurrent() || !current(execution.input(), target, configured.range())) break;
|
||||
if (target.player().getPotionEffect(type) == null) continue;
|
||||
attempted++;
|
||||
target.player().removePotionEffect(type);
|
||||
if (target.player().getPotionEffect(type) == null) removed++;
|
||||
if (target.player().getPotionEffect(type) == null)
|
||||
removed.add(type.getKey().getKey().toUpperCase(java.util.Locale.ROOT));
|
||||
}
|
||||
if (removed == 0) return;
|
||||
var amount = BigDecimal.valueOf(removed);
|
||||
if (removed.isEmpty()) { execution.resolve(attempted == 0 ? "miss" : "cancelled"); return; }
|
||||
execution.acceptedCleanse(target.active(), target.player(), List.copyOf(removed));
|
||||
execution.resolve("cleansed", Map.of("target_player_id", target.player().getUniqueId().toString(),
|
||||
"removed_effects", List.copyOf(removed)));
|
||||
var amount = BigDecimal.valueOf(removed.size());
|
||||
execution.report(AbilityReport.Type.CLEANSING, target.player(), amount);
|
||||
if (target.player() != execution.input().player()
|
||||
&& execution.stillCurrent() && current(execution.input(), target, configured.range())) {
|
||||
|
||||
@@ -146,6 +146,25 @@ public final class AbilityCastEventBridge {
|
||||
return java.math.BigDecimal.valueOf(value).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
public void cleansed(Cast cast, org.bukkit.entity.Player target, UUID aidedCharacter,
|
||||
UUID aidedSession, java.util.List<String> removed) {
|
||||
if (cast == null || removed == null || removed.isEmpty()) 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());
|
||||
data.put("target_player_id", target.getUniqueId().toString());
|
||||
data.put("target_character_id", aidedCharacter.toString());
|
||||
data.put("target_session_id", aidedSession.toString());
|
||||
data.put("target_location", locationData(target.getLocation()));
|
||||
data.put("self_target", aidedCharacter.equals(cast.context().characterId()));
|
||||
data.put("removed_effects", java.util.List.copyOf(removed));
|
||||
data.put("actual_count", Integer.toString(removed.size()));
|
||||
emit(cast, "combat.cleansed", "accepted", data, application);
|
||||
} catch (RuntimeException ignored) { /* Native cleanse remains authoritative. */ }
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
@@ -67,10 +67,11 @@ public final class PurifySettings implements AutoCloseable {
|
||||
public String value(String key) { return effectiveValues.get(key); }
|
||||
public Purify.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) {
|
||||
|
||||
@@ -1296,8 +1296,11 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
|
||||
private void purifyConfiguration(CommandSender sender, String[] args) {
|
||||
String key = args[1];
|
||||
var actor = abilityConfigEvents == null ? null : abilityConfigEvents.capture(sender);
|
||||
String before = purifySettings.value(key);
|
||||
if (args.length == 2) {
|
||||
sender.sendMessage(key + "=" + purifySettings.value(key) + " (applies to future Purify casts only)"); return;
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.read(actor, "purify", key, before);
|
||||
sender.sendMessage(key + "=" + before + " (applies to future Purify casts only)"); return;
|
||||
}
|
||||
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.PurifySettings.Update> operation;
|
||||
try { operation = purifySettings.set(key, args[2]); }
|
||||
@@ -1309,8 +1312,9 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
if (closed) return;
|
||||
try { nextTick.accept(() -> {
|
||||
if (closed) return;
|
||||
if (failure == null) {
|
||||
purify.configure(saved.settings()); abilities.replace(purify.definition()); purifySettings.publish(saved);
|
||||
if (failure == null && purifySettings.publish(saved)) {
|
||||
purify.configure(saved.settings()); abilities.replace(purify.definition());
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.changed(actor, "purify", key, before, saved.value());
|
||||
}
|
||||
if (!sender.hasPermission(PERMISSION)
|
||||
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
|
||||
|
||||
@@ -73,6 +73,18 @@ class PurifyNativeTest {
|
||||
learnAndAssign(fixture, actor); use(fixture, actor);
|
||||
assertTrue(actor.messages.stream().anyMatch(message -> message.contains("ready in")));
|
||||
}
|
||||
var recorded = events(data);
|
||||
for (var type : List.of("ability.learned", "ability.assigned", "ability.cast", "ability.resolved", "combat.cleansed"))
|
||||
assertEquals(type.equals("ability.learned") || type.equals("ability.assigned") ? 2 : 1,
|
||||
recorded.stream().filter(event -> event.get("type").getAsString().endsWith(type)
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("purify")).count(), type);
|
||||
var cleansed = recorded.stream().filter(event -> event.get("type").getAsString().endsWith("combat.cleansed")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("purify")).findFirst().orElseThrow();
|
||||
var names = new java.util.HashSet<String>();
|
||||
cleansed.getAsJsonObject("data").getAsJsonArray("removed_effects").forEach(value -> names.add(value.getAsString()));
|
||||
assertEquals(java.util.Set.of("POISON", "WITHER", "SLOWNESS", "WEAKNESS"), names);
|
||||
assertEquals("4", cleansed.getAsJsonObject("data").get("actual_count").getAsString());
|
||||
}
|
||||
|
||||
@Test void aimedAllyActualCleanseCreatesPersistedSupportShareThatFollowsSourceCharacter() throws Exception {
|
||||
@@ -102,6 +114,23 @@ class PurifyNativeTest {
|
||||
assertEquals(BigInteger.valueOf(5), storage(fixture).find(ally.core.record.id()).get().orElseThrow().experience());
|
||||
assertTrue(holder.messages.stream().anyMatch(message -> message.contains("5") && message.contains("XP")));
|
||||
}
|
||||
var allyCleanse = events(data).stream().filter(event -> event.get("type").getAsString().endsWith("combat.cleansed")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("purify")).toList();
|
||||
assertEquals(1, allyCleanse.size());
|
||||
assertEquals("POISON", allyCleanse.getFirst().getAsJsonObject("data")
|
||||
.getAsJsonArray("removed_effects").get(0).getAsString());
|
||||
assertFalse(allyCleanse.getFirst().getAsJsonObject("data").get("self_target").getAsBoolean());
|
||||
}
|
||||
|
||||
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 normalSelfOrdinaryNonPartyOutOfRangeObstructedAndNoOpTargetsRefuseBeforeCooldown() throws Exception {
|
||||
@@ -125,6 +154,9 @@ class PurifyNativeTest {
|
||||
assertFalse(storage(fixture).find(caster.core.record.id()).get().orElseThrow().cooldowns().containsKey("purify"),
|
||||
"route " + route + " must refuse before cooldown");
|
||||
}
|
||||
assertEquals(0, events(directory).stream().filter(event -> event.get("type").getAsString().endsWith("combat.cleansed")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("purify")).count());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +177,14 @@ class PurifyNativeTest {
|
||||
assertTrue(effects.containsKey(PotionEffectType.POISON));
|
||||
assertNotNull(storage(fixture).find(caster.core.record.id()).get().orElseThrow().cooldowns().get("purify"));
|
||||
}
|
||||
var records = events(data);
|
||||
assertEquals(0, records.stream().filter(event -> event.get("type").getAsString().endsWith("combat.cleansed")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("purify")).count());
|
||||
assertEquals(1, records.stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("purify")
|
||||
&& event.getAsJsonObject("data").get("outcome").getAsString().equals("cancelled")).count());
|
||||
}
|
||||
|
||||
@Test void selfCleanseNeverEmitsSupportEvidence() {
|
||||
@@ -196,6 +236,19 @@ class PurifyNativeTest {
|
||||
effects(fixture, ally.player).put(PotionEffectType.SLOWNESS, instance(PotionEffectType.SLOWNESS, 200, 0));
|
||||
doReturn(false).when(caster.player).isSneaking(); use(fixture, caster); drain(fixture, 2);
|
||||
assertNull(ally.player.getPotionEffect(PotionEffectType.SLOWNESS));
|
||||
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", "purify-range-blocks", "21"});
|
||||
var failed = fixture.tasks.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(failed); failed.run();
|
||||
}
|
||||
assertEquals("20", ((games.dmg.fantasystick.persistence.PurifySettings)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "purifySettings"))
|
||||
.value("purify-range-blocks"));
|
||||
assertTrue(sender.messages.getLast().contains("save failed"));
|
||||
fixture.command.execute(sender.port, "fantasystick", new String[]{"config", "purify-range-blocks", "0"});
|
||||
assertTrue(sender.messages.getLast().contains("invalid"));
|
||||
fixture.command.execute(sender.port, "fantasystick", new String[]{"config", "purify-range-blocks", "20", "extra"});
|
||||
@@ -205,6 +258,14 @@ class PurifyNativeTest {
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "abilityRegistry");
|
||||
assertEquals(0, new BigDecimal("20").compareTo(reloaded.find("purify").orElseThrow().parameters().get("range")));
|
||||
}
|
||||
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().equals("purify-range-blocks")).toList();
|
||||
assertEquals(2, config.size());
|
||||
assertEquals("16", config.getFirst().getAsJsonObject("data").get("after").getAsString());
|
||||
assertEquals("16", config.getLast().getAsJsonObject("data").get("before").getAsString());
|
||||
assertEquals("20", config.getLast().getAsJsonObject("data").get("after").getAsString());
|
||||
for (String invalid : List.of("purify-cooldown-seconds: -1\n", "purify-range-blocks: 0\n")) {
|
||||
var directory = data.resolve(Integer.toHexString(invalid.hashCode())); java.nio.file.Files.createDirectories(directory);
|
||||
java.nio.file.Files.writeString(directory.resolve("config.yml"), invalid);
|
||||
|
||||
Reference in new issue
Block a user