This commit is contained in:
1 parent
e6b66d9e34
commit
4616ee80e3
6 files changed
+236
-18
No files matched your search
@@ -762,6 +762,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
abilityEngine.enableCastEventsFor("frost_shard");
|
||||
abilityEngine.enableCastEventsFor("longbow");
|
||||
abilityEngine.enableCastEventsFor("quickstep");
|
||||
abilityEngine.enableCastEventsFor("second_wind");
|
||||
abilityEngine.enableCastEventsFor("mending_light");
|
||||
abilityEngine.enableCastEventsFor("purify");
|
||||
abilityEngine.enableCastEventsFor("guardian_bond");
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
|
||||
private void refuseCast(ControlInput input, CharacterRecord record, AbilityDefinition definition, String reason) {
|
||||
if (castEvents == null || record == null
|
||||
|| !(definition.id().equals("great_sword") || definition.id().equals("longbow")
|
||||
|| definition.id().equals("quickstep"))
|
||||
|| definition.id().equals("quickstep") || definition.id().equals("second_wind"))
|
||||
|| !eventOwnedAbilities.contains(definition.id())) return;
|
||||
resolve(castEvents.begin(input, record, definition), "refused", reason);
|
||||
}
|
||||
|
||||
@@ -80,17 +80,24 @@ public final class SecondWind implements AbilityEffect {
|
||||
BigInteger base = definition.parameters().get("base-healing").toBigIntegerExact();
|
||||
var player = execution.input().player();
|
||||
var attribute = player.getAttribute(Attribute.MAX_HEALTH);
|
||||
if (attribute == null) { execution.finish(); return; }
|
||||
if (attribute == null) { execution.resolve("cancelled", Map.of("reason", "maximum_unavailable")); execution.finish(); return; }
|
||||
double current = player.getHealth(); double maximum = attribute.getValue();
|
||||
double missing = maximum - current;
|
||||
if (!Double.isFinite(current) || !Double.isFinite(maximum) || missing <= 0) { execution.finish(); return; }
|
||||
if (!Double.isFinite(current) || !Double.isFinite(maximum) || missing <= 0) {
|
||||
execution.resolve("cancelled", Map.of("reason", "health_unavailable_or_full")); execution.finish(); return;
|
||||
}
|
||||
BigInteger exact = healing(base, execution.character().stats().get(Stat.WIS));
|
||||
double proposed = exact.bitLength() > 1023 ? missing : Math.min(missing, exact.doubleValue());
|
||||
if (!Double.isFinite(proposed) || proposed <= 0) { execution.finish(); return; }
|
||||
if (!Double.isFinite(proposed) || proposed <= 0) {
|
||||
execution.resolve("cancelled", Map.of("reason", "healing_nonpositive")); execution.finish(); return;
|
||||
}
|
||||
var event = new EntityRegainHealthEvent(player, proposed, EntityRegainHealthEvent.RegainReason.CUSTOM);
|
||||
healingEvents.accept(event);
|
||||
double accepted = Math.min(missing, event.getAmount());
|
||||
if (event.isCancelled() || !Double.isFinite(accepted) || accepted <= 0) { execution.finish(); return; }
|
||||
if (event.isCancelled() || !Double.isFinite(accepted) || accepted <= 0) {
|
||||
execution.resolve("cancelled", Map.of("reason", "native_healing_cancelled_or_nonpositive"));
|
||||
execution.finish(); return;
|
||||
}
|
||||
var record = execution.character(); var expected = record.revision();
|
||||
BigDecimal target = BigDecimal.valueOf(current + accepted);
|
||||
BigDecimal limit = nativeLimit.get();
|
||||
@@ -99,18 +106,33 @@ public final class SecondWind implements AbilityEffect {
|
||||
nextTick.accept(() -> {
|
||||
try {
|
||||
if (failure != null || saved == null || saved.isEmpty() || !execution.stillCurrent()) {
|
||||
execution.resolve(failure != null || saved == null || saved.isEmpty() ? "failed" : "interrupted",
|
||||
Map.of("reason", failure != null ? "health_save_failed" : saved == null || saved.isEmpty()
|
||||
? "health_save_unconfirmed" : "session_changed_before_healing"));
|
||||
player.sendMessage("Second Wind health could not be saved; no healing was applied."); return;
|
||||
}
|
||||
var persisted = saved.orElseThrow();
|
||||
if (!sessions.acknowledgeProgress(execution.input().session(), expected, persisted)) {
|
||||
execution.resolve("interrupted", Map.of("reason", "health_saved_session_changed"));
|
||||
player.sendMessage("Second Wind was saved, but the session changed; no native healing was applied."); return;
|
||||
}
|
||||
double nativeBefore = player.getHealth();
|
||||
double publish = Math.min(attribute.getValue(), Math.max(nativeBefore, persisted.health().doubleValue()));
|
||||
player.setHealth(publish);
|
||||
double restored = publish - nativeBefore;
|
||||
if (Double.isFinite(restored) && restored > 0) execution.report(AbilityReport.Type.HEALING,
|
||||
player, BigDecimal.valueOf(restored).stripTrailingZeros());
|
||||
try { player.setHealth(publish); }
|
||||
catch (RuntimeException nativeFailure) {
|
||||
execution.resolve("failed", Map.of("reason", "native_health_publication_failed"));
|
||||
throw nativeFailure;
|
||||
}
|
||||
double nativeAfter = player.getHealth();
|
||||
double restored = nativeAfter - nativeBefore;
|
||||
if (Double.isFinite(restored) && restored > 0) {
|
||||
try { execution.acceptedHeal(execution.input().session(), player, persisted, proposed,
|
||||
restored, nativeBefore, nativeAfter, maximum); }
|
||||
catch (RuntimeException ignored) { /* Auditing cannot change an applied native heal. */ }
|
||||
execution.resolve("healed", Map.of("reason", "native_self_heal_applied"));
|
||||
execution.report(AbilityReport.Type.HEALING, player,
|
||||
BigDecimal.valueOf(restored).stripTrailingZeros());
|
||||
} else execution.resolve("cancelled", Map.of("reason", "native_heal_not_observed"));
|
||||
} finally { execution.finish(); }
|
||||
});
|
||||
} catch (RuntimeException rejected) { execution.finish(); }
|
||||
|
||||
@@ -70,11 +70,12 @@ public final class SecondWindSettings implements AutoCloseable {
|
||||
public SecondWind.Settings settings() { return parse(effectiveValues); }
|
||||
|
||||
/** Publish only after the caller returns the durable acknowledgment to the server thread. */
|
||||
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) {
|
||||
|
||||
@@ -1702,8 +1702,11 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
|
||||
private void secondWindConfiguration(CommandSender sender, String[] args) {
|
||||
String key = args[1];
|
||||
var actor = abilityConfigEvents == null ? null : abilityConfigEvents.capture(sender);
|
||||
String before = secondWindSettings.value(key);
|
||||
if (args.length == 2) {
|
||||
sender.sendMessage(key + "=" + secondWindSettings.value(key) + " (applies to future Second Wind casts only)");
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.read(actor, "second_wind", key, before);
|
||||
sender.sendMessage(key + "=" + before + " (applies to future Second Wind casts only)");
|
||||
return;
|
||||
}
|
||||
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.SecondWindSettings.Update> operation;
|
||||
@@ -1717,15 +1720,18 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
try {
|
||||
nextTick.accept(() -> {
|
||||
if (closed) return;
|
||||
if (failure == null) {
|
||||
secondWind.configure(saved.settings()); abilities.replace(secondWind.definition());
|
||||
secondWindSettings.publish(saved);
|
||||
String effectiveBefore = failure == null ? secondWindSettings.value(key) : null;
|
||||
boolean applied = failure == null && secondWindSettings.publish(saved);
|
||||
if (applied) {
|
||||
secondWind.configure(secondWindSettings.settings()); abilities.replace(secondWind.definition());
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.changed(actor, "second_wind", key, effectiveBefore, saved.value());
|
||||
}
|
||||
if (!sender.hasPermission(PERMISSION)
|
||||
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
|
||||
sender.sendMessage(failure == null
|
||||
? "Saved " + key + "=" + saved.value() + " for future Second Wind casts; active work and deadlines are unchanged."
|
||||
: "Second Wind configuration save failed; the effective setting is unchanged.");
|
||||
sender.sendMessage(failure != null
|
||||
? "Second Wind configuration save failed; the effective setting is unchanged."
|
||||
: applied ? "Saved " + key + "=" + saved.value() + " for future Second Wind casts; active work and deadlines are unchanged."
|
||||
: "Saved request was superseded; effective " + key + "=" + secondWindSettings.value(key) + ".");
|
||||
});
|
||||
} catch (RuntimeException rejected) { report(null, "config-response", rejected); }
|
||||
});
|
||||
|
||||
@@ -25,10 +25,16 @@ class SecondWindNativeTest {
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void bookAcquisitionActualNativeHealPersistsBeforePublicationAndRetainsCooldown() throws Exception {
|
||||
java.util.UUID expectedPlayer, expectedCharacter, expectedSession, expectedWorld;
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data); withSecondWind(data, a, false, BigDecimal.valueOf(4));
|
||||
f.players.put(a.core.id, a.player); f.activate(a);
|
||||
expectedPlayer = a.core.id; expectedCharacter = a.core.record.id();
|
||||
expectedSession = ((games.dmg.fantasystick.session.EquipmentSessions)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "sessions"))
|
||||
.active(expectedPlayer).orElseThrow().session();
|
||||
expectedWorld = a.core.location.getWorld().getUID();
|
||||
a.core.inventory.getInventory().setSelectedSlot(0); PartyBookNativeTest.open(f, a);
|
||||
String text = PartyBookNativeTest.text(a).replaceAll("\\s+", "");
|
||||
assertTrue(text.contains("SecondWind")); assertTrue(text.contains("CON12")); assertTrue(text.contains("WIS12"));
|
||||
@@ -57,6 +63,34 @@ class SecondWindNativeTest {
|
||||
learnAndAssign(f, a); use(f, a);
|
||||
assertTrue(a.messages.stream().anyMatch(message -> message.contains("ready in")));
|
||||
}
|
||||
var own = events(data).stream().filter(event -> event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("second_wind")).toList();
|
||||
assertEquals(2, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.learned")).count());
|
||||
assertEquals(2, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.assigned")).count());
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.unlearned")).count());
|
||||
var cast = own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.cast"))
|
||||
.findFirst().orElseThrow().getAsJsonObject("data");
|
||||
var healed = own.stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed"))
|
||||
.findFirst().orElseThrow().getAsJsonObject("data");
|
||||
assertEquals(2, cast.get("slot").getAsInt());
|
||||
assertEquals("12", cast.get("captured_wis").getAsString());
|
||||
assertTrue(cast.has("cooldown_until"));
|
||||
assertEquals(cast.get("action_id").getAsString(), healed.get("action_id").getAsString());
|
||||
assertEquals(expectedPlayer.toString(), cast.get("player_id").getAsString());
|
||||
assertEquals(expectedCharacter.toString(), cast.get("character_id").getAsString());
|
||||
assertEquals(expectedSession.toString(), cast.get("session_id").getAsString());
|
||||
assertEquals(expectedPlayer.toString(), healed.get("target_player_id").getAsString());
|
||||
assertEquals(expectedCharacter.toString(), healed.get("target_character_id").getAsString());
|
||||
assertEquals(expectedSession.toString(), healed.get("target_session_id").getAsString());
|
||||
assertEquals(expectedWorld.toString(), healed.getAsJsonObject("target_location").get("world_id").getAsString());
|
||||
assertEquals("4", healed.get("health_before").getAsString());
|
||||
assertEquals("11", healed.get("health_after").getAsString());
|
||||
assertEquals("7", healed.get("actual_amount").getAsString());
|
||||
assertEquals("7", healed.get("requested_amount").getAsString());
|
||||
assertTrue(healed.getAsJsonObject("target_location").has("world_id"));
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed")).count());
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").get("outcome").getAsString().equals("healed")).count());
|
||||
}
|
||||
|
||||
@Test void fullHealthAndCancelledNativeHealingApplyNoHealth() throws Exception {
|
||||
@@ -82,6 +116,114 @@ class SecondWindNativeTest {
|
||||
if (full) assertFalse(saved.cooldowns().containsKey("second_wind"));
|
||||
else assertNotNull(saved.cooldowns().get("second_wind"));
|
||||
}
|
||||
for (boolean full : new boolean[]{true, false}) {
|
||||
var own = events(data.resolve(Boolean.toString(full))).stream()
|
||||
.filter(event -> event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("second_wind")).toList();
|
||||
assertEquals(full ? 0 : 1, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.cast")).count());
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").get("outcome").getAsString().equals(full ? "refused" : "cancelled")).count());
|
||||
assertEquals(0, own.stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed")).count());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void nativeAdjustedHealIsAuditedDespiteDisabledRoutineFeedback() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
withSecondWind(data, a, true, BigDecimal.valueOf(4));
|
||||
f.players.put(a.core.id, a.player); f.activate(a);
|
||||
assertTrue(f.feedbackCommand.execute(a.player, "fantasyfeedback", new String[]{"off"}));
|
||||
f.tick();
|
||||
class Reduce implements org.bukkit.event.Listener {
|
||||
@org.bukkit.event.EventHandler public void heal(EntityRegainHealthEvent event) {
|
||||
if (event.getEntity() == a.player && event.getRegainReason() == EntityRegainHealthEvent.RegainReason.CUSTOM)
|
||||
event.setAmount(2);
|
||||
}
|
||||
}
|
||||
f.dispatcher.register(new Reduce(), f.plugin);
|
||||
use(f, a); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
assertEquals(6, a.player.getHealth());
|
||||
assertFalse(a.messages.stream().anyMatch(message -> message.contains("Second Wind healed")));
|
||||
f.plugin.setEnabled(false);
|
||||
}
|
||||
var own = events(data).stream().filter(event -> event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("second_wind")).toList();
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed")).count());
|
||||
var healed = own.stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed"))
|
||||
.findFirst().orElseThrow().getAsJsonObject("data");
|
||||
assertEquals("7", healed.get("requested_amount").getAsString());
|
||||
assertEquals("2", healed.get("actual_amount").getAsString());
|
||||
assertEquals("6", healed.get("health_after").getAsString());
|
||||
}
|
||||
|
||||
@Test void rejectedEventStoreDoesNotChangeApprovedSelfHealingOrDurableHealth() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
withSecondWind(data, a, true, BigDecimal.valueOf(4));
|
||||
f.players.put(a.core.id, a.player); f.activate(a);
|
||||
var eventLog = (games.dmg.fantasystick.events.LocalEventLog)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "eventLog");
|
||||
eventLog.close();
|
||||
use(f, a); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
assertEquals(11, a.player.getHealth());
|
||||
var storage = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
assertEquals(0, BigDecimal.valueOf(11).compareTo(storage.find(a.core.record.id()).get().orElseThrow().health()));
|
||||
f.plugin.setEnabled(false);
|
||||
}
|
||||
assertEquals(0, events(data).stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed"))
|
||||
.count(), "A rejected audit record cannot manufacture healing or undo real native health");
|
||||
}
|
||||
|
||||
@Test void staleHealthRevisionAfterCooldownCannotClaimAHealOrOverwriteAnotherWrite() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
withSecondWind(data, a, true, BigDecimal.valueOf(4));
|
||||
f.players.put(a.core.id, a.player); f.activate(a); use(f, a);
|
||||
var cooldownResponse = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(cooldownResponse);
|
||||
var storage = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
var before = storage.find(a.core.record.id()).get().orElseThrow();
|
||||
assertNotNull(before.cooldowns().get("second_wind"));
|
||||
assertTrue(storage.saveHealth(before.id(), before.revision(), BigDecimal.valueOf(5),
|
||||
BigDecimal.valueOf(1024)).get().isPresent());
|
||||
cooldownResponse.run();
|
||||
var healthResponse = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(healthResponse);
|
||||
healthResponse.run();
|
||||
assertEquals(4, a.player.getHealth(), "Stale Second Wind cannot publish native health");
|
||||
assertEquals(0, BigDecimal.valueOf(5).compareTo(storage.find(before.id()).get().orElseThrow().health()));
|
||||
f.plugin.setEnabled(false);
|
||||
}
|
||||
var own = events(data).stream().filter(event -> event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("second_wind")).toList();
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").get("outcome").getAsString().equals("failed")).count());
|
||||
assertEquals(0, own.stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed")).count());
|
||||
}
|
||||
|
||||
@Test void failedHealthSaveAfterAcknowledgedCooldownNeverClaimsNativeHealing() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
withSecondWind(data, a, true, BigDecimal.valueOf(4));
|
||||
f.players.put(a.core.id, a.player); f.activate(a);
|
||||
use(f, a);
|
||||
var cooldownResponse = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
assertNotNull(cooldownResponse, "Cooldown must commit before the health-save failure");
|
||||
var storage = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
storage.close(); cooldownResponse.run();
|
||||
var healthResponse = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
assertNotNull(healthResponse); healthResponse.run();
|
||||
assertEquals(4, a.player.getHealth(), "Failed health save cannot publish native health");
|
||||
f.plugin.setEnabled(false);
|
||||
}
|
||||
var own = events(data).stream().filter(event -> event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("second_wind")).toList();
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.cast")).count());
|
||||
assertEquals(1, own.stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").get("outcome").getAsString().equals("failed")).count());
|
||||
assertEquals(0, own.stream().filter(event -> event.get("type").getAsString().endsWith("combat.healed")).count());
|
||||
}
|
||||
|
||||
@Test void failedHealthPersistencePublishesNoNativeHealing() {
|
||||
@@ -116,6 +258,38 @@ class SecondWindNativeTest {
|
||||
verify(player, never()).setHealth(anyDouble()); verify(execution).finish();
|
||||
}
|
||||
|
||||
@Test void authorizedRegisteredConfigurationReadAndAcknowledgedChangeReachJsonlOnlyAfterSave() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var admin = new EquipmentAdminCommandNativeTest.Sender(); admin.allowed = true;
|
||||
f.command.execute(admin.port, "fstickadmin", new String[]{"config", "second-wind-base-healing"});
|
||||
assertTrue(admin.messages.getLast().contains("second-wind-base-healing=6"));
|
||||
f.command.execute(admin.port, "fstickadmin", new String[]{"config", "second-wind-base-healing", "9"});
|
||||
var saved = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(saved); saved.run();
|
||||
var registry = (games.dmg.fantasystick.ability.AbilityRegistry)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "abilityRegistry");
|
||||
assertEquals(new BigDecimal("9"), registry.find("second_wind").orElseThrow()
|
||||
.parameters().get("base-healing"));
|
||||
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());
|
||||
f.command.execute(admin.port, "fstickadmin", new String[]{"config", "second-wind-base-healing", "11"});
|
||||
var failed = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(failed); failed.run();
|
||||
}
|
||||
admin.allowed = false;
|
||||
f.command.execute(admin.port, "fstickadmin", new String[]{"config", "second-wind-base-healing"});
|
||||
f.plugin.setEnabled(false);
|
||||
}
|
||||
var config = events(data).stream().filter(event -> event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("second_wind")
|
||||
&& (event.get("type").getAsString().endsWith("config.read")
|
||||
|| event.get("type").getAsString().endsWith("config.changed"))).toList();
|
||||
assertEquals(2, config.size(), "Denied reads and failed writes cannot claim success");
|
||||
assertEquals("6", config.getLast().getAsJsonObject("data").get("before").getAsString());
|
||||
assertEquals("9", config.getLast().getAsJsonObject("data").get("after").getAsString());
|
||||
}
|
||||
|
||||
@Test void configuredDefinitionFormulaAndAdminRoutesCoverBothSettings() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"second-wind-base-healing: 8\nsecond-wind-cooldown-seconds: 20\n");
|
||||
@@ -168,6 +342,20 @@ class SecondWindNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static java.util.List<com.google.gson.JsonObject> events(Path data) throws Exception {
|
||||
var results = new java.util.ArrayList<com.google.gson.JsonObject>();
|
||||
try (var days = java.nio.file.Files.list(data.resolve("events"))) {
|
||||
for (var day : days.filter(path -> path.getFileName().toString().matches("\\d{4}-\\d{2}-\\d{2}")).toList()) {
|
||||
try (var segments = java.nio.file.Files.list(day)) {
|
||||
for (var path : segments.filter(file -> file.getFileName().toString().endsWith(".jsonl")).toList())
|
||||
for (var line : java.nio.file.Files.readAllLines(path))
|
||||
if (!line.isBlank()) results.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void learnAndAssign(FantasyStickLifecycleNativeTest.Fixture fixture,
|
||||
FantasyStickLifecycleNativeTest.Actor actor) throws Exception {
|
||||
actor.core.inventory.getInventory().setSelectedSlot(0); PartyBookNativeTest.open(fixture, actor);
|
||||
|
||||
Reference in new issue
Block a user