This commit is contained in:
1 parent
39881ad6fc
commit
af7c75b6ed
4 files changed
+704
-11
No files matched your search
@@ -752,6 +752,8 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
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);
|
||||
partyAuditEvents = new games.dmg.fantasystick.events.PartyEventBridge(eventLog::emit, Clock.systemUTC());partyManagement.attachEvents(partyAuditEvents);admin.attachPartyEvents(partyAuditEvents);
|
||||
partyCombat.attachProtectionEvents(new games.dmg.fantasystick.events.PartyProtectionEventBridge(
|
||||
eventLog::emit, Clock.systemUTC()));
|
||||
combatRewardEvents = new games.dmg.fantasystick.events.CombatRewardEventBridge(eventLog::emit,Clock.systemUTC());combatExperience.attachEvents(combatRewardEvents);
|
||||
coinEvents = new games.dmg.fantasystick.events.CoinEventBridge(eventLog::emit,Clock.systemUTC());characters.attachCoinEvents(coinEvents);admin.attachCoinEvents(coinEvents);
|
||||
sessionAuditEvents = new games.dmg.fantasystick.events.SessionEventBridge(eventLog::emit,Clock.systemUTC());sessions.attachSessionEvents(sessionAuditEvents);
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import games.dmg.fantasystick.party.CombatSources;
|
||||
import games.dmg.fantasystick.party.PartyRoster;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
|
||||
/** Captures native protection decisions without changing the native damage/event authority. */
|
||||
public final class PartyProtectionEventBridge {
|
||||
public record Action(UUID id, Instant observedAt, EventInput.Context actor, Map<String, Object> data) { }
|
||||
private final Consumer<EventInput> sink;
|
||||
private final Clock clock;
|
||||
|
||||
public PartyProtectionEventBridge(Consumer<EventInput> sink, Clock clock) {
|
||||
this.sink = Objects.requireNonNull(sink);
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
}
|
||||
|
||||
public Action damage(EntityDamageEvent event, CombatSources.Origin origin,
|
||||
CombatSources.Origin recipient, Player target,
|
||||
PartyRoster.Party sourceParty, PartyRoster.Party targetParty, String reason) {
|
||||
try {
|
||||
var id = UUID.randomUUID();
|
||||
var time = clock.instant();
|
||||
Entity causing = event.getDamageSource().getCausingEntity();
|
||||
Entity direct = event.getDamageSource().getDirectEntity();
|
||||
var source = causing instanceof Player player && player.getUniqueId().equals(origin.player()) ? player : null;
|
||||
var context = source == null ? null : new EventInput.Context(source.getUniqueId(), source.getName(),
|
||||
null, null, null, location(source.getLocation()));
|
||||
var data = new LinkedHashMap<String, Object>();
|
||||
data.put("action_kind", "party_damage_protection");
|
||||
if (context == null) data.put("actor_kind", "system");
|
||||
data.put("damage_cause", event.getCause().name().toLowerCase(java.util.Locale.ROOT));
|
||||
data.put("source_kind", causing == null ? "provenance" : causing instanceof Player ? "player"
|
||||
: causing.getType() == null ? "unavailable" : causing.getType().name().toLowerCase(java.util.Locale.ROOT));
|
||||
data.put("source_player_id", origin.player().toString());
|
||||
if (causing != null && causing.getUniqueId() != null)
|
||||
data.put("causing_entity_id", causing.getUniqueId().toString());
|
||||
if (direct != null && direct.getUniqueId() != null)
|
||||
data.put("direct_entity_id", direct.getUniqueId().toString());
|
||||
if (origin.character() != null) data.put("source_character_id", origin.character().toString());
|
||||
if (origin.session() != null) data.put("source_session_id", origin.session().toString());
|
||||
data.put("target_player_id", target.getUniqueId().toString());
|
||||
if (recipient.character() != null) data.put("target_character_id", recipient.character().toString());
|
||||
if (recipient.session() != null) data.put("target_session_id", recipient.session().toString());
|
||||
if (sourceParty != null) data.put("source_party_id", sourceParty.id().toString());
|
||||
if (targetParty != null) data.put("target_party_id", targetParty.id().toString());
|
||||
data.put("provenance_quality", origin.session() == null ? "ordinary_or_unproved"
|
||||
: reason.equals("stale_source") ? "claimed_session_unverified" : "matching_session_id");
|
||||
data.put("provenance_age_status", "unavailable");
|
||||
data.put("requested_amount", event.getDamage());
|
||||
data.put("prior_cancelled", event.isCancelled());
|
||||
data.put("proposed_reason", reason);
|
||||
var sourceLocation = causing == null ? null : location(causing.getLocation());
|
||||
var targetLocation = location(target.getLocation());
|
||||
if (sourceLocation != null) data.put("source_location", coordinates(sourceLocation));
|
||||
else data.put("source_location_status", "unavailable");
|
||||
if (targetLocation != null) data.put("target_location", coordinates(targetLocation));
|
||||
else data.put("target_location_status", "unavailable");
|
||||
var action = new Action(id, time, context, Map.copyOf(data));
|
||||
var observed = new LinkedHashMap<String, Object>(data);
|
||||
observed.put("outcome", "observed");
|
||||
emit(action, "action.observed", "observed", observed);
|
||||
return action;
|
||||
} catch (RuntimeException ignored) { return null; }
|
||||
}
|
||||
|
||||
public Action nativeAttempt(String kind, Entity causing, Player target, CombatSources.Origin origin,
|
||||
CombatSources.Origin recipient, PartyRoster.Party sourceParty,
|
||||
PartyRoster.Party targetParty, String reason, String nativeKind) {
|
||||
try {
|
||||
var id = UUID.randomUUID();
|
||||
var now = clock.instant();
|
||||
var source = causing instanceof Player player && player.getUniqueId().equals(origin.player()) ? player : null;
|
||||
var context = source == null ? null : new EventInput.Context(source.getUniqueId(), source.getName(),
|
||||
null, null, null, location(source.getLocation()));
|
||||
var data = new LinkedHashMap<String, Object>();
|
||||
data.put("action_kind", kind);
|
||||
if (context == null) data.put("actor_kind", "system");
|
||||
data.put("native_kind", nativeKind);
|
||||
data.put("source_player_id", origin.player().toString());
|
||||
if (causing != null && causing.getUniqueId() != null)
|
||||
data.put("direct_entity_id", causing.getUniqueId().toString());
|
||||
if (origin.character() != null) data.put("source_character_id", origin.character().toString());
|
||||
if (origin.session() != null) data.put("source_session_id", origin.session().toString());
|
||||
if (recipient != null) {
|
||||
data.put("target_player_id", recipient.player().toString());
|
||||
if (recipient.character() != null) data.put("target_character_id", recipient.character().toString());
|
||||
if (recipient.session() != null) data.put("target_session_id", recipient.session().toString());
|
||||
} else data.put("target_status", "not_single_proved_player");
|
||||
if (sourceParty != null) data.put("source_party_id", sourceParty.id().toString());
|
||||
if (targetParty != null) data.put("target_party_id", targetParty.id().toString());
|
||||
data.put("provenance_quality", origin.session() == null ? "ordinary_or_unproved"
|
||||
: reason.equals("stale_source") ? "claimed_session_unverified" : "matching_session_id");
|
||||
data.put("provenance_age_status", "unavailable");
|
||||
data.put("requested_amount_status", "not_applicable");
|
||||
data.put("prior_cancelled", false);
|
||||
var sourceLocation = causing == null ? null : location(causing.getLocation());
|
||||
var targetLocation = target == null ? null : location(target.getLocation());
|
||||
if (sourceLocation != null) data.put("source_location", coordinates(sourceLocation));
|
||||
else data.put("source_location_status", "unavailable");
|
||||
if (targetLocation != null) data.put("target_location", coordinates(targetLocation));
|
||||
else data.put("target_location_status", "unavailable");
|
||||
var action = new Action(id, now, context, Map.copyOf(data));
|
||||
var observed = new LinkedHashMap<String, Object>(data);
|
||||
observed.put("outcome", "observed");
|
||||
emit(action, "action.observed", "observed", observed);
|
||||
return action;
|
||||
} catch (RuntimeException ignored) { return null; }
|
||||
}
|
||||
|
||||
public void resolved(Action action, String outcome, String reason, boolean cancelled, Double finalAmount) {
|
||||
if (action == null) return;
|
||||
var values = new LinkedHashMap<String, Object>(action.data());
|
||||
values.put("outcome", outcome);
|
||||
values.put("reason", reason);
|
||||
values.put("final_cancelled", cancelled);
|
||||
if (finalAmount != null) values.put("final_amount", finalAmount);
|
||||
else values.put("final_amount_status", "not_applicable");
|
||||
emit(action, "action.resolved", "resolved", values);
|
||||
}
|
||||
|
||||
private EventInput.Location location(Location value) {
|
||||
return value == null || value.getWorld() == null ? null
|
||||
: new EventInput.Location(value.getWorld().getUID(), value.getWorld().getName(), value.getX(),
|
||||
value.getY(), value.getZ(), clock.instant(), "exact");
|
||||
}
|
||||
|
||||
private Map<String, Object> coordinates(EventInput.Location value) {
|
||||
return Map.of("world_id", value.worldId().toString(), "world_name", value.worldName(),
|
||||
"x", value.x(), "y", value.y(), "z", value.z(), "captured_at", value.capturedAt().toString());
|
||||
}
|
||||
|
||||
private void emit(Action action, String type, String stage, Map<String, Object> data) {
|
||||
try { sink.accept(new EventInput(UUID.nameUUIDFromBytes(("fantasystick:party-protection:" + action.id()
|
||||
+ ':' + stage).getBytes(StandardCharsets.UTF_8)), action.id(),
|
||||
stage.equals("observed") ? action.observedAt() : clock.instant(), type, action.actor(), data)); }
|
||||
catch (RuntimeException ignored) { }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import java.util.Objects;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import java.util.WeakHashMap;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
@@ -17,12 +18,24 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
private boolean closed;
|
||||
private final PartyRoster parties;
|
||||
private final CombatSources sources;
|
||||
private games.dmg.fantasystick.events.PartyProtectionEventBridge protectionEvents;
|
||||
private final WeakHashMap<EntityDamageEvent, DamageAudit> damageAudits = new WeakHashMap<>();
|
||||
private final WeakHashMap<org.bukkit.event.Event, NativeAudit> nativeAudits = new WeakHashMap<>();
|
||||
private record NativeAudit(games.dmg.fantasystick.events.PartyProtectionEventBridge.Action action,
|
||||
String reason) { }
|
||||
private record DamageAudit(games.dmg.fantasystick.events.PartyProtectionEventBridge.Action action,
|
||||
String decision) { }
|
||||
|
||||
public PartyCombat(PartyRoster parties, EquipmentSessions sessions) {
|
||||
this.parties = Objects.requireNonNull(parties);
|
||||
this.sources = new CombatSources(sessions);
|
||||
}
|
||||
|
||||
public void attachProtectionEvents(games.dmg.fantasystick.events.PartyProtectionEventBridge events) {
|
||||
if (protectionEvents != null || closed) throw new IllegalStateException("Party protection events already attached");
|
||||
protectionEvents = Objects.requireNonNull(events);
|
||||
}
|
||||
|
||||
public void install(org.bukkit.plugin.Plugin plugin) {
|
||||
cleanup = plugin.getServer().getScheduler().runTaskTimer(plugin, () -> {
|
||||
if (!closed) for (var player : plugin.getServer().getOnlinePlayers()) OngoingDamage.sweep(player);
|
||||
@@ -31,6 +44,8 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
|
||||
@Override public void close() {
|
||||
closed = true;
|
||||
damageAudits.clear();
|
||||
nativeAudits.clear();
|
||||
if (cleanup != null) cleanup.cancel();
|
||||
}
|
||||
|
||||
@@ -40,8 +55,21 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void impact(org.bukkit.event.entity.ProjectileHitEvent event) {
|
||||
var origin = sources.read(event.getEntity());
|
||||
if (origin != null && (!sources.current(origin) || !(event.getEntity() instanceof org.bukkit.entity.ThrownPotion)
|
||||
&& event.getHitEntity() instanceof Player target && blocked(origin, target))) event.setCancelled(true);
|
||||
if (origin == null) return;
|
||||
var target = event.getHitEntity() instanceof Player player ? player : null;
|
||||
var reason = !sources.current(origin) ? "stale_source"
|
||||
: target != null && !(event.getEntity() instanceof org.bukkit.entity.ThrownPotion)
|
||||
? protectionReason(origin, target) : null;
|
||||
if (reason != null) {
|
||||
captureNative(event, "party_projectile_impact_protection", event.getEntity(), target, origin,
|
||||
reason, "projectile_impact");
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void resolvedImpact(org.bukkit.event.entity.ProjectileHitEvent event) {
|
||||
resolveNative(event, event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
@@ -50,13 +78,33 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
// Revoke stale continuation here. Valid healing and unrelated recipients are untouched;
|
||||
// actual harmful applications and instantaneous damage have their own guards below.
|
||||
var origin = sources.read(event.getEntity());
|
||||
if (origin != null && !sources.current(origin)) event.setCancelled(true);
|
||||
if (origin != null && !sources.current(origin)) {
|
||||
var recipients = event.getAffectedEntities().stream().filter(Player.class::isInstance)
|
||||
.map(Player.class::cast).toList();
|
||||
captureNative(event, "party_splash_protection", event.getEntity(),
|
||||
recipients.size() == 1 ? recipients.getFirst() : null, origin, "stale_source", "potion_splash");
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void resolvedSplash(org.bukkit.event.entity.PotionSplashEvent event) {
|
||||
resolveNative(event, event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void staleLingering(LingeringPotionSplashEvent event) {
|
||||
var origin = sources.read(event.getEntity());
|
||||
if (origin != null && !sources.current(origin)) event.setCancelled(true);
|
||||
if (origin != null && !sources.current(origin)) {
|
||||
captureNative(event, "party_lingering_protection", event.getEntity(), null,
|
||||
origin, "stale_source", "lingering_splash");
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void resolvedLingering(LingeringPotionSplashEvent event) {
|
||||
resolveNative(event, event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
@@ -64,12 +112,49 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
sources.store(event.getAreaEffectCloud(), sources.read(event.getEntity()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void observeDamage(EntityDamageEvent event) {
|
||||
if (protectionEvents == null || !(event.getEntity() instanceof Player target)) return;
|
||||
// OngoingDamage.read reconciles native PDC. Only the original HIGHEST guard may do so;
|
||||
// the LOWEST audit must not change its timing or mutate an earlier protection's state.
|
||||
if (event.getCause() == EntityDamageEvent.DamageCause.POISON
|
||||
|| event.getCause() == EntityDamageEvent.DamageCause.WITHER
|
||||
|| event.getCause() == EntityDamageEvent.DamageCause.FIRE_TICK) return;
|
||||
var origin = sources.source(event);
|
||||
if (origin == null) return;
|
||||
var ongoing = OngoingDamage.read(target, event.getCause());
|
||||
String reason = ongoing != null && !sources.current(ongoing.target()) ? "stale_target"
|
||||
: protectionReason(origin, target);
|
||||
if (reason == null) return;
|
||||
captureDamage(event, target, origin, reason);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void resolvedDamage(EntityDamageEvent event) {
|
||||
if (protectionEvents == null) return;
|
||||
var audit = damageAudits.remove(event);
|
||||
if (audit == null) return;
|
||||
boolean prior = Boolean.TRUE.equals(audit.action().data().get("prior_cancelled"));
|
||||
String reason = prior && event.isCancelled() ? "prior_cancelled"
|
||||
: !event.isCancelled() && audit.decision() != null ? "protection_overridden"
|
||||
: event.isCancelled() && audit.decision() != null ? audit.decision()
|
||||
: event.isCancelled() ? "external_cancelled" : "protection_not_applied";
|
||||
String outcome = !event.isCancelled() ? "accepted" : prior || audit.decision() == null
|
||||
? "cancelled" : "refused";
|
||||
protectionEvents.resolved(audit.action(), outcome, reason, event.isCancelled(),
|
||||
event.isCancelled() ? 0 : event.getFinalDamage());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void damage(EntityDamageEvent event) {
|
||||
if (games.dmg.fantasystick.ability.GuardianBond.redirectedDamage()) return;
|
||||
if (event.getEntity() instanceof Player target) {
|
||||
var ongoing = OngoingDamage.read(target, event.getCause());
|
||||
if (ongoing != null && (!sources.current(ongoing.target()) || blocked(ongoing.source(), target))) {
|
||||
var reason = !sources.current(ongoing.target()) ? "stale_target"
|
||||
: protectionReason(ongoing.source(), target);
|
||||
captureDamage(event, target, ongoing.source(), reason);
|
||||
markDamageDecision(event, reason);
|
||||
event.setCancelled(true); return;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +162,14 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
if (origin == null && event instanceof EntityDamageByEntityEvent entity) origin = sources.read(entity.getDamager());
|
||||
if (origin == null) origin = sources.read(event.getDamageSource().getCausingEntity());
|
||||
if (origin != null && (!sources.current(origin)
|
||||
|| event.getEntity() instanceof Player target && blocked(origin, target))) event.setCancelled(true);
|
||||
|| event.getEntity() instanceof Player target && blocked(origin, target))) {
|
||||
if (event.getEntity() instanceof Player target) {
|
||||
var reason = protectionReason(origin, target);
|
||||
captureDamage(event, target, origin, reason);
|
||||
markDamageDecision(event, reason);
|
||||
}
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
@@ -85,7 +177,16 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
if (!(event.getEntity() instanceof Player target) || event.getNewEffect() == null
|
||||
|| OngoingDamage.cause(event.getModifiedType()) == null) return;
|
||||
var origin = sources.read(event.getSource());
|
||||
if (origin != null && blocked(origin, target)) event.setCancelled(true);
|
||||
if (origin != null && blocked(origin, target)) {
|
||||
captureNative(event, "party_effect_application_protection", event.getSource(), target,
|
||||
origin, protectionReason(origin, target), event.getModifiedType().getKey().getKey());
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void resolvedPotion(org.bukkit.event.entity.EntityPotionEffectEvent event) {
|
||||
resolveNative(event, event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
@@ -105,7 +206,16 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
public void combust(org.bukkit.event.entity.EntityCombustByEntityEvent event) {
|
||||
if (!(event.getEntity() instanceof Player target)) return;
|
||||
var origin = sources.read(event.getCombuster());
|
||||
if (origin != null && blocked(origin, target)) event.setCancelled(true);
|
||||
if (origin != null && blocked(origin, target)) {
|
||||
captureNative(event, "party_ignite_protection", event.getCombuster(), target,
|
||||
origin, protectionReason(origin, target), "combust");
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void resolvedCombust(org.bukkit.event.entity.EntityCombustByEntityEvent event) {
|
||||
resolveNative(event, event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
@@ -118,11 +228,49 @@ public final class PartyCombat implements Listener, AutoCloseable {
|
||||
: new OngoingDamage.Application(origin, sources.player(target.getUniqueId())));
|
||||
}
|
||||
|
||||
private boolean blocked(CombatSources.Origin origin, Player target) {
|
||||
private void captureNative(org.bukkit.event.Event event, String kind, org.bukkit.entity.Entity causing,
|
||||
Player target, CombatSources.Origin origin, String reason, String nativeKind) {
|
||||
if (protectionEvents == null || reason == null) return;
|
||||
var recipient = target == null ? null : sources.player(target.getUniqueId());
|
||||
var action = protectionEvents.nativeAttempt(kind, causing, target, origin, recipient,
|
||||
parties.partyOf(origin.player()).orElse(null),
|
||||
recipient == null ? null : parties.partyOf(recipient.player()).orElse(null),
|
||||
reason, nativeKind);
|
||||
if (action != null) nativeAudits.put(event, new NativeAudit(action, reason));
|
||||
}
|
||||
|
||||
private void resolveNative(org.bukkit.event.Event event, org.bukkit.event.Cancellable cancellable) {
|
||||
if (protectionEvents == null) return;
|
||||
var audit = nativeAudits.remove(event);
|
||||
if (audit != null) protectionEvents.resolved(audit.action(), cancellable.isCancelled() ? "refused" : "accepted",
|
||||
cancellable.isCancelled() ? audit.reason() : "external_uncancelled", cancellable.isCancelled(), null);
|
||||
}
|
||||
|
||||
private void captureDamage(EntityDamageEvent event, Player target, CombatSources.Origin origin, String reason) {
|
||||
if (protectionEvents == null || damageAudits.containsKey(event) || reason == null) return;
|
||||
var recipient = sources.player(target.getUniqueId());
|
||||
var action = protectionEvents.damage(event, origin, recipient, target,
|
||||
parties.partyOf(origin.player()).orElse(null), parties.partyOf(recipient.player()).orElse(null), reason);
|
||||
if (action != null) damageAudits.put(event, new DamageAudit(action, null));
|
||||
}
|
||||
|
||||
private void markDamageDecision(EntityDamageEvent event, String reason) {
|
||||
var audit = damageAudits.get(event);
|
||||
if (audit != null && reason != null) damageAudits.put(event, new DamageAudit(audit.action(), reason));
|
||||
}
|
||||
|
||||
private String protectionReason(CombatSources.Origin origin, Player target) {
|
||||
if (!sources.current(origin)) return "stale_source";
|
||||
var recipient = sources.player(target.getUniqueId());
|
||||
boolean fantasy = origin.session() != null;
|
||||
return !sources.current(origin) || fantasy != (recipient.session() != null) || fantasy
|
||||
&& !origin.player().equals(recipient.player())
|
||||
&& parties.partyOf(origin.player()).filter(group -> group.members().contains(recipient.player())).isPresent();
|
||||
if (fantasy != (recipient.session() != null)) return "mixed_mode";
|
||||
if (fantasy && !origin.player().equals(recipient.player())
|
||||
&& parties.partyOf(origin.player()).filter(group -> group.members().contains(recipient.player())).isPresent())
|
||||
return "same_party";
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean blocked(CombatSources.Origin origin, Player target) {
|
||||
return protectionReason(origin, target) != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/** Current persistent-party native damage decision -> generic local JSONL action. */
|
||||
class PartyProtectionEventsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void currentCharacterPartyNativeDirectDamageRefusalHasOneCorrelatedFinalDecision() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
var parties = PartyLifecycleNativeTest.parties(fixture);
|
||||
var group = parties.create(attacker.core.id).orElseThrow();
|
||||
var invitation = parties.invite(attacker.core.id, group.id(), target.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(target.core.id, invitation.id()));
|
||||
var damage = PartyCombatNativeTest.hit(fixture, attacker.player, target.player);
|
||||
assertTrue(damage.isCancelled());
|
||||
for (int i = 0; i < 250 && records().stream().noneMatch(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("reason")
|
||||
&& entry.getAsJsonObject("data").get("reason").getAsString().equals("same_party")); i++)
|
||||
Thread.sleep(20);
|
||||
assertTrue(records().stream().noneMatch(entry -> entry.get("type").getAsString()
|
||||
.endsWith("combat.damage")), "A protected hit cannot become an accepted damage record");
|
||||
assertTrue(parties.leave(target.core.id, group.id()));
|
||||
assertFalse(PartyCombatNativeTest.hit(fixture, attacker.player, target.player).isCancelled());
|
||||
fixture.plugin.setEnabled(false);
|
||||
var entries = records();
|
||||
var observed = entries.stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.observed") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.toList();
|
||||
var resolved = entries.stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("reason")
|
||||
&& entry.getAsJsonObject("data").get("reason").getAsString().equals("same_party"))
|
||||
.toList();
|
||||
assertEquals(1, observed.size()); assertEquals(1, resolved.size());
|
||||
assertEquals(observed.getFirst().getAsJsonObject("data").get("action_id").getAsString(),
|
||||
resolved.getFirst().getAsJsonObject("data").get("action_id").getAsString());
|
||||
var value = resolved.getFirst().getAsJsonObject("data");
|
||||
assertEquals(group.id().toString(), value.get("source_party_id").getAsString());
|
||||
assertEquals(group.id().toString(), value.get("target_party_id").getAsString());
|
||||
assertEquals("refused", value.get("outcome").getAsString());
|
||||
assertTrue(value.has("source_location")); assertTrue(value.has("target_location"));
|
||||
assertTrue(entries.stream().noneMatch(entry -> entry.get("type").getAsString()
|
||||
.endsWith("party.created")), "Protection cannot resurrect transient lifecycle records");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void observationDoesNotReconcileNativeOngoingProvenanceBeforeProtectionPriority() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
var effect = new org.bukkit.potion.PotionEffect(org.bukkit.potion.PotionEffectType.POISON, 200, 0);
|
||||
org.mockito.Mockito.doReturn(effect).when(target.player).getPotionEffect(effect.getType());
|
||||
fixture.dispatcher.accept(new org.bukkit.event.entity.EntityPotionEffectEvent(target.player,
|
||||
null, effect, attacker.player, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_SPLASH,
|
||||
org.bukkit.event.entity.EntityPotionEffectEvent.Action.ADDED, true));
|
||||
var before = target.core.pdc.serializeToBytes();
|
||||
org.mockito.Mockito.doReturn(null).when(target.player).getPotionEffect(effect.getType());
|
||||
var atNormalPriority = new org.bukkit.event.Listener() {
|
||||
byte[] observed;
|
||||
@org.bukkit.event.EventHandler(priority = org.bukkit.event.EventPriority.NORMAL)
|
||||
public void inspect(org.bukkit.event.entity.EntityDamageEvent ignored) {
|
||||
observed = assertDoesNotThrow(() -> target.core.pdc.serializeToBytes());
|
||||
}
|
||||
};
|
||||
fixture.dispatcher.register(atNormalPriority, fixture.plugin);
|
||||
var tick = new org.bukkit.event.entity.EntityDamageEvent(target.player,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.POISON,
|
||||
org.mockito.Mockito.mock(org.bukkit.damage.DamageSource.class), 1);
|
||||
fixture.dispatcher.accept(tick);
|
||||
assertNotNull(atNormalPriority.observed, "NORMAL-priority observer must execute");
|
||||
assertArrayEquals(before, atNormalPriority.observed,
|
||||
"Audit must not reconcile native effect PDC earlier than its protection guard");
|
||||
assertFalse(tick.isCancelled());
|
||||
assertFalse(java.util.Arrays.equals(before, target.core.pdc.serializeToBytes()),
|
||||
"Original HIGHEST protection read still reconciles an ended effect");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void poisonWitherAndFireTickRecheckBoundSourceWithoutInventedNativeAttacker() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
for (var type : List.of(org.bukkit.potion.PotionEffectType.POISON,
|
||||
org.bukkit.potion.PotionEffectType.WITHER)) {
|
||||
var effect = new org.bukkit.potion.PotionEffect(type, 200, 0);
|
||||
org.mockito.Mockito.doReturn(effect).when(target.player).getPotionEffect(type);
|
||||
var apply = new org.bukkit.event.entity.EntityPotionEffectEvent(target.player, null, effect,
|
||||
attacker.player, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_SPLASH,
|
||||
org.bukkit.event.entity.EntityPotionEffectEvent.Action.ADDED, true);
|
||||
fixture.dispatcher.accept(apply);
|
||||
assertFalse(apply.isCancelled());
|
||||
}
|
||||
org.mockito.Mockito.doReturn(200).when(target.player).getFireTicks();
|
||||
var ignite = new org.bukkit.event.entity.EntityCombustByEntityEvent(attacker.player, target.player, 10.0f);
|
||||
fixture.dispatcher.accept(ignite); assertFalse(ignite.isCancelled());
|
||||
var parties = PartyLifecycleNativeTest.parties(fixture);
|
||||
var group = parties.create(attacker.core.id).orElseThrow();
|
||||
var invitation = parties.invite(attacker.core.id, group.id(), target.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(target.core.id, invitation.id()));
|
||||
var beforeVeto = target.core.pdc.serializeToBytes();
|
||||
org.mockito.Mockito.doReturn(null).when(target.player).getPotionEffect(org.bukkit.potion.PotionEffectType.POISON);
|
||||
var vetoedTick = new org.bukkit.event.entity.EntityDamageEvent(target.player,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.POISON,
|
||||
org.mockito.Mockito.mock(org.bukkit.damage.DamageSource.class), 1);
|
||||
vetoedTick.setCancelled(true); fixture.dispatcher.accept(vetoedTick);
|
||||
assertArrayEquals(beforeVeto, target.core.pdc.serializeToBytes(),
|
||||
"A prior veto must not reconcile or clear owned native provenance");
|
||||
org.mockito.Mockito.doReturn(new org.bukkit.potion.PotionEffect(
|
||||
org.bukkit.potion.PotionEffectType.POISON, 200, 0)).when(target.player)
|
||||
.getPotionEffect(org.bukkit.potion.PotionEffectType.POISON);
|
||||
for (var cause : List.of(org.bukkit.event.entity.EntityDamageEvent.DamageCause.POISON,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.WITHER,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.FIRE_TICK)) {
|
||||
var tick = new org.bukkit.event.entity.EntityDamageEvent(target.player, cause,
|
||||
org.mockito.Mockito.mock(org.bukkit.damage.DamageSource.class), 1);
|
||||
fixture.dispatcher.accept(tick);
|
||||
assertTrue(tick.isCancelled(), cause.toString());
|
||||
}
|
||||
for (var type : List.of(org.bukkit.potion.PotionEffectType.POISON,
|
||||
org.bukkit.potion.PotionEffectType.WITHER)) {
|
||||
var remove = new org.bukkit.event.entity.EntityPotionEffectEvent(target.player,
|
||||
new org.bukkit.potion.PotionEffect(type, 200, 0), null, null,
|
||||
org.bukkit.event.entity.EntityPotionEffectEvent.Cause.MILK,
|
||||
org.bukkit.event.entity.EntityPotionEffectEvent.Action.REMOVED, false);
|
||||
fixture.dispatcher.accept(remove);
|
||||
org.mockito.Mockito.doReturn(null).when(target.player).getPotionEffect(type);
|
||||
}
|
||||
org.mockito.Mockito.doReturn(0).when(target.player).getFireTicks();
|
||||
for (var cause : List.of(org.bukkit.event.entity.EntityDamageEvent.DamageCause.POISON,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.WITHER,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.FIRE_TICK)) {
|
||||
var expired = new org.bukkit.event.entity.EntityDamageEvent(target.player, cause,
|
||||
org.mockito.Mockito.mock(org.bukkit.damage.DamageSource.class), 1);
|
||||
fixture.dispatcher.accept(expired);
|
||||
assertFalse(expired.isCancelled(), "Expired provenance cannot create party protection: " + cause);
|
||||
}
|
||||
for (int i = 0; i < 250 && records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.count() < 3; i++) Thread.sleep(20);
|
||||
fixture.plugin.setEnabled(false);
|
||||
var decisions = records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.map(entry -> entry.getAsJsonObject("data")).toList();
|
||||
assertEquals(3, decisions.size());
|
||||
assertEquals(java.util.Set.of("poison", "wither", "fire_tick"), decisions.stream()
|
||||
.map(value -> value.get("damage_cause").getAsString()).collect(java.util.stream.Collectors.toSet()));
|
||||
assertTrue(decisions.stream().allMatch(value -> value.get("reason").getAsString().equals("same_party")
|
||||
&& value.get("source_location_status").getAsString().equals("unavailable")
|
||||
&& value.has("target_location")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void mixedModeAndPriorCancelledHitsReportTruthfulDistinctOutcomes() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker);
|
||||
var mixed = PartyCombatNativeTest.hit(fixture, attacker.player, target.player);
|
||||
assertTrue(mixed.isCancelled());
|
||||
fixture.activate(target);
|
||||
var parties = PartyLifecycleNativeTest.parties(fixture);
|
||||
var group = parties.create(attacker.core.id).orElseThrow();
|
||||
var invitation = parties.invite(attacker.core.id, group.id(), target.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(target.core.id, invitation.id()));
|
||||
var source = org.mockito.Mockito.mock(org.bukkit.damage.DamageSource.class);
|
||||
org.mockito.Mockito.when(source.getCausingEntity()).thenReturn(attacker.player);
|
||||
org.mockito.Mockito.when(source.getDirectEntity()).thenReturn(attacker.player);
|
||||
var cancelled = new org.bukkit.event.entity.EntityDamageByEntityEvent(attacker.player, target.player,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK, source, 4);
|
||||
cancelled.setCancelled(true); fixture.dispatcher.accept(cancelled);
|
||||
assertTrue(cancelled.isCancelled());
|
||||
for (int i = 0; i < 250 && records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.count() < 2; i++) Thread.sleep(20);
|
||||
fixture.plugin.setEnabled(false);
|
||||
var results = records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.map(entry -> entry.getAsJsonObject("data")).toList();
|
||||
assertEquals(2, results.size());
|
||||
assertTrue(results.stream().anyMatch(value -> value.get("reason").getAsString().equals("mixed_mode")
|
||||
&& value.get("outcome").getAsString().equals("refused")));
|
||||
assertTrue(results.stream().anyMatch(value -> value.get("reason").getAsString().equals("prior_cancelled")
|
||||
&& value.get("outcome").getAsString().equals("cancelled")
|
||||
&& value.get("prior_cancelled").getAsBoolean()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void stalePotionAndLingeringSplashCancellationHaveBoundedNativeResolutions() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
var potion = PartyCombatNativeTest.entity(org.bukkit.entity.ThrownPotion.class);
|
||||
org.mockito.Mockito.when(potion.getShooter()).thenReturn(attacker.player);
|
||||
fixture.dispatcher.accept(new org.bukkit.event.entity.ProjectileLaunchEvent(potion));
|
||||
PartyCombatNativeTest.exit(fixture, attacker);
|
||||
var splash = new org.bukkit.event.entity.PotionSplashEvent(potion,
|
||||
new java.util.HashMap<>(java.util.Map.of(target.player, 1.0)));
|
||||
fixture.dispatcher.accept(splash); assertTrue(splash.isCancelled());
|
||||
var cloud = PartyCombatNativeTest.entity(org.bukkit.entity.AreaEffectCloud.class);
|
||||
var lingering = new org.bukkit.event.entity.LingeringPotionSplashEvent(potion, cloud);
|
||||
fixture.dispatcher.accept(lingering); assertTrue(lingering.isCancelled());
|
||||
for (int i = 0; i < 250 && records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& (entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_splash_protection")
|
||||
|| entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_lingering_protection")))
|
||||
.count() < 2; i++) Thread.sleep(20);
|
||||
fixture.plugin.setEnabled(false);
|
||||
var decisions = records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& (entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_splash_protection")
|
||||
|| entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_lingering_protection")))
|
||||
.map(entry -> entry.getAsJsonObject("data")).toList();
|
||||
assertEquals(java.util.Set.of("party_splash_protection", "party_lingering_protection"), decisions.stream()
|
||||
.map(value -> value.get("action_kind").getAsString()).collect(java.util.stream.Collectors.toSet()));
|
||||
assertTrue(decisions.stream().allMatch(value -> value.get("reason").getAsString().equals("stale_source")
|
||||
&& value.get("outcome").getAsString().equals("refused")));
|
||||
assertEquals("not_single_proved_player", decisions.stream().filter(value -> value.get("action_kind")
|
||||
.getAsString().equals("party_lingering_protection")).findFirst().orElseThrow()
|
||||
.get("target_status").getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void protectedImpactPotionAndIgnitionDecisionsWithoutDamageEventsAreAudited() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
var parties = PartyLifecycleNativeTest.parties(fixture);
|
||||
var group = parties.create(attacker.core.id).orElseThrow();
|
||||
var invitation = parties.invite(attacker.core.id, group.id(), target.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(target.core.id, invitation.id()));
|
||||
var arrow = PartyCombatNativeTest.projectile(fixture, attacker.player);
|
||||
var impact = new org.bukkit.event.entity.ProjectileHitEvent(arrow, target.player);
|
||||
fixture.dispatcher.accept(impact); assertTrue(impact.isCancelled());
|
||||
var effect = new org.bukkit.potion.PotionEffect(org.bukkit.potion.PotionEffectType.POISON, 200, 0);
|
||||
var poison = new org.bukkit.event.entity.EntityPotionEffectEvent(target.player, null, effect,
|
||||
attacker.player, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.POTION_SPLASH,
|
||||
org.bukkit.event.entity.EntityPotionEffectEvent.Action.ADDED, true);
|
||||
fixture.dispatcher.accept(poison); assertTrue(poison.isCancelled());
|
||||
var ignition = new org.bukkit.event.entity.EntityCombustByEntityEvent(arrow, target.player, 10.0f);
|
||||
fixture.dispatcher.accept(ignition); assertTrue(ignition.isCancelled());
|
||||
for (int i = 0; i < 250 && records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().startsWith("party_"))
|
||||
.count() < 3; i++) Thread.sleep(20);
|
||||
fixture.plugin.setEnabled(false);
|
||||
var outcomes = records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().startsWith("party_"))
|
||||
.map(entry -> entry.getAsJsonObject("data")).toList();
|
||||
assertEquals(java.util.Set.of("party_projectile_impact_protection", "party_effect_application_protection",
|
||||
"party_ignite_protection"), outcomes.stream().map(value -> value.get("action_kind").getAsString())
|
||||
.collect(java.util.stream.Collectors.toSet()));
|
||||
assertTrue(outcomes.stream().allMatch(value -> value.get("reason").getAsString().equals("same_party")
|
||||
&& value.get("final_cancelled").getAsBoolean()));
|
||||
assertTrue(records().stream().noneMatch(entry -> entry.get("type").getAsString()
|
||||
.endsWith("combat.damage")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void serializedOldProjectileAfterRestartRetainsOldSessionAndNeverClaimsNewHolder() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
var sessions = (games.dmg.fantasystick.session.EquipmentSessions)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "sessions");
|
||||
var originalSession = sessions.active(attacker.core.id).orElseThrow().session();
|
||||
var arrow = PartyCombatNativeTest.projectile(fixture, attacker.player);
|
||||
var bytes = arrow.getPersistentDataContainer().serializeToBytes();
|
||||
fixture.plugin.setEnabled(false);
|
||||
fixture.awaitWorkers();
|
||||
var serialized = PartyCombatNativeTest.entity(org.bukkit.entity.Arrow.class);
|
||||
org.mockito.Mockito.when(serialized.getShooter()).thenReturn(attacker.player);
|
||||
serialized.getPersistentDataContainer().readFromBytes(bytes, true);
|
||||
fixture.plugin.setEnabled(true);
|
||||
assertTrue(PartyCombatNativeTest.hit(fixture, serialized, target.player).isCancelled());
|
||||
for (int i = 0; i < 250 && records().stream().noneMatch(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("reason")
|
||||
&& entry.getAsJsonObject("data").get("reason").getAsString().equals("stale_source")); i++)
|
||||
Thread.sleep(20);
|
||||
fixture.plugin.setEnabled(false);
|
||||
var result = records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("reason")
|
||||
&& entry.getAsJsonObject("data").get("reason").getAsString().equals("stale_source"))
|
||||
.findFirst().orElseThrow().getAsJsonObject("data");
|
||||
assertEquals(originalSession.toString(), result.get("source_session_id").getAsString());
|
||||
assertEquals("unavailable", result.get("provenance_age_status").getAsString());
|
||||
assertTrue(result.has("target_location"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void malformedAndForgedProjectileOriginsNeverBorrowCurrentCharacterOrParty() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
var arrow = PartyCombatNativeTest.projectile(fixture, attacker.player);
|
||||
var key = arrow.getPersistentDataContainer().getKeys().stream()
|
||||
.filter(value -> value.getKey().equals("combat_origin_v1")).findFirst().orElseThrow();
|
||||
arrow.getPersistentDataContainer().set(key, org.bukkit.persistence.PersistentDataType.STRING, "malformed");
|
||||
assertTrue(PartyCombatNativeTest.hit(fixture, arrow, target.player).isCancelled());
|
||||
arrow.getPersistentDataContainer().set(key, org.bukkit.persistence.PersistentDataType.STRING,
|
||||
attacker.core.id + "/" + java.util.UUID.randomUUID() + "/" + attacker.core.record.id());
|
||||
assertTrue(PartyCombatNativeTest.hit(fixture, arrow, target.player).isCancelled());
|
||||
for (int i = 0; i < 250 && records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.count() < 2; i++) Thread.sleep(20);
|
||||
fixture.plugin.setEnabled(false);
|
||||
var decisions = records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.map(entry -> entry.getAsJsonObject("data")).toList();
|
||||
assertEquals(List.of("stale_source", "stale_source"), decisions.stream()
|
||||
.map(value -> value.get("reason").getAsString()).toList());
|
||||
assertEquals("ordinary_or_unproved", decisions.getFirst().get("provenance_quality").getAsString());
|
||||
assertFalse(decisions.getFirst().has("source_session_id"));
|
||||
assertEquals("claimed_session_unverified", decisions.get(1).get("provenance_quality").getAsString());
|
||||
assertFalse(decisions.stream().anyMatch(value -> value.has("source_party_id")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void attributedProjectileRechecksCurrentPartyAndExpiresWithOriginalSession() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var attacker = actor(fixture); var target = actor(fixture);
|
||||
fixture.activate(attacker); fixture.activate(target);
|
||||
var projectile = PartyCombatNativeTest.projectile(fixture, attacker.player);
|
||||
var parties = PartyLifecycleNativeTest.parties(fixture);
|
||||
var group = parties.create(attacker.core.id).orElseThrow();
|
||||
var invitation = parties.invite(attacker.core.id, group.id(), target.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(target.core.id, invitation.id()));
|
||||
assertTrue(PartyCombatNativeTest.hit(fixture, projectile, target.player).isCancelled());
|
||||
PartyCombatNativeTest.exit(fixture, attacker);
|
||||
assertTrue(PartyCombatNativeTest.hit(fixture, projectile, target.player).isCancelled());
|
||||
for (int i = 0; i < 250 && records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.count() < 2; i++) Thread.sleep(20);
|
||||
fixture.plugin.setEnabled(false);
|
||||
var reasons = records().stream().filter(entry -> entry.get("type").getAsString()
|
||||
.endsWith("action.resolved") && entry.getAsJsonObject("data").has("action_kind")
|
||||
&& entry.getAsJsonObject("data").get("action_kind").getAsString().equals("party_damage_protection"))
|
||||
.map(entry -> entry.getAsJsonObject("data").get("reason").getAsString()).toList();
|
||||
assertEquals(List.of("same_party", "stale_source"), reasons);
|
||||
}
|
||||
}
|
||||
|
||||
private FantasyStickLifecycleNativeTest.Actor actor(FantasyStickLifecycleNativeTest.Fixture fixture) throws Exception {
|
||||
var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
fixture.players.put(actor.core.id, actor.player);
|
||||
return actor;
|
||||
}
|
||||
|
||||
private List<JsonObject> records() throws Exception {
|
||||
var result = new ArrayList<JsonObject>();
|
||||
var directory = data.resolve("events");
|
||||
if (!Files.exists(directory)) return result;
|
||||
try (var paths = Files.walk(directory)) {
|
||||
for (var path : paths.filter(p -> p.getFileName().toString().endsWith(".jsonl")).toList()) {
|
||||
var content = Files.readString(path);
|
||||
int end = content.lastIndexOf('\n');
|
||||
if (end < 0) continue;
|
||||
for (var line : content.substring(0, end).lines().toList())
|
||||
if (!line.isBlank()) result.add(JsonParser.parseString(line).getAsJsonObject());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user