feat(events): record Fireball and configuration outcomes
Release / release (push) Successful in 14m20s
Release / release (push) Successful in 14m20s
This commit is contained in:
1 parent
dd75e6a76f
commit
9674a8e5e6
12 files changed
+418
-21
No files matched your search
@@ -712,12 +712,15 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
getServer().getVersion(), getServer().getMinecraftVersion(), getDescription().getVersion()), 1024, 134_217_728L,
|
||||
message -> getLogger().warning(message));
|
||||
eventCapture = new games.dmg.fantasystick.events.EventCaptureListener(eventLog, sessions, Clock.systemUTC());
|
||||
eventCapture.excludeOwnedDamage(fireball::ownsAcceptedDamage);
|
||||
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);
|
||||
progressionEvents = new games.dmg.fantasystick.events.ProgressionEventBridge(eventLog::emit, Clock.systemUTC());books.attachProgressionEvents(progressionEvents);
|
||||
abilityEvents = new games.dmg.fantasystick.events.AbilityEventBridge(eventLog::emit, Clock.systemUTC());books.attachAbilityEvents(abilityEvents);
|
||||
abilityEngine.attachCastEvents(new games.dmg.fantasystick.events.AbilityCastEventBridge(eventLog::emit, Clock.systemUTC()));
|
||||
abilityEngine.enableCastEventsFor("fireball");
|
||||
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);
|
||||
reviveEvents = new games.dmg.fantasystick.events.ReviveEventBridge(eventLog::emit, Clock.systemUTC());revive.attachEvents(reviveEvents);
|
||||
|
||||
@@ -239,7 +239,7 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
|
||||
return;
|
||||
}
|
||||
CharacterRecord saved = mutation.record().orElseThrow();
|
||||
if (castEvents != null) castEvents.cast(audit, saved, readyAt);
|
||||
if (castEvents != null) castEvents.cast(audit, saved, readyAt, definition);
|
||||
boolean synchronizedProgress = unchangedDeadline
|
||||
? sessions.currentRecord(input.session()).filter(saved::equals).isPresent()
|
||||
: sessions.acknowledgeProgress(input.session(), expectedRevision, saved);
|
||||
@@ -331,6 +331,7 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
|
||||
private final Map<UUID, String> observedEffects = new HashMap<>();
|
||||
private final Set<AutoCloseable> owned = new HashSet<>();
|
||||
private String outcome;
|
||||
private Map<String, Object> resolutionEvidence = Map.of();
|
||||
private boolean finished;
|
||||
private int scheduled;
|
||||
|
||||
@@ -407,15 +408,29 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
|
||||
observedEffects.put(application, effectId);
|
||||
return application;
|
||||
}
|
||||
@Override public void acceptedDamage(LivingEntity target, double requested, double actual,
|
||||
Entity direct, Map<String, Object> evidence) {
|
||||
if (castEvents == null || audit == null) return;
|
||||
try {
|
||||
var details = new java.util.LinkedHashMap<String, Object>(evidence);
|
||||
if (target instanceof Player player)
|
||||
sessions.active(player.getUniqueId()).filter(sessions::current).ifPresent(active -> {
|
||||
details.put("target_character_id", active.character().toString());
|
||||
details.put("target_session_id", active.session().toString());
|
||||
});
|
||||
castEvents.acceptedDamage(audit, target, requested, actual, direct, details);
|
||||
} catch (RuntimeException ignored) { /* Recording cannot change accepted native damage. */ }
|
||||
}
|
||||
@Override public void effectEnded(UUID application, String reason) {
|
||||
Objects.requireNonNull(application); Objects.requireNonNull(reason);
|
||||
var effectId = observedEffects.remove(application);
|
||||
if (effectId != null && castEvents != null) castEvents.effectEnded(audit, application, effectId, reason);
|
||||
}
|
||||
@Override public void resolve(String result) {
|
||||
@Override public void resolve(String result) { resolve(result, Map.of()); }
|
||||
@Override public void resolve(String result, Map<String, Object> evidence) {
|
||||
if (!Set.of("completed", "hit", "miss", "expired", "interrupted", "failed", "cancelled").contains(result))
|
||||
throw new IllegalArgumentException("Unsupported ability resolution");
|
||||
if (!finished) outcome = result;
|
||||
if (!finished) { outcome = result; resolutionEvidence = Map.copyOf(Objects.requireNonNull(evidence)); }
|
||||
}
|
||||
@Override public void finish() {
|
||||
if (outcome == null) outcome = "completed";
|
||||
@@ -428,7 +443,8 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
|
||||
var resources = List.copyOf(owned); owned.clear();
|
||||
for (var resource : resources) try { resource.close(); } catch (Exception failure) { AbilityEngine.this.report(key.character(), key.ability(), failure); }
|
||||
for (var application : List.copyOf(observedEffects.keySet())) effectEnded(application, outcome);
|
||||
AbilityEngine.this.resolve(audit, outcome, outcome.equals("completed") ? "effect_finished_without_hit_claim" : "effect_ended");
|
||||
if (castEvents != null) castEvents.resolved(audit, outcome,
|
||||
outcome.equals("completed") ? "effect_finished_without_hit_claim" : "effect_ended", resolutionEvidence);
|
||||
var runtimes = active.get(key);
|
||||
if (runtimes != null) { runtimes.remove(this); if (runtimes.isEmpty()) active.remove(key); }
|
||||
}
|
||||
|
||||
@@ -25,10 +25,14 @@ public interface AbilityExecution extends AutoCloseable {
|
||||
void report(AbilityReport.Type type, Entity target, BigDecimal amount);
|
||||
/** An owner reports an effect only after its actual native application. */
|
||||
UUID effectApplied(String effectId, Entity target, Map<String, Object> evidence);
|
||||
/** Only the effect owner calls this after positive, uncancelled native damage is accepted. */
|
||||
void acceptedDamage(LivingEntity target, double requested, double actual, Entity direct,
|
||||
Map<String, Object> evidence);
|
||||
/** 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. */
|
||||
void resolve(String outcome);
|
||||
void resolve(String outcome, Map<String, Object> evidence);
|
||||
void finish();
|
||||
@Override void close();
|
||||
}
|
||||
@@ -56,6 +56,8 @@ public final class Fireball implements AbilityEffect, Listener, AutoCloseable {
|
||||
final BigInteger intelligence;
|
||||
final Settings settings;
|
||||
LivingEntity target;
|
||||
double requestedDamage;
|
||||
boolean accepted;
|
||||
boolean finished;
|
||||
State(Arrow projectile, AbilityExecution execution, Location origin, BigInteger intelligence, Settings settings) {
|
||||
this.projectile = projectile; this.execution = execution; this.origin = origin;
|
||||
@@ -123,8 +125,14 @@ public final class Fireball implements AbilityEffect, Listener, AutoCloseable {
|
||||
}
|
||||
execution.own(state);
|
||||
long rangeTicks = Math.max(1, (long) Math.ceil(current.range() / current.speedPerTick()));
|
||||
execution.later(Duration.ofMillis(Math.multiplyExact(rangeTicks, 50)), execution::finish);
|
||||
execution.later(current.lifetime(), execution::finish);
|
||||
execution.later(Duration.ofMillis(Math.multiplyExact(rangeTicks, 50)), () -> {
|
||||
execution.resolve("expired", Map.of("projectile_id", projectile.getUniqueId().toString(),
|
||||
"limit", "range", "effective_settings", settingsData(current))); execution.finish();
|
||||
});
|
||||
execution.later(current.lifetime(), () -> {
|
||||
execution.resolve("expired", Map.of("projectile_id", projectile.getUniqueId().toString(),
|
||||
"limit", "lifetime", "effective_settings", settingsData(current))); execution.finish();
|
||||
});
|
||||
particles.world(Particle.FLAME, origin, 8, 0.15, 0.15, 0.15, 0.01);
|
||||
}
|
||||
|
||||
@@ -141,14 +149,44 @@ public final class Fireball implements AbilityEffect, Listener, AutoCloseable {
|
||||
var source = DamageSource.builder(DamageType.FIREBALL).withCausingEntity(state.execution.input().player())
|
||||
.withDirectEntity(state.projectile).withDamageLocation(state.projectile.getLocation()).build();
|
||||
state.target = target;
|
||||
state.requestedDamage = nativeDamage;
|
||||
damaging = state;
|
||||
try { state.execution.damage(target, nativeDamage, source); }
|
||||
finally { if (damaging == state) damaging = null; }
|
||||
} finally {
|
||||
state.execution.finish();
|
||||
String outcome = state.accepted ? "hit" : event.isCancelled() ? "cancelled" : "miss";
|
||||
try {
|
||||
if (outcome.equals("miss") && (closed || !state.execution.stillCurrent())) outcome = "interrupted";
|
||||
var evidence = new java.util.LinkedHashMap<String, Object>();
|
||||
evidence.put("projectile_id", state.projectile.getUniqueId().toString());
|
||||
evidence.put("direct_entity_id", state.projectile.getUniqueId().toString());
|
||||
evidence.put("effective_settings", settingsData(state.settings));
|
||||
var impact = event.getEntity().getLocation();
|
||||
if (impact != null && impact.getWorld() != null) evidence.put("target_location", locationData(impact));
|
||||
if (event.getHitEntity() != null) evidence.put("target_entity_id", event.getHitEntity().getUniqueId().toString());
|
||||
state.execution.resolve(outcome, evidence);
|
||||
} catch (RuntimeException auditFailure) {
|
||||
state.execution.resolve(outcome);
|
||||
} finally {
|
||||
state.execution.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> settingsData(Settings settings) {
|
||||
return Map.of("base-damage", settings.baseDamage().toString(),
|
||||
"cooldown-seconds", seconds(settings.cooldown()).toPlainString(),
|
||||
"range", Double.toString(settings.range()),
|
||||
"lifetime", seconds(settings.lifetime()).toPlainString(),
|
||||
"projectile-speed", Double.toString(settings.speedPerTick()));
|
||||
}
|
||||
|
||||
private static Map<String, Object> locationData(Location location) {
|
||||
return Map.of("world_id", location.getWorld().getUID().toString(),
|
||||
"world_name", location.getWorld().getName(), "x", location.getX(),
|
||||
"y", location.getY(), "z", location.getZ());
|
||||
}
|
||||
|
||||
private boolean inRange(State state, Location current) {
|
||||
return current.getWorld() != null && state.origin.getWorld() != null
|
||||
&& current.getWorld().equals(state.origin.getWorld())
|
||||
@@ -162,16 +200,25 @@ public final class Fireball implements AbilityEffect, Listener, AutoCloseable {
|
||||
if (state != null && damaging != state) event.setCancelled(true);
|
||||
}
|
||||
|
||||
/** One native application belongs to this projectile, including a fantasy-player target. */
|
||||
public boolean ownsAcceptedDamage(EntityDamageEvent event) {
|
||||
var state = damaging;
|
||||
if (state == null || event.getEntity() != state.target) return false;
|
||||
var direct = event.getDamageSource().getDirectEntity();
|
||||
if (direct != state.projectile && (!(event instanceof org.bukkit.event.entity.EntityDamageByEntityEvent byEntity)
|
||||
|| byEntity.getDamager() != state.projectile)) return false;
|
||||
var causing = event.getDamageSource().getCausingEntity();
|
||||
return causing == null || causing == state.execution.input().player();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void acceptedDamage(EntityDamageEvent event) {
|
||||
var state = damaging;
|
||||
if (state == null || event.getEntity() != state.target || event.getFinalDamage() <= 0
|
||||
|| !Double.isFinite(event.getFinalDamage())) return;
|
||||
var direct = event.getDamageSource().getDirectEntity();
|
||||
if (direct != state.projectile && (!(event instanceof org.bukkit.event.entity.EntityDamageByEntityEvent byEntity)
|
||||
|| byEntity.getDamager() != state.projectile)) return;
|
||||
var causing = event.getDamageSource().getCausingEntity();
|
||||
if (causing != null && causing != state.execution.input().player()) return;
|
||||
if (state == null || state.accepted || event.getFinalDamage() <= 0
|
||||
|| !Double.isFinite(event.getFinalDamage()) || !ownsAcceptedDamage(event)) return;
|
||||
state.accepted = true;
|
||||
state.execution.acceptedDamage(state.target, state.requestedDamage, event.getFinalDamage(),
|
||||
state.projectile, Map.of("damage_kind", "owned_direct_projectile"));
|
||||
state.execution.report(AbilityReport.Type.DAMAGE, event.getEntity(), BigDecimal.valueOf(event.getFinalDamage()));
|
||||
}
|
||||
|
||||
@@ -182,7 +229,9 @@ public final class Fireball implements AbilityEffect, Listener, AutoCloseable {
|
||||
@Override public void close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
for (var state : List.copyOf(projectiles.values())) state.execution.finish();
|
||||
for (var state : List.copyOf(projectiles.values())) {
|
||||
state.execution.resolve("interrupted"); state.execution.finish();
|
||||
}
|
||||
projectiles.clear(); damaging = null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import games.dmg.fantasystick.ability.AbilityDefinition;
|
||||
import games.dmg.fantasystick.progression.Stat;
|
||||
import games.dmg.fantasystick.persistence.CharacterRecord;
|
||||
import games.dmg.fantasystick.session.ControlInput;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -46,15 +47,33 @@ public final class AbilityCastEventBridge {
|
||||
}
|
||||
}
|
||||
|
||||
public void cast(Cast cast, CharacterRecord saved, Instant deadline) {
|
||||
public void cast(Cast cast, CharacterRecord saved, Instant deadline, AbilityDefinition definition) {
|
||||
if (cast == null) return;
|
||||
emit(cast, "ability.cast", "accepted", Map.of("cooldown_until", deadline,
|
||||
"after_revision", saved.revision().toString(), "stage", "cooldown_committed"));
|
||||
try {
|
||||
var settings = new LinkedHashMap<String, Object>();
|
||||
definition.parameters().forEach((key, value) -> settings.put(key, value.toPlainString()));
|
||||
var data = new LinkedHashMap<String, Object>();
|
||||
data.put("cooldown_until", deadline);
|
||||
data.put("after_revision", saved.revision().toString());
|
||||
data.put("stage", "cooldown_committed");
|
||||
data.put("activation", definition.activation().name().toLowerCase(java.util.Locale.ROOT));
|
||||
data.put("captured_settings", settings);
|
||||
data.put("settings_scope", "definition_at_request");
|
||||
for (Stat stat : definition.requirements().keySet())
|
||||
data.put("captured_" + stat.name().toLowerCase(java.util.Locale.ROOT), saved.stats().get(stat).toString());
|
||||
emit(cast, "ability.cast", "accepted", data);
|
||||
} catch (RuntimeException ignored) { /* Capturing metadata cannot refuse a saved cast. */ }
|
||||
}
|
||||
|
||||
public void resolved(Cast cast, String outcome, String reason) {
|
||||
resolved(cast, outcome, reason, Map.of());
|
||||
}
|
||||
|
||||
public void resolved(Cast cast, String outcome, String reason, Map<String, Object> evidence) {
|
||||
if (cast == null) return;
|
||||
emit(cast, "ability.resolved", outcome, Map.of("reason", reason));
|
||||
var data = new LinkedHashMap<String, Object>(evidence);
|
||||
data.put("reason", reason);
|
||||
emit(cast, "ability.resolved", outcome, data);
|
||||
}
|
||||
|
||||
public UUID effectApplied(Cast cast, String effectId, Entity target, Map<String, Object> evidence) {
|
||||
@@ -78,6 +97,27 @@ public final class AbilityCastEventBridge {
|
||||
return application;
|
||||
}
|
||||
|
||||
public void acceptedDamage(Cast cast, Entity target, double requested, double actual,
|
||||
Entity direct, Map<String, Object> evidence) {
|
||||
if (cast == null || !Double.isFinite(requested) || !Double.isFinite(actual) || actual <= 0) return;
|
||||
try {
|
||||
var data = new LinkedHashMap<String, Object>(evidence);
|
||||
var application = UUID.randomUUID();
|
||||
data.put("application_id", application.toString());
|
||||
data.put("causation_id", cast.actionId().toString());
|
||||
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());
|
||||
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());
|
||||
emit(cast, "combat.damage", "accepted", data, application);
|
||||
} catch (RuntimeException ignored) { /* Native damage remains authoritative even when auditing fails. */ }
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import org.bukkit.command.BlockCommandSender;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/** Authorized ability-setting reads and acknowledged updates; no write authority. */
|
||||
public final class AbilityConfigEventBridge {
|
||||
public record Actor(String kind, UUID playerId, String playerName) { }
|
||||
|
||||
private final Consumer<EventInput> sink;
|
||||
private final Clock clock;
|
||||
|
||||
public AbilityConfigEventBridge(Consumer<EventInput> sink, Clock clock) {
|
||||
this.sink = Objects.requireNonNull(sink);
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
}
|
||||
|
||||
/** Capture account identity on the command thread, never on the file worker. */
|
||||
public Actor capture(CommandSender sender) {
|
||||
try {
|
||||
if (sender instanceof Player player) return new Actor("player", player.getUniqueId(), player.getName());
|
||||
return new Actor(sender instanceof BlockCommandSender ? "command_block" : "console", null, null);
|
||||
} catch (RuntimeException ignored) {
|
||||
return null; // Omit uncertain identity, never refuse the authorized action.
|
||||
}
|
||||
}
|
||||
|
||||
public void read(Actor actor, String abilityId, String key, String value) {
|
||||
emit("config.read", actor, abilityId, key, null, value);
|
||||
}
|
||||
|
||||
public void changed(Actor actor, String abilityId, String key, String before, String after) {
|
||||
emit("config.changed", actor, abilityId, key, before, after);
|
||||
}
|
||||
|
||||
private void emit(String type, Actor actor, String abilityId, String key, String before, String after) {
|
||||
try {
|
||||
var data = new LinkedHashMap<String, Object>();
|
||||
data.put("actor_kind", actor.kind());
|
||||
data.put("ability_id", abilityId);
|
||||
data.put("key", key);
|
||||
if (before != null) data.put("before", before);
|
||||
data.put("after", after);
|
||||
data.put("application", "future_casts_only");
|
||||
data.put("outcome", "acknowledged");
|
||||
var action = UUID.randomUUID();
|
||||
var id = UUID.nameUUIDFromBytes(("fantasystick:ability-config:" + action + ":" + type)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
var context = actor.playerId() == null ? null
|
||||
: new EventInput.Context(actor.playerId(), actor.playerName(), null, null, null, null);
|
||||
sink.accept(new EventInput(id, action, clock.instant(), type, context, data));
|
||||
} catch (RuntimeException ignored) { /* File logging cannot change an authorized configuration action. */ }
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,12 @@ public final class EventCaptureListener implements Listener {
|
||||
private final EquipmentSessions sessions;
|
||||
private final Clock clock;
|
||||
private final Map<Object, Captured> captured = java.util.Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private java.util.function.Predicate<org.bukkit.event.entity.EntityDamageEvent> ownedDamage = ignored -> false;
|
||||
|
||||
/** Feature owners replace the generic incoming view with one richer application record. */
|
||||
public void excludeOwnedDamage(java.util.function.Predicate<org.bukkit.event.entity.EntityDamageEvent> owner) {
|
||||
ownedDamage = ownedDamage.or(Objects.requireNonNull(owner));
|
||||
}
|
||||
|
||||
public EventCaptureListener(LocalEventLog events, EquipmentSessions sessions, Clock clock) {
|
||||
this.events = Objects.requireNonNull(events);
|
||||
@@ -102,6 +108,8 @@ public final class EventCaptureListener implements Listener {
|
||||
public void acceptedDamage(org.bukkit.event.entity.EntityDamageEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player) || !Double.isFinite(event.getFinalDamage())
|
||||
|| event.getFinalDamage() <= 0) return;
|
||||
try { if (ownedDamage.test(event)) return; }
|
||||
catch (RuntimeException ignored) { /* Observation failure cannot change native damage. */ }
|
||||
var snapshot = context(player).orElse(null);
|
||||
if (snapshot == null) return;
|
||||
var details = new LinkedHashMap<String, Object>();
|
||||
|
||||
@@ -337,7 +337,11 @@ public final class LocalEventLog implements AutoCloseable {
|
||||
data.put("session_id", context.sessionId().toString());
|
||||
}
|
||||
if (context.location() != null) data.put("location", location(context.location()));
|
||||
} else data.put("actor_kind", "system");
|
||||
} else {
|
||||
var claimed = data.get("actor_kind");
|
||||
data.put("actor_kind", "console".equals(claimed) || "command_block".equals(claimed)
|
||||
? claimed : "system");
|
||||
}
|
||||
var envelope = new LinkedHashMap<String, Object>();
|
||||
envelope.put("specversion", "1.0");
|
||||
envelope.put("id", input.id().toString());
|
||||
|
||||
@@ -27,6 +27,7 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
private volatile boolean closed;
|
||||
private games.dmg.fantasystick.persistence.AdventureSettings settings;
|
||||
private games.dmg.fantasystick.persistence.FireballSettings fireballSettings;
|
||||
private games.dmg.fantasystick.events.AbilityConfigEventBridge abilityConfigEvents;
|
||||
private games.dmg.fantasystick.ability.Fireball fireball;
|
||||
private games.dmg.fantasystick.persistence.FrostShardSettings frostSettings;
|
||||
private games.dmg.fantasystick.ability.FrostShard frostShard;
|
||||
@@ -338,6 +339,7 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
public void attachGearSettings(games.dmg.fantasystick.persistence.GearSettings settings){if(closed||gearSettings!=null)throw new IllegalStateException("Gear settings already attached or command closed");gearSettings=Objects.requireNonNull(settings);}
|
||||
public void attachCoinSettings(games.dmg.fantasystick.persistence.CoinSettings settings){if(closed||coinSettings!=null)throw new IllegalStateException("Coin settings already attached or command closed");coinSettings=Objects.requireNonNull(settings);}
|
||||
public void attachCoinEvents(games.dmg.fantasystick.events.CoinEventBridge events){if(closed||coinEvents!=null)throw new IllegalStateException("Coin events already attached or command closed");coinEvents=Objects.requireNonNull(events);}
|
||||
public void attachAbilityConfigEvents(games.dmg.fantasystick.events.AbilityConfigEventBridge events){if(closed||abilityConfigEvents!=null)throw new IllegalStateException("Ability config events already attached or command closed");abilityConfigEvents=Objects.requireNonNull(events);}
|
||||
public void attachHardMobXpSettings(games.dmg.fantasystick.persistence.HardMobXpSettings settings,
|
||||
games.dmg.fantasystick.party.CombatExperience experience) {
|
||||
if (closed || hardMobXpSettings != null) throw new IllegalStateException("Hard-mob XP settings already attached or command closed");
|
||||
@@ -1045,8 +1047,11 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
|
||||
private void fireballConfiguration(CommandSender sender, String[] args) {
|
||||
String key = args[1];
|
||||
var actor = abilityConfigEvents == null ? null : abilityConfigEvents.capture(sender);
|
||||
String before = fireballSettings.value(key);
|
||||
if (args.length == 2) {
|
||||
sender.sendMessage(key + "=" + fireballSettings.value(key) + " (applies to future Fireball casts only)"); return;
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.read(actor, "fireball", key, before);
|
||||
sender.sendMessage(key + "=" + before + " (applies to future Fireball casts only)"); return;
|
||||
}
|
||||
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.FireballSettings.Update> operation;
|
||||
try { operation = fireballSettings.set(key, args[2]); }
|
||||
@@ -1060,6 +1065,7 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
if (closed) return;
|
||||
if (failure == null && fireballSettings.publish(saved)) {
|
||||
fireball.configure(fireballSettings.settings()); abilities.replace(fireball.definition());
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.changed(actor, "fireball", key, before, saved.value());
|
||||
}
|
||||
if (!sender.hasPermission(PERMISSION)
|
||||
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
|
||||
|
||||
@@ -128,6 +128,69 @@ class FireballNativeTest {
|
||||
assertEquals(1, launches.get(), "Exit/resume and control reconstruction preserve the same Fireball deadline");
|
||||
assertEquals(deadline, storage.find(a.core.record.id()).get().orElseThrow().cooldowns().get("fireball"));
|
||||
}
|
||||
var events = events(data);
|
||||
for (var suffix : java.util.List.of("ability.learned", "ability.assigned", "ability.cast",
|
||||
"ability.resolved", "combat.damage"))
|
||||
assertEquals(1, events.stream().filter(event -> event.get("type").getAsString().endsWith(suffix)
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("fireball")).count(), suffix);
|
||||
var castEvent = events.stream().filter(event -> event.get("type").getAsString().endsWith("ability.cast")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("fireball")).findFirst().orElseThrow();
|
||||
var resolution = events.stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("fireball")).findFirst().orElseThrow();
|
||||
assertEquals(castEvent.getAsJsonObject("data").get("action_id").getAsString(),
|
||||
resolution.getAsJsonObject("data").get("action_id").getAsString());
|
||||
assertEquals("hit", resolution.getAsJsonObject("data").get("outcome").getAsString());
|
||||
assertEquals("12", castEvent.getAsJsonObject("data").get("captured_int").getAsString());
|
||||
assertEquals("6", castEvent.getAsJsonObject("data").getAsJsonObject("captured_settings")
|
||||
.get("base-damage").getAsString());
|
||||
assertEquals("definition_at_request", castEvent.getAsJsonObject("data").get("settings_scope").getAsString());
|
||||
assertEquals("6", resolution.getAsJsonObject("data").getAsJsonObject("effective_settings")
|
||||
.get("base-damage").getAsString());
|
||||
assertTrue(resolution.getAsJsonObject("data").has("direct_entity_id"));
|
||||
assertTrue(resolution.getAsJsonObject("data").has("target_location"));
|
||||
var damage = events.stream().filter(event -> event.get("type").getAsString().endsWith("combat.damage")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("fireball")).findFirst().orElseThrow();
|
||||
assertEquals("7.0", damage.getAsJsonObject("data").get("actual_amount").getAsString());
|
||||
assertEquals(castEvent.getAsJsonObject("data").get("action_id").getAsString(),
|
||||
damage.getAsJsonObject("data").get("action_id").getAsString());
|
||||
}
|
||||
|
||||
@Test void acceptedFantasyPlayerHitHasOneOwnedDamageRecordNotAnIncomingDuplicate() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var b = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
withFireball(data, a, 12);
|
||||
f.players.put(a.core.id, a.player); f.players.put(b.core.id, b.player);
|
||||
f.activate(a); f.activate(b);
|
||||
var worldId = UUID.randomUUID();
|
||||
org.mockito.Mockito.when(a.world.getUID()).thenReturn(worldId);
|
||||
b.core.location.setWorld(a.world); b.core.location.setX(10); b.core.location.setY(65); b.core.location.setZ(0);
|
||||
org.mockito.Mockito.doReturn(org.bukkit.entity.EntityType.PLAYER).when(b.player).getType();
|
||||
var arrow = projectile(a, new org.bukkit.Location(a.world, 10, 65.6, 0));
|
||||
prepareLaunches(f, a, arrow);
|
||||
var hits = new java.util.ArrayList<org.bukkit.event.entity.EntityDamageByEntityEvent>();
|
||||
org.mockito.Mockito.doAnswer(call -> {
|
||||
var event = new org.bukkit.event.entity.EntityDamageByEntityEvent(arrow, b.player,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.PROJECTILE,
|
||||
(org.bukkit.damage.DamageSource) call.getArgument(1), (Double) call.getArgument(0));
|
||||
f.dispatcher.accept(event); hits.add(event); return null;
|
||||
}).when(b.player).damage(org.mockito.ArgumentMatchers.anyDouble(),
|
||||
org.mockito.ArgumentMatchers.any(org.bukkit.damage.DamageSource.class));
|
||||
var eye = new org.bukkit.Location(a.world, 0, 65.6, 0);
|
||||
eye.setDirection(new org.bukkit.util.Vector(1, 0, 0));
|
||||
org.mockito.Mockito.doReturn(eye).when(a.player).getEyeLocation();
|
||||
cast(f, a);
|
||||
f.dispatcher.accept(new org.bukkit.event.entity.ProjectileHitEvent(arrow, b.player));
|
||||
assertEquals(1, hits.size()); assertFalse(hits.getFirst().isCancelled());
|
||||
}
|
||||
var damage = events(data).stream().filter(event -> event.get("type").getAsString().endsWith("combat.damage")
|
||||
&& event.getAsJsonObject("data").has("target_player_id")).toList();
|
||||
assertEquals(1, damage.size(), "The native incoming observer must yield to the ability owner");
|
||||
assertEquals("fireball", damage.getFirst().getAsJsonObject("data").get("ability_id").getAsString());
|
||||
assertEquals("7.0", damage.getFirst().getAsJsonObject("data").get("actual_amount").getAsString());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -296,6 +359,14 @@ class FireballNativeTest {
|
||||
assertTrue(org.mockito.Mockito.mockingDetails(a.world).getInvocations().stream()
|
||||
.noneMatch(invocation -> invocation.getMethod().getName().equals("createExplosion")));
|
||||
}
|
||||
var observed = events(data);
|
||||
assertEquals(2, observed.stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("fireball")
|
||||
&& event.getAsJsonObject("data").get("outcome").getAsString().equals("cancelled")).count());
|
||||
assertEquals(0, 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("fireball")).count());
|
||||
}
|
||||
|
||||
@Test void rangeLifetimeAndSessionEndEachRemoveOwnedProjectileWithoutLateDamage() throws Exception {
|
||||
@@ -333,6 +404,16 @@ class FireballNativeTest {
|
||||
((games.dmg.fantasystick.ability.AbilityEngine) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "abilityEngine")).sweep();
|
||||
org.mockito.Mockito.verify(exiting, org.mockito.Mockito.atLeastOnce()).remove();
|
||||
}
|
||||
var outcomes = events(data).stream().filter(event -> event.get("type").getAsString().endsWith("ability.resolved")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("fireball"))
|
||||
.map(event -> event.getAsJsonObject("data").get("outcome").getAsString()).toList();
|
||||
assertEquals(4, outcomes.size(), outcomes.toString());
|
||||
assertEquals(2, outcomes.stream().filter("miss"::equals).count());
|
||||
assertTrue(outcomes.contains("expired")); assertTrue(outcomes.contains("interrupted"));
|
||||
assertEquals(0, 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("fireball")).count());
|
||||
}
|
||||
|
||||
@Test void validatedConfiguredBalanceReachesTheRegisteredDefinition() throws Exception {
|
||||
@@ -351,6 +432,12 @@ class FireballNativeTest {
|
||||
assertEquals(new java.math.BigDecimal("4"), definition.parameters().get("projectile-speed"));
|
||||
assertTrue(String.join(" ", definition.lore()).contains("Deals 9"));
|
||||
var admin = new EquipmentAdminCommandNativeTest.Sender(); admin.allowed = true;
|
||||
f.command.execute(admin.port, "fstickadmin",
|
||||
new String[]{"config", "fireball-projectile-speed-blocks-per-tick"});
|
||||
admin.allowed = false;
|
||||
f.command.execute(admin.port, "fstickadmin",
|
||||
new String[]{"config", "fireball-projectile-speed-blocks-per-tick"});
|
||||
admin.allowed = true;
|
||||
assertEquals(java.util.List.of("fireball-projectile-speed-blocks-per-tick"), f.command.tabComplete(admin.port,
|
||||
"fantasystick", new String[]{"config", "fireball-projectile-s"}));
|
||||
assertEquals(java.util.List.of("3", "4"), f.command.tabComplete(admin.port, "fantasystick",
|
||||
@@ -362,7 +449,28 @@ class FireballNativeTest {
|
||||
assertEquals(new java.math.BigDecimal("4.5"), updated.parameters().get("projectile-speed"));
|
||||
assertTrue(java.nio.file.Files.readString(data.resolve("config.yml"))
|
||||
.contains("fireball-projectile-speed-blocks-per-tick: '4.5'"));
|
||||
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", "fireball-projectile-speed-blocks-per-tick", "5"});
|
||||
var failed = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
assertNotNull(failed); failed.run();
|
||||
}
|
||||
assertEquals("4.5", ((games.dmg.fantasystick.persistence.FireballSettings)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "fireballSettings"))
|
||||
.value("fireball-projectile-speed-blocks-per-tick"));
|
||||
assertTrue(admin.messages.getLast().contains("save failed"));
|
||||
}
|
||||
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("fireball-projectile-speed-blocks-per-tick")).toList();
|
||||
assertEquals(2, config.size());
|
||||
assertTrue(config.getFirst().get("type").getAsString().endsWith("config.read"));
|
||||
assertEquals("4", config.getLast().getAsJsonObject("data").get("before").getAsString());
|
||||
assertEquals("4.5", config.getLast().getAsJsonObject("data").get("after").getAsString());
|
||||
assertEquals("console", config.getLast().getAsJsonObject("data").get("actor_kind").getAsString());
|
||||
}
|
||||
|
||||
@Test void liveSpeedUpdateAppliesOnlyToFutureCastsAndRetainsActiveArrow() throws Exception {
|
||||
@@ -473,6 +581,12 @@ class FireballNativeTest {
|
||||
var active = new games.dmg.fantasystick.session.EquipmentSessions.Active(java.util.UUID.randomUUID(),
|
||||
java.util.UUID.randomUUID(), record.id());
|
||||
var player = org.mockito.Mockito.mock(org.bukkit.entity.Player.class);
|
||||
var world = org.mockito.Mockito.mock(org.bukkit.World.class);
|
||||
org.mockito.Mockito.when(world.getUID()).thenReturn(UUID.randomUUID());
|
||||
org.mockito.Mockito.when(world.getName()).thenReturn("world");
|
||||
org.mockito.Mockito.when(player.getUniqueId()).thenReturn(active.player());
|
||||
org.mockito.Mockito.when(player.getName()).thenReturn("Caster");
|
||||
org.mockito.Mockito.when(player.getLocation()).thenReturn(new org.bukkit.Location(world, 1, 64, 2));
|
||||
org.mockito.Mockito.when(sessions.current(active)).thenReturn(true);
|
||||
org.mockito.Mockito.when(sessions.currentRecord(active)).thenReturn(java.util.Optional.of(record));
|
||||
var failed = new java.util.concurrent.CompletableFuture<games.dmg.fantasystick.persistence.CharacterStorage.AbilityMutation>();
|
||||
@@ -483,6 +597,10 @@ class FireballNativeTest {
|
||||
var failures = new java.util.ArrayList<games.dmg.fantasystick.ability.AbilityEngine.Failure>();
|
||||
var engine = new games.dmg.fantasystick.ability.AbilityEngine(registry, storage, sessions, parties,
|
||||
java.time.Clock.systemUTC(), Runnable::run, (delay, action) -> () -> {}, ignored -> {}, failures::add);
|
||||
var audit = new java.util.ArrayList<games.dmg.fantasystick.events.EventInput>();
|
||||
engine.attachCastEvents(new games.dmg.fantasystick.events.AbilityCastEventBridge(audit::add,
|
||||
java.time.Clock.systemUTC()));
|
||||
engine.enableCastEventsFor("fireball");
|
||||
engine.accept(new games.dmg.fantasystick.session.ControlInput(player, active, 2, "fireball",
|
||||
games.dmg.fantasystick.session.ControlInput.Kind.ABILITY,
|
||||
games.dmg.fantasystick.session.ControlInput.Gesture.USE, null, 0, 0, () -> true));
|
||||
@@ -491,6 +609,10 @@ class FireballNativeTest {
|
||||
org.mockito.Mockito.verify(sessions, org.mockito.Mockito.never()).acknowledgeProgress(
|
||||
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
assertEquals(1, failures.size());
|
||||
assertEquals(java.util.List.of("action.observed", "ability.resolved"),
|
||||
audit.stream().map(games.dmg.fantasystick.events.EventInput::type).toList());
|
||||
assertEquals("failed", audit.getLast().data().get("outcome"));
|
||||
assertEquals(audit.getFirst().actionId(), audit.getLast().actionId());
|
||||
engine.close(); fireball.close(); parties.close();
|
||||
}
|
||||
|
||||
@@ -527,6 +649,16 @@ class FireballNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static java.util.List<com.google.gson.JsonObject> events(Path data) throws Exception {
|
||||
var events = new java.util.ArrayList<com.google.gson.JsonObject>();
|
||||
try (var files = java.nio.file.Files.walk(data.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()) events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
private static void await(java.util.concurrent.atomic.AtomicInteger value, int expected) throws Exception {
|
||||
for (int attempt = 0; attempt < 500 && value.get() < expected; attempt++) Thread.sleep(10);
|
||||
assertEquals(expected, value.get());
|
||||
@@ -602,6 +734,7 @@ class FireballNativeTest {
|
||||
org.bukkit.Location location) {
|
||||
var projectile = org.mockito.Mockito.mock(org.bukkit.entity.Arrow.class);
|
||||
org.mockito.Mockito.when(projectile.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
|
||||
org.mockito.Mockito.when(projectile.getType()).thenReturn(org.bukkit.entity.EntityType.ARROW);
|
||||
org.mockito.Mockito.when(projectile.getShooter()).thenReturn(actor.player);
|
||||
org.mockito.Mockito.when(projectile.getLocation()).thenAnswer(ignored -> location.clone());
|
||||
org.mockito.Mockito.when(projectile.getWorld()).thenReturn(location.getWorld());
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AbilityConfigEventBridgeTest {
|
||||
private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-09-25T04:00:00Z"), ZoneOffset.UTC);
|
||||
|
||||
@Test void authorizedReadAndAcknowledgedChangeHaveDistinctTypedRecords() {
|
||||
var events = new ArrayList<EventInput>();
|
||||
var bridge = new AbilityConfigEventBridge(events::add, CLOCK);
|
||||
var actor = new AbilityConfigEventBridge.Actor("console", null, null);
|
||||
bridge.read(actor, "fireball", "fireball-cooldown-seconds", "1.2");
|
||||
bridge.changed(actor, "fireball", "fireball-cooldown-seconds", "1.2", "2");
|
||||
assertEquals(2, events.size());
|
||||
assertEquals("config.read", events.getFirst().type());
|
||||
assertEquals("config.changed", events.getLast().type());
|
||||
assertNotEquals(events.getFirst().id(), events.getLast().id());
|
||||
assertEquals("fireball", events.getLast().data().get("ability_id"));
|
||||
assertEquals("1.2", events.getLast().data().get("before"));
|
||||
assertEquals("2", events.getLast().data().get("after"));
|
||||
assertEquals("future_casts_only", events.getLast().data().get("application"));
|
||||
assertNull(events.getLast().context(), "Console is not a fictional player or character");
|
||||
}
|
||||
|
||||
@Test void identityCaptureFailureCannotBlockAnAuthorizedReadOrSave() {
|
||||
var bridge = new AbilityConfigEventBridge(event -> fail("No unsupported identity may be emitted"), CLOCK);
|
||||
var player = (org.bukkit.entity.Player) java.lang.reflect.Proxy.newProxyInstance(
|
||||
org.bukkit.entity.Player.class.getClassLoader(), new Class<?>[]{org.bukkit.entity.Player.class},
|
||||
(proxy, method, args) -> {
|
||||
if (method.getName().equals("getUniqueId")) throw new IllegalStateException("identity unavailable");
|
||||
return null;
|
||||
});
|
||||
var actor = assertDoesNotThrow(() -> bridge.capture(player));
|
||||
assertNull(actor);
|
||||
assertDoesNotThrow(() -> bridge.read(actor, "fireball", "fireball-range-blocks", "32"));
|
||||
assertDoesNotThrow(() -> bridge.changed(actor, "fireball", "fireball-range-blocks", "32", "40"));
|
||||
}
|
||||
|
||||
@Test void aBrokenLogSinkCannotRefuseAnAlreadyAuthorizedConfigurationAction() {
|
||||
var bridge = new AbilityConfigEventBridge(event -> { throw new IllegalStateException("writer down"); }, CLOCK);
|
||||
var actor = new AbilityConfigEventBridge.Actor("player", UUID.randomUUID(), "Admin");
|
||||
assertDoesNotThrow(() -> bridge.read(actor, "fireball", "fireball-range-blocks", "32"));
|
||||
assertDoesNotThrow(() -> bridge.changed(actor, "fireball", "fireball-range-blocks", "32", "40"));
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,27 @@ final class LocalEventLogTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void preservesRealNonPlayerAdminActorWithoutAllowingAContextFreePlayerImpersonation() throws Exception {
|
||||
var clock = Clock.fixed(Instant.parse("2026-09-25T05:00:00Z"), ZoneOffset.UTC);
|
||||
try (var events = LocalEventLog.open(data, clock,
|
||||
new LocalEventLog.RuntimeInfo("Season 4", "26.2", "test"), 8, 134_217_728L,
|
||||
message -> fail(message))) {
|
||||
events.ready().toCompletableFuture().get();
|
||||
for (var actor : java.util.List.of("console", "command_block", "player")) {
|
||||
var action = UUID.randomUUID();
|
||||
assertEquals(LocalEventLog.Delivery.EXPORTED,
|
||||
events.emit(new EventInput(action, action, clock.instant(), "config.read", null,
|
||||
Map.of("actor_kind", actor, "key", actor))).toCompletableFuture().get());
|
||||
}
|
||||
}
|
||||
var lines = jsonLines(data.resolve("events"));
|
||||
for (var actor : java.util.List.of("console", "command_block"))
|
||||
assertTrue(java.util.Arrays.stream(lines).anyMatch(line -> line.contains("\"key\":\"" + actor + "\"")
|
||||
&& line.contains("\"actor_kind\":\"" + actor + "\"")));
|
||||
assertTrue(java.util.Arrays.stream(lines).anyMatch(line -> line.contains("\"key\":\"player\"")
|
||||
&& line.contains("\"actor_kind\":\"system\"")));
|
||||
}
|
||||
|
||||
@Test void pendingEvidenceAloneReportsRecoveryWithoutInventingPriorShutdown() throws Exception {
|
||||
var root=data.resolve("events");Files.createDirectories(root.resolve("pending"));var server=UUID.randomUUID();Files.writeString(root.resolve("server-id"),server+"\n");var event=UUID.randomUUID();var exact="{\"specversion\":\"1.0\",\"id\":\""+event+"\",\"source\":\"urn:minecraft-server:"+server+"\",\"type\":\"games.dmg.minecraft.fantasystick.action.observed\",\"time\":\"2026-09-24T20:00:00Z\",\"datacontenttype\":\"application/json\",\"data\":{\"schema_version\":1}}\n";Files.writeString(root.resolve("pending/"+event+".pending"),exact);
|
||||
try(var recovered=LocalEventLog.open(data,Clock.fixed(Instant.parse("2026-09-25T00:00:00Z"),ZoneOffset.UTC),new LocalEventLog.RuntimeInfo("Season 4","26.2","test"),8,134_217_728L,message->fail(message))){assertEquals(LocalEventLog.Delivery.EXPORTED,recovered.ready().toCompletableFuture().get());}
|
||||
|
||||
Reference in new issue
Block a user