feat(events): audit Witch rewards and hard-mob XP policy
Release / release (push) Failing after 11m55s
Release / release (push) Failing after 11m55s
This commit is contained in:
1 parent
6f8e83a2e8
commit
0486de9b30
11 files changed
+747
-7
No files matched your search
@@ -187,6 +187,8 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
private games.dmg.fantasystick.events.PartyEventBridge partyAuditEvents;
|
||||
private games.dmg.fantasystick.events.CombatRewardEventBridge combatRewardEvents;
|
||||
private java.util.concurrent.CompletionStage<Void> combatRewardAuditDrain = java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
private games.dmg.fantasystick.events.HardMobXpEventBridge hardMobXpAuditEvents;
|
||||
private java.util.concurrent.CompletionStage<Void> hardMobXpAuditDrain = java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
private games.dmg.fantasystick.events.TrainingEventBridge trainingAuditEvents;
|
||||
private java.util.concurrent.CompletionStage<Void> trainingAuditDrain = java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
private games.dmg.fantasystick.events.ArmorEventBridge armorAuditEvents;
|
||||
@@ -910,6 +912,9 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
failure -> getLogger().log(Level.WARNING, "Armor event intent failed", failure));
|
||||
if (armorMarket != null) armorMarket.attachEvents(armorAuditEvents);
|
||||
admin.attachArmorEvents(armorAuditEvents);
|
||||
hardMobXpAuditEvents = new games.dmg.fantasystick.events.HardMobXpEventBridge(eventLog::emit, getDataFolder().toPath(),
|
||||
failure -> getLogger().log(Level.WARNING, "Hard-mob XP event intent failed", failure));
|
||||
admin.attachHardMobXpEvents(hardMobXpAuditEvents);
|
||||
if (armorShop != null) armorShop.attachEvents(armorAuditEvents);
|
||||
combatRewardEvents = new games.dmg.fantasystick.events.CombatRewardEventBridge(eventLog::emit,Clock.systemUTC(),getDataFolder().toPath(),
|
||||
failure -> getLogger().log(Level.WARNING,"Combat reward event intent failed",failure));combatExperience.attachEvents(combatRewardEvents);
|
||||
@@ -1084,6 +1089,11 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Previous passive potion event drain failed", failure);
|
||||
if (replayPassivePotionEvents != null) replayPassivePotionEvents.replay();
|
||||
});
|
||||
var replayHardMobXpEvents = hardMobXpAuditEvents;
|
||||
hardMobXpAuditDrain.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Previous hard-mob XP event drain failed", failure);
|
||||
if (replayHardMobXpEvents != null) replayHardMobXpEvents.replay();
|
||||
});
|
||||
var replayTrainingEvents = trainingAuditEvents;
|
||||
trainingAuditDrain.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Previous training event drain failed", failure);
|
||||
@@ -1197,6 +1207,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
var closingPartyCapacityEvents = partyCapacityEvents; partyCapacityEvents = null;
|
||||
var closingSheetPackEvents = sheetResourcePackEvents; sheetResourcePackEvents = null;
|
||||
var closingGrantEvents = adminExperienceEvents; adminExperienceEvents = null;
|
||||
var closingHardMobXpEvents = hardMobXpAuditEvents; hardMobXpAuditEvents = null;
|
||||
var closingTrainingEvents = trainingAuditEvents; trainingAuditEvents = null;
|
||||
var closingArmorEvents = armorAuditEvents; armorAuditEvents = null;
|
||||
var closingCoinEvents = coinEvents;
|
||||
@@ -1568,6 +1579,14 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
closingPassivePotionEvents.close();
|
||||
passivePotionAuditDrain = closingPassivePotionEvents.drained();
|
||||
});
|
||||
if (closingHardMobXpEvents != null) cleanup("hard-mob XP event intent drain", () -> {
|
||||
var settingsDrained = closingHardMobXpSettings == null
|
||||
? java.util.concurrent.CompletableFuture.<Void>completedFuture(null) : closingHardMobXpSettings.drained();
|
||||
hardMobXpAuditDrain = settingsDrained.handle((ignored, failure) -> {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Hard-mob XP settings drain failed", failure);
|
||||
closingHardMobXpEvents.close(); return null;
|
||||
}).thenCompose(ignored -> closingHardMobXpEvents.drained());
|
||||
});
|
||||
if (closingTrainingEvents != null) cleanup("training event intent drain", () -> {
|
||||
var registrationsDrained = closingTrainingDummies == null
|
||||
? java.util.concurrent.CompletableFuture.<Void>completedFuture(null) : closingTrainingDummies.drained();
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class CombatRewardEventBridge implements AutoCloseable {
|
||||
CombatRewardRules.KillPosition restricted,CombatRewardRules rules,Capture capture,
|
||||
CharacterRecord before,CharacterStorage.CombatAward award,EquipmentSessions.Active active,
|
||||
Player player,String nativeSync,String feedback){
|
||||
if(award==null||award.record().isEmpty()||before==null||award.status()==CharacterStorage.CombatAwardStatus.DUPLICATE)return;var after=award.record().orElseThrow();var data=new LinkedHashMap<String,Object>();data.put("operation_id",operation.toString());data.put("reward_receipt",operation.toString());data.put("stage",capture.stage());data.put("recipient_character_id",character.toString());data.put("target_id",capture.targetId().toString());data.put("target_type",capture.targetType());data.put("experience_class",capture.experienceClass());data.put("coin_class",capture.coinClass());data.put("defeated_at",capture.defeatedAt().toString());data.put("direct_contributor_character_ids",capture.directContributors().stream().map(UUID::toString).sorted().toList());data.put("reward_unit_id",capture.rewardUnitId());data.put("reward_unit_count",capture.rewardUnitCount());data.put("experience_pool",capture.experiencePool().toString());data.put("coin_pool",capture.coinPool().toString());if(capture.killPosition()!=null)data.put("kill_location",position(capture.killPosition()));data.put("allocated_experience",allocatedExperience.toString());data.put("allocated_coins",allocatedCoins.toString());var appliedExperience=after.experience().subtract(before.experience()).max(BigInteger.ZERO);var appliedCoins=after.coins().subtract(before.coins()).max(BigInteger.ZERO);data.put("applied_experience",appliedExperience.toString());data.put("applied_coins",appliedCoins.toString());data.put("suppressed_experience",allocatedExperience.subtract(appliedExperience.min(allocatedExperience)).toString());data.put("suppressed_coins",allocatedCoins.subtract(appliedCoins.min(allocatedCoins)).toString());data.put("before_revision",before.revision().toString());data.put("after_revision",after.revision().toString());data.put("before_level",before.level());data.put("after_level",after.level());data.put("before_experience",before.experience().toString());data.put("after_experience",after.experience().toString());data.put("before_points",before.unspentPoints().toString());data.put("after_points",after.unspentPoints().toString());data.put("balance_before",before.coins().toString());data.put("balance_after",after.coins().toString());data.put("storage_status",award.status().name().toLowerCase(java.util.Locale.ROOT));data.put("levels_gained",award.levels());data.put("native_sync",nativeSync);data.put("feedback",feedback);if(capture.managedOrigin()!=null){data.put("resolution","managed_origin_exempt");data.put("managed_origin_kind",capture.managedOrigin().kind().name().toLowerCase(java.util.Locale.ROOT));data.put("managed_origin_source",capture.managedOrigin().source().toString());data.put("spacing_applicability","authenticated_managed_origin_exempt");if(capture.managedOrigin().kind()==ManagedEncounterOrigin.Kind.DUNGEON&&capture.managedOrigin().source().toString().equals(capture.managedEvidence().get("managed_source_id")))data.putAll(capture.managedEvidence());}else if(restricted!=null){boolean blocked=award.status()==CharacterStorage.CombatAwardStatus.SPACING_BLOCKED;data.put("resolution",blocked?"ordinary_spacing_blocked":"ordinary_spacing_eligible");data.put("spacing_applicability","ordinary_restricted_kill");data.put("spacing_minimum",rules.restrictedSpacing().toPlainString());var prior=before.restrictedKills().get(restricted.world());if(prior!=null)data.put("ordinary_history_before",position(prior));var current=after.restrictedKills().get(restricted.world());if(current!=null)data.put("ordinary_history_after",position(current));}else{data.put("resolution","spacing_not_applicable");data.put("spacing_applicability","passive_or_fantasy_character");}data.put("loss_reason",loss(award.status()));var context=context(active,player,after.name());data.put("location_capture_quality",context==null||context.location()==null?"unavailable":"exact");try{var event=new EventInput(id(operation),operation,clock.instant(),"combat.reward_resolved",context,Map.copyOf(data));if(outbox!=null)outbox.accepted(event);else sink.accept(event);}catch(RuntimeException ignored){}
|
||||
if(award==null||award.record().isEmpty()||before==null||award.status()==CharacterStorage.CombatAwardStatus.DUPLICATE)return;var after=award.record().orElseThrow();var data=new LinkedHashMap<String,Object>();data.put("operation_id",operation.toString());data.put("reward_receipt",operation.toString());data.put("stage",capture.stage());data.put("recipient_character_id",character.toString());data.put("target_id",capture.targetId().toString());data.put("target_type",capture.targetType());data.put("target_entity_type",capture.targetType().toUpperCase(java.util.Locale.ROOT));data.put("reward_class",capture.experienceClass());data.put("classifier_version","native_entity_type_v1");data.put("classifier_evidence",Map.of("native_entity_type",capture.targetType().toUpperCase(java.util.Locale.ROOT),"selected_experience_class",capture.experienceClass(),"capture_stage",capture.stage()));if("hard".equals(capture.experienceClass()))data.put("hard_mob_xp",capture.experiencePool().toString());data.put("experience_class",capture.experienceClass());data.put("coin_class",capture.coinClass());data.put("defeated_at",capture.defeatedAt().toString());data.put("direct_contributor_character_ids",capture.directContributors().stream().map(UUID::toString).sorted().toList());data.put("reward_unit_id",capture.rewardUnitId());data.put("reward_unit_count",capture.rewardUnitCount());data.put("experience_pool",capture.experiencePool().toString());data.put("coin_pool",capture.coinPool().toString());if(capture.killPosition()!=null)data.put("kill_location",position(capture.killPosition()));data.put("allocated_experience",allocatedExperience.toString());data.put("allocated_coins",allocatedCoins.toString());var appliedExperience=after.experience().subtract(before.experience()).max(BigInteger.ZERO);var appliedCoins=after.coins().subtract(before.coins()).max(BigInteger.ZERO);data.put("applied_experience",appliedExperience.toString());data.put("applied_coins",appliedCoins.toString());data.put("suppressed_experience",allocatedExperience.subtract(appliedExperience.min(allocatedExperience)).toString());data.put("suppressed_coins",allocatedCoins.subtract(appliedCoins.min(allocatedCoins)).toString());data.put("before_revision",before.revision().toString());data.put("after_revision",after.revision().toString());data.put("before_level",before.level());data.put("after_level",after.level());data.put("before_experience",before.experience().toString());data.put("after_experience",after.experience().toString());data.put("before_points",before.unspentPoints().toString());data.put("after_points",after.unspentPoints().toString());data.put("balance_before",before.coins().toString());data.put("balance_after",after.coins().toString());data.put("storage_status",award.status().name().toLowerCase(java.util.Locale.ROOT));data.put("levels_gained",award.levels());data.put("native_sync",nativeSync);data.put("feedback",feedback);if(capture.managedOrigin()!=null){data.put("resolution","managed_origin_exempt");data.put("managed_origin_kind",capture.managedOrigin().kind().name().toLowerCase(java.util.Locale.ROOT));data.put("managed_origin_source",capture.managedOrigin().source().toString());data.put("spacing_applicability","authenticated_managed_origin_exempt");if(capture.managedOrigin().kind()==ManagedEncounterOrigin.Kind.DUNGEON&&capture.managedOrigin().source().toString().equals(capture.managedEvidence().get("managed_source_id")))data.putAll(capture.managedEvidence());}else if(restricted!=null){boolean blocked=award.status()==CharacterStorage.CombatAwardStatus.SPACING_BLOCKED;data.put("resolution",blocked?"ordinary_spacing_blocked":"ordinary_spacing_eligible");data.put("spacing_applicability","ordinary_restricted_kill");data.put("spacing_minimum",rules.restrictedSpacing().toPlainString());var prior=before.restrictedKills().get(restricted.world());if(prior!=null)data.put("ordinary_history_before",position(prior));var current=after.restrictedKills().get(restricted.world());if(current!=null)data.put("ordinary_history_after",position(current));}else{data.put("resolution","spacing_not_applicable");data.put("spacing_applicability","passive_or_fantasy_character");}data.put("loss_reason",loss(award.status()));var context=context(active,player,after.name());data.put("location_capture_quality",context==null||context.location()==null?"unavailable":"exact");try{var event=new EventInput(id(operation),operation,clock.instant(),"combat.reward_resolved",context,Map.copyOf(data));if(outbox!=null)outbox.accepted(event);else sink.accept(event);}catch(RuntimeException ignored){}
|
||||
}
|
||||
private EventInput.Context context(EquipmentSessions.Active active,Player player,String characterName){
|
||||
if(active==null||player==null)return null;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import games.dmg.fantasystick.persistence.HardMobXpSettings;
|
||||
import games.dmg.fantasystick.session.EquipmentSessions;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/** Captured Witch XP policy history; replay never configures combat rewards. */
|
||||
public final class HardMobXpEventBridge implements AutoCloseable {
|
||||
public record Actor(String kind, UUID playerId, String playerName, EventInput.Context context) {}
|
||||
private final FeatureEventOutbox outbox;
|
||||
private final Clock clock = Clock.systemUTC();
|
||||
|
||||
public HardMobXpEventBridge(Function<EventInput, CompletionStage<LocalEventLog.Delivery>> sink,
|
||||
Path data, Consumer<Throwable> failures) {
|
||||
outbox = new FeatureEventOutbox(data, "hard-mob-xp-event-intents", Set.of("config.read", "config.changed"), sink, failures);
|
||||
}
|
||||
|
||||
public Actor actor(CommandSender sender, EquipmentSessions sessions) {
|
||||
String kind = sender instanceof Player ? "player" : sender instanceof org.bukkit.command.ConsoleCommandSender
|
||||
? "console" : sender instanceof org.bukkit.command.BlockCommandSender ? "command_block" : "system";
|
||||
if (!(sender instanceof Player player)) return new Actor(kind, null, null, null);
|
||||
UUID id = null; String name = null; EventInput.Context context = null;
|
||||
try { id = player.getUniqueId(); } catch (RuntimeException ignored) { }
|
||||
try { name = player.getName(); } catch (RuntimeException ignored) { }
|
||||
try {
|
||||
var active = id == null ? null : sessions.active(id).filter(sessions::current).orElse(null);
|
||||
var record = active == null ? null : sessions.currentRecord(active).orElse(null);
|
||||
var at = player.getLocation();
|
||||
var location = at == null || at.getWorld() == null ? null : new EventInput.Location(
|
||||
at.getWorld().getUID(), at.getWorld().getName(), at.getX(), at.getY(), at.getZ(), clock.instant(), "native");
|
||||
context = new EventInput.Context(id, name, record == null ? null : record.id(),
|
||||
record == null ? null : record.name(), active == null ? null : active.session(), location);
|
||||
} catch (RuntimeException ignored) { }
|
||||
return new Actor(kind, id, name, context);
|
||||
}
|
||||
|
||||
public void read(Actor actor, String value) {
|
||||
record("config.read", actor, UUID.randomUUID(), null, value);
|
||||
}
|
||||
|
||||
public void changed(Actor actor, UUID operation, String before, String value) {
|
||||
record("config.changed", actor, operation, before, value);
|
||||
}
|
||||
|
||||
private void record(String type, Actor actor, UUID operation, String before, String value) {
|
||||
try {
|
||||
var data = new LinkedHashMap<String, Object>();
|
||||
data.put("actor_kind", actor == null ? "unavailable" : actor.kind());
|
||||
data.put("request_actor_kind", actor == null ? "unavailable" : actor.kind());
|
||||
data.put("actor_capture_quality", actor == null || "player".equals(actor.kind()) && actor.context() == null ? "unavailable" : "native");
|
||||
if (actor != null && actor.playerId() != null) data.put("actor_player_id", actor.playerId().toString());
|
||||
if (actor != null && actor.playerName() != null) data.put("actor_player_name", actor.playerName());
|
||||
data.put("key", HardMobXpSettings.KEY); data.put("after", value); data.put("effective", value);
|
||||
if (before != null) data.put("before", before);
|
||||
data.put("application_scope", "future_uncaptured_defeats"); data.put("outcome", "acknowledged");
|
||||
outbox.accepted(new EventInput(operation, operation, clock.instant(), type,
|
||||
actor == null ? null : actor.context(), Map.copyOf(data)));
|
||||
} catch (RuntimeException ignored) { /* Policy publication and reads remain independent of auditing. */ }
|
||||
}
|
||||
|
||||
public void replay() { outbox.replay(); }
|
||||
public CompletionStage<Void> drained() { return outbox.drained(); }
|
||||
@Override public void close() { outbox.close(); }
|
||||
}
|
||||
@@ -83,6 +83,7 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
private games.dmg.fantasystick.events.CoinEventBridge coinEvents;
|
||||
private games.dmg.fantasystick.events.ArmorEventBridge armorEvents;
|
||||
private games.dmg.fantasystick.persistence.HardMobXpSettings hardMobXpSettings;
|
||||
private games.dmg.fantasystick.events.HardMobXpEventBridge hardMobXpEvents;
|
||||
private games.dmg.fantasystick.persistence.AdventureQuestSettings adventureQuestSettings;
|
||||
private games.dmg.fantasystick.events.FishingEventBridge fishingEvents;
|
||||
private games.dmg.fantasystick.adventure.AdventureQuestCommands adventureQuestCommands;
|
||||
@@ -371,6 +372,10 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
if (closed || armorEvents != null) throw new IllegalStateException("Armor events already attached or closed");
|
||||
armorEvents = Objects.requireNonNull(events);
|
||||
}
|
||||
public void attachHardMobXpEvents(games.dmg.fantasystick.events.HardMobXpEventBridge events) {
|
||||
if (closed || hardMobXpEvents != null) throw new IllegalStateException("Hard-mob XP events already attached or closed");
|
||||
hardMobXpEvents = Objects.requireNonNull(events);
|
||||
}
|
||||
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,
|
||||
@@ -2061,8 +2066,14 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
|
||||
private void hardMobXpConfiguration(CommandSender sender, String[] args) {
|
||||
String key = games.dmg.fantasystick.persistence.HardMobXpSettings.KEY;
|
||||
if (args.length == 2) { sender.sendMessage(key + "=" + hardMobXpSettings.value()
|
||||
+ " (applies to future uncaptured Witch defeats only)"); return; }
|
||||
if (args.length == 2) {
|
||||
String value = hardMobXpSettings.value();
|
||||
if (hardMobXpEvents != null) hardMobXpEvents.read(hardMobXpEvents.actor(sender, sessions), value);
|
||||
sender.sendMessage(key + "=" + value + " (applies to future uncaptured Witch defeats only)"); return;
|
||||
}
|
||||
var audit = hardMobXpEvents;
|
||||
var actor = audit == null ? null : audit.actor(sender, sessions);
|
||||
var operationId = UUID.randomUUID();
|
||||
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.HardMobXpSettings.Update> operation;
|
||||
try { operation = hardMobXpSettings.set(args[2]); }
|
||||
catch (IllegalArgumentException invalid) {
|
||||
@@ -2073,8 +2084,12 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
if (closed) return;
|
||||
try { nextTick.accept(() -> {
|
||||
if (closed) return;
|
||||
String before = hardMobXpSettings.value();
|
||||
boolean applied = failure == null && hardMobXpSettings.publish(saved);
|
||||
if (applied) combatExperience.configure(combatExperience.rules().withHardPool(saved.pool()));
|
||||
if (applied) {
|
||||
combatExperience.configure(combatExperience.rules().withHardPool(saved.pool()));
|
||||
if (audit != null && !before.equals(saved.value())) audit.changed(actor, operationId, before, saved.value());
|
||||
}
|
||||
if (!sender.hasPermission(PERMISSION)
|
||||
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
|
||||
if (failure != null) sender.sendMessage("Hard-mob XP save failed; the effective pool is unchanged.");
|
||||
|
||||
@@ -658,7 +658,10 @@ class CombatExperienceNativeTest {
|
||||
var b = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(a.core.id, a.player); f.players.put(b.core.id, b.player);
|
||||
f.activate(a); f.activate(b);
|
||||
hit(f, a.player, b.player, true);
|
||||
doReturn(EntityType.PLAYER).when(a.player).getType();
|
||||
doReturn(EntityType.PLAYER).when(b.player).getType();
|
||||
var nativeHit = hit(f, a.player, b.player, true);
|
||||
doReturn(nativeHit).when(b.player).getLastDamageCause();
|
||||
var source = mock(org.bukkit.damage.DamageSource.class);
|
||||
var cancelled = new org.bukkit.event.entity.PlayerDeathEvent(b.player, source, new ArrayList<>(), 0,
|
||||
net.kyori.adventure.text.Component.text("defeat"), true);
|
||||
@@ -676,7 +679,10 @@ class CombatExperienceNativeTest {
|
||||
f.activate(a); f.activate(b);finalAttacker=a.core.record.id();finalVictimPlayer=b.core.id;
|
||||
var attackerCoins = record(f, a).coins();finalAttackerCoins=attackerCoins;
|
||||
var victimCoins = record(f, b).coins();finalVictimCoins=victimCoins;
|
||||
hit(f, a.player, b.player, false);
|
||||
doReturn(EntityType.PLAYER).when(a.player).getType();
|
||||
doReturn(EntityType.PLAYER).when(b.player).getType();
|
||||
var nativeHit = hit(f, a.player, b.player, false);
|
||||
doReturn(nativeHit).when(b.player).getLastDamageCause();
|
||||
var source = mock(org.bukkit.damage.DamageSource.class);
|
||||
var defeat = new org.bukkit.event.entity.PlayerDeathEvent(b.player, source, new ArrayList<>(), 0,
|
||||
net.kyori.adventure.text.Component.text("defeat"), true);
|
||||
@@ -690,7 +696,7 @@ class CombatExperienceNativeTest {
|
||||
assertEquals(java.math.BigInteger.TEN, record(f, a).experience(), "Repeated intercepted callback cannot award twice");
|
||||
awaitRewardCount(f,second,1);
|
||||
}
|
||||
var rewards=events(second).stream().filter(event->event.get("type").getAsString().endsWith("combat.reward_resolved")).toList();assertEquals(1,rewards.size(),"Initial downing and repeated callbacks emit no extra reward resolution");var reward=rewards.getFirst().getAsJsonObject("data");assertEquals("final_death",reward.get("stage").getAsString());assertEquals(finalVictimPlayer.toString(),reward.get("target_id").getAsString());assertEquals("10",reward.get("experience_pool").getAsString());assertEquals("10",reward.get("allocated_experience").getAsString());assertEquals("10",reward.get("applied_experience").getAsString());assertEquals(finalVictimCoins.toString(),reward.get("coin_pool").getAsString());assertEquals(finalVictimCoins.toString(),reward.get("allocated_coins").getAsString());assertEquals(finalVictimCoins.toString(),reward.get("applied_coins").getAsString());assertEquals(finalAttackerCoins.toString(),reward.get("balance_before").getAsString());assertEquals(finalAttackerCoins.add(finalVictimCoins).toString(),reward.get("balance_after").getAsString());assertEquals("spacing_not_applicable",reward.get("resolution").getAsString());assertEquals(java.util.List.of(finalAttacker.toString()),reward.getAsJsonArray("direct_contributor_character_ids").asList().stream().map(com.google.gson.JsonElement::getAsString).toList());assertTrue(reward.has("reward_unit_id"));var losses=events(second).stream().filter(event->event.get("type").getAsString().endsWith("character.coins_lost")).toList();assertEquals(1,losses.size());var loss=losses.getFirst().getAsJsonObject("data");assertEquals(deathEpisode.toString(),loss.get("death_id").getAsString());assertEquals(finalVictimCoins.toString(),loss.get("balance_before").getAsString());assertEquals("0",loss.get("balance_after").getAsString());assertEquals(finalVictimCoins.toString(),loss.get("death_bound_escrow").getAsString());assertEquals("player_attributed_final_death",loss.get("reason").getAsString());var returns=events(second).stream().filter(event->event.get("type").getAsString().endsWith("action.resolved")&&event.getAsJsonObject("data").has("action_kind")&&event.getAsJsonObject("data").get("action_kind").getAsString().equals("final_death_return")).toList();assertEquals(1,returns.size());assertEquals(deathEpisode.toString(),returns.getFirst().getAsJsonObject("data").get("death_id").getAsString());assertEquals("returned",returns.getFirst().getAsJsonObject("data").get("outcome").getAsString());
|
||||
var rewards=events(second).stream().filter(event->event.get("type").getAsString().endsWith("combat.reward_resolved")).toList();assertEquals(1,rewards.size(),"Initial downing and repeated callbacks emit no extra reward resolution");var reward=rewards.getFirst().getAsJsonObject("data");assertEquals("final_death",reward.get("stage").getAsString());assertEquals("fantasy_character",reward.get("reward_class").getAsString());assertEquals("PLAYER",reward.get("target_entity_type").getAsString());assertEquals("native_entity_type_v1",reward.get("classifier_version").getAsString());assertFalse(reward.has("hard_mob_xp"));assertEquals(finalVictimPlayer.toString(),reward.get("target_id").getAsString());assertEquals("10",reward.get("experience_pool").getAsString());assertEquals("10",reward.get("allocated_experience").getAsString());assertEquals("10",reward.get("applied_experience").getAsString());assertEquals(finalVictimCoins.toString(),reward.get("coin_pool").getAsString());assertEquals(finalVictimCoins.toString(),reward.get("allocated_coins").getAsString());assertEquals(finalVictimCoins.toString(),reward.get("applied_coins").getAsString());assertEquals(finalAttackerCoins.toString(),reward.get("balance_before").getAsString());assertEquals(finalAttackerCoins.add(finalVictimCoins).toString(),reward.get("balance_after").getAsString());assertEquals("spacing_not_applicable",reward.get("resolution").getAsString());assertEquals(java.util.List.of(finalAttacker.toString()),reward.getAsJsonArray("direct_contributor_character_ids").asList().stream().map(com.google.gson.JsonElement::getAsString).toList());assertTrue(reward.has("reward_unit_id"));var losses=events(second).stream().filter(event->event.get("type").getAsString().endsWith("character.coins_lost")).toList();assertEquals(1,losses.size());var loss=losses.getFirst().getAsJsonObject("data");assertEquals(deathEpisode.toString(),loss.get("death_id").getAsString());assertEquals(finalVictimCoins.toString(),loss.get("balance_before").getAsString());assertEquals("0",loss.get("balance_after").getAsString());assertEquals(finalVictimCoins.toString(),loss.get("death_bound_escrow").getAsString());assertEquals("player_attributed_final_death",loss.get("reason").getAsString());var returns=events(second).stream().filter(event->event.get("type").getAsString().endsWith("action.resolved")&&event.getAsJsonObject("data").has("action_kind")&&event.getAsJsonObject("data").get("action_kind").getAsString().equals("final_death_return")).toList();assertEquals(1,returns.size());assertEquals(deathEpisode.toString(),returns.getFirst().getAsJsonObject("data").get("death_id").getAsString());assertEquals("returned",returns.getFirst().getAsJsonObject("data").get("outcome").getAsString());
|
||||
}
|
||||
|
||||
static EntityDamageByEntityEvent hit(FantasyStickLifecycleNativeTest.Fixture f, org.bukkit.entity.Entity source,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class HardMobXpEventsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void twoRegisteredQueuedChangesRecordActualPublicationBeforeAndCapturedActorWithoutTouchingCharacters() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var actor = new FantasyStickLifecycleNativeTest.Actor(data); actor.allowed = true;
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
var before = CombatExperienceNativeTest.record(f, actor);
|
||||
var settings = (games.dmg.fantasystick.persistence.HardMobXpSettings)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "hardMobXpSettings");
|
||||
Files.writeString(data.resolve("config.yml"), "\nus058-unrelated: retain-me\n",
|
||||
java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
|
||||
f.adminCommand.execute(actor.player, "fstickadmin", new String[]{"config", "hard-mob-xp", "30"});
|
||||
actor.core.location.setX(900);
|
||||
f.adminCommand.execute(actor.player, "fstickadmin", new String[]{"config", "hard-mob-xp", "40"});
|
||||
assertEquals("20", settings.value(), "Persistence alone must not publish either queued request");
|
||||
ReskillEventsNativeTest.await(f, () -> actor.messages.stream().anyMatch(message -> message.startsWith("Saved hard-mob-xp=40")));
|
||||
assertEquals("40", settings.value());
|
||||
assertEquals(before, CombatExperienceNativeTest.record(f, actor));
|
||||
var yaml = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(data.resolve("config.yml").toFile());
|
||||
assertEquals("40", yaml.getString("hard-mob-xp")); assertEquals("retain-me", yaml.getString("us058-unrelated"));
|
||||
ReskillEventsNativeTest.await(f, () -> {
|
||||
try { return changes().size() == 2; } catch (Exception failure) { throw new AssertionError(failure); }
|
||||
});
|
||||
// Flush accepted owner work without treating native publication as a writer acknowledgement.
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
((java.util.concurrent.CompletionStage<?>) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "hardMobXpAuditDrain"))
|
||||
.toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
var rows = changes();
|
||||
assertEquals(2, rows.size(), "Two real queued policy publications must reach their owner JSONL history");
|
||||
var first = rows.stream().filter(row -> row.getAsJsonObject("data").get("after").getAsString().equals("30")).findFirst().orElseThrow();
|
||||
var second = rows.stream().filter(row -> row.getAsJsonObject("data").get("after").getAsString().equals("40")).findFirst().orElseThrow();
|
||||
assertEquals("20", first.getAsJsonObject("data").get("before").getAsString());
|
||||
assertEquals("30", second.getAsJsonObject("data").get("before").getAsString(),
|
||||
"The second change starts at the preceding actual publication, not its request-time snapshot");
|
||||
assertNotEquals(first.get("id"), second.get("id"));
|
||||
assertEquals("12.0", first.getAsJsonObject("data").getAsJsonObject("location").get("x").getAsString());
|
||||
assertEquals("900.0", second.getAsJsonObject("data").getAsJsonObject("location").get("x").getAsString());
|
||||
for (var row : rows) {
|
||||
var values = row.getAsJsonObject("data");
|
||||
assertEquals(actor.core.id.toString(), row.get("playerid").getAsString());
|
||||
assertEquals(before.id().toString(), row.get("characterid").getAsString());
|
||||
assertEquals(values.get("after"), values.get("effective"));
|
||||
assertEquals("future_uncaptured_defeats", values.get("application_scope").getAsString());
|
||||
assertEquals("acknowledged", values.get("outcome").getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private java.util.List<com.google.gson.JsonObject> changes() throws Exception {
|
||||
return ReskillEventsNativeTest.records(data).stream().filter(row ->
|
||||
row.get("type").getAsString().endsWith("config.changed")
|
||||
&& row.getAsJsonObject("data").get("key").getAsString().equals("hard-mob-xp")).toList();
|
||||
}
|
||||
|
||||
@Test void actualAuthorizedDefaultReadExportsSafeFutureUncapturedScopeWithoutSavingOrChangingRules() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var sender = new EquipmentAdminCommandNativeTest.Sender();
|
||||
var settings = (games.dmg.fantasystick.persistence.HardMobXpSettings)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "hardMobXpSettings");
|
||||
var combat = (games.dmg.fantasystick.party.CombatExperience)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "combatExperience");
|
||||
var rules = combat.rules();
|
||||
var file = data.resolve("config.yml"); var bytes = Files.exists(file) ? Files.readAllBytes(file) : null;
|
||||
assertEquals(java.util.List.of("hard-mob-xp"), f.adminCommand.getTabCompleter().onTabComplete(sender.port,
|
||||
f.adminCommand, "fstickadmin", new String[]{"config", "hard-mob"}));
|
||||
assertTrue(f.adminCommand.getExecutor().onCommand(sender.port, f.adminCommand,
|
||||
"fstickadmin", new String[]{"config", "hard-mob-xp"}));
|
||||
assertTrue(sender.messages.getLast().startsWith("hard-mob-xp=20"));
|
||||
assertEquals("20", settings.value()); assertEquals(rules, combat.rules());
|
||||
if (bytes == null) assertFalse(Files.exists(file)); else assertArrayEquals(bytes, Files.readAllBytes(file));
|
||||
ReskillEventsNativeTest.await(f, () -> {
|
||||
try { return ReskillEventsNativeTest.records(data).stream().anyMatch(row ->
|
||||
row.get("type").getAsString().endsWith("config.read")
|
||||
&& row.getAsJsonObject("data").get("key").getAsString().equals("hard-mob-xp")); }
|
||||
catch (Exception failure) { throw new AssertionError(failure); }
|
||||
});
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
var records = ReskillEventsNativeTest.records(data).stream()
|
||||
.filter(row -> row.get("type").getAsString().endsWith("config.read")
|
||||
&& row.getAsJsonObject("data").get("key").getAsString().equals("hard-mob-xp"))
|
||||
.toList();
|
||||
assertEquals(1, records.size(),
|
||||
"An actual registered authorized hard-mob-XP read must reach its feature-owned JSONL record");
|
||||
var values = records.getFirst().getAsJsonObject("data");
|
||||
assertEquals("20", values.get("after").getAsString());
|
||||
assertEquals("20", values.get("effective").getAsString());
|
||||
assertEquals("future_uncaptured_defeats", values.get("application_scope").getAsString());
|
||||
assertEquals("acknowledged", values.get("outcome").getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static games.dmg.fantasystick.WitchAuditTestSupport.*;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import games.dmg.fantasystick.events.LocalEventLog;
|
||||
import games.dmg.fantasystick.persistence.HardMobXpSettings;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class HardMobXpSafetyReplayNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void deniedInvalidLockedClosedAndSameValueRequestsCannotInventChangedPolicy() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var sender = new EquipmentAdminCommandNativeTest.Sender();
|
||||
var denied = new EquipmentAdminCommandNativeTest.Sender(); denied.allowed = false;
|
||||
var settings = settings(f);
|
||||
assertTrue(f.adminCommand.tabComplete(denied.port, "fstickadmin", new String[]{"config", "hard"}).isEmpty());
|
||||
f.adminCommand.execute(denied.port, "fstickadmin", new String[]{"config", "hard-mob-xp"});
|
||||
f.adminCommand.execute(denied.port, "fstickadmin", new String[]{"config", "hard-mob-xp", "30"});
|
||||
for (String invalid : List.of("-1", "2147483648", "NaN", "1.5")) {
|
||||
f.adminCommand.execute(sender.port, "fstickadmin", new String[]{"config", "hard-mob-xp", invalid});
|
||||
assertEquals("20", settings.value());
|
||||
}
|
||||
f.adminCommand.execute(sender.port, "fstickadmin", new String[]{"config", "hard-mob-xp", "30", "extra"});
|
||||
assertTrue(rows().isEmpty());
|
||||
f.adminCommand.execute(sender.port, "fstickadmin", new String[]{"config", "hard-mob-xp", "20"});
|
||||
ReskillEventsNativeTest.await(f, () -> sender.messages.stream().anyMatch(m -> m.startsWith("Saved hard-mob-xp=20")));
|
||||
var bytes = Files.readAllBytes(data.resolve("config.yml"));
|
||||
try (var channel = FileChannel.open(data.resolve(".config.lock"), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
||||
var lock = channel.lock()) {
|
||||
assertTrue(lock.isValid());
|
||||
f.adminCommand.execute(sender.port, "fstickadmin", new String[]{"config", "hard-mob-xp", "30"});
|
||||
ReskillEventsNativeTest.await(f, () -> sender.messages.contains("Hard-mob XP save failed; the effective pool is unchanged."));
|
||||
assertEquals("20", settings.value()); assertArrayEquals(bytes, Files.readAllBytes(data.resolve("config.yml")));
|
||||
}
|
||||
settings.close(); sender.messages.clear();
|
||||
f.adminCommand.execute(sender.port, "fstickadmin", new String[]{"config", "hard-mob-xp", "40"});
|
||||
ReskillEventsNativeTest.await(f, () -> sender.messages.contains("Hard-mob XP save failed; the effective pool is unchanged."));
|
||||
assertEquals("20", settings.value()); assertArrayEquals(bytes, Files.readAllBytes(data.resolve("config.yml")));
|
||||
f.adminCommand.execute(sender.port, "fstickadmin", new String[]{"config", "hard-mob-xp"});
|
||||
ReskillEventsNativeTest.await(f, () -> rows().size() == 1);
|
||||
assertEquals("games.dmg.minecraft.fantasystick.config.read", rows().getFirst().get("type").getAsString());
|
||||
f.plugin.setEnabled(false); f.awaitWorkers(); drain(f, "hardMobXpAuditDrain");
|
||||
assertEquals(1, rows().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void rejectedRealReadAndPublicationIntentsReplayEveryOriginalPropertyWithoutPolicyOrPlayerAccess() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data); actor.allowed = true;
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
var before = CombatExperienceNativeTest.record(f, actor);
|
||||
((LocalEventLog) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "eventLog")).close();
|
||||
f.adminCommand.execute(actor.player, "fstickadmin", new String[]{"config", "hard-mob-xp"});
|
||||
f.adminCommand.execute(actor.player, "fstickadmin", new String[]{"config", "hard-mob-xp", "30"});
|
||||
ReskillEventsNativeTest.await(f, () -> actor.messages.stream().anyMatch(m -> m.startsWith("Saved hard-mob-xp=30")));
|
||||
f.adminCommand.execute(actor.player, "fstickadmin", new String[]{"config", "hard-mob-xp"});
|
||||
f.adminCommand.execute(actor.player, "fstickadmin", new String[]{"config", "hard-mob-xp", "40"});
|
||||
ReskillEventsNativeTest.await(f, () -> actor.messages.stream().anyMatch(m -> m.startsWith("Saved hard-mob-xp=40")));
|
||||
assertEquals("40", settings(f).value()); assertEquals(before, CombatExperienceNativeTest.record(f, actor));
|
||||
f.plugin.setEnabled(false); f.awaitWorkers(); drain(f, "hardMobXpAuditDrain"); drain(f, "configurationDrain");
|
||||
var original = intents(data.resolve("hard-mob-xp-event-intents")); assertEquals(4, original.size());
|
||||
assertTrue(rows().isEmpty()); var bytes = Files.readAllBytes(data.resolve("config.yml"));
|
||||
var inventory = actor.core.snapshot();
|
||||
var messages = List.copyOf(actor.messages);
|
||||
f.players.clear(); clearInvocations(actor.player);
|
||||
f.plugin.setEnabled(true);
|
||||
ReskillEventsNativeTest.await(f, () -> rows().size() == 4);
|
||||
assertEquals("40", settings(f).value()); assertArrayEquals(bytes, Files.readAllBytes(data.resolve("config.yml")));
|
||||
var storage = (games.dmg.fantasystick.persistence.CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
assertEquals(before, storage.find(before.id()).get().orElseThrow());
|
||||
assertEquals(messages, actor.messages); assertArrayEquals(inventory, actor.core.snapshot());
|
||||
verifyNoInteractions(actor.player);
|
||||
var all = rows(); assertEquals(2, all.stream().filter(r -> r.get("type").getAsString().endsWith("config.changed")).count());
|
||||
for (var intent : original) assertOriginal(intent, all.stream().filter(r -> r.get("id").getAsString().equals(intent.getProperty("id"))).findFirst().orElseThrow());
|
||||
var bridge = (games.dmg.fantasystick.events.HardMobXpEventBridge) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "hardMobXpAuditEvents");
|
||||
bridge.replay(); bridge.replay();
|
||||
f.plugin.setEnabled(false); f.awaitWorkers(); drain(f, "hardMobXpAuditDrain");
|
||||
assertEquals(4, rows().size()); assertEquals(messages, actor.messages);
|
||||
}
|
||||
}
|
||||
|
||||
private static HardMobXpSettings settings(FantasyStickLifecycleNativeTest.Fixture f) throws Exception {
|
||||
return (HardMobXpSettings) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "hardMobXpSettings");
|
||||
}
|
||||
private List<JsonObject> rows() {
|
||||
try {
|
||||
return ReskillEventsNativeTest.records(data).stream().filter(r -> {
|
||||
var type = r.get("type").getAsString(); var values = r.getAsJsonObject("data");
|
||||
return (type.endsWith("config.read") || type.endsWith("config.changed")) && values.has("key")
|
||||
&& values.get("key").getAsString().equals("hard-mob-xp");
|
||||
}).toList();
|
||||
} catch (Exception failure) { throw new AssertionError(failure); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class WitchAuditTestSupport {
|
||||
private WitchAuditTestSupport() {}
|
||||
static void drain(FantasyStickLifecycleNativeTest.Fixture f, String field) throws Exception {
|
||||
((java.util.concurrent.CompletionStage<?>) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, field))
|
||||
.toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
static List<Properties> intents(Path folder) throws Exception {
|
||||
var result = new ArrayList<Properties>();
|
||||
try (var files = Files.list(folder)) {
|
||||
for (var path : files.filter(p -> p.getFileName().toString().endsWith(".pending")).toList()) {
|
||||
var intent = new Properties(); try (var input = Files.newInputStream(path)) { intent.load(input); }
|
||||
result.add(intent);
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
static void assertOriginal(Properties intent, JsonObject row) {
|
||||
var values = row.getAsJsonObject("data");
|
||||
assertEquals(intent.getProperty("id"), row.get("id").getAsString());
|
||||
assertEquals(intent.getProperty("time"), row.get("time").getAsString());
|
||||
assertEquals(intent.getProperty("action"), values.get("action_id").getAsString());
|
||||
if (intent.containsKey("player")) assertEquals(intent.getProperty("player"), row.get("playerid").getAsString());
|
||||
if (intent.containsKey("character")) assertEquals(intent.getProperty("character"), row.get("characterid").getAsString());
|
||||
if (intent.containsKey("session")) assertEquals(intent.getProperty("session"), values.get("session_id").getAsString());
|
||||
for (var key : intent.stringPropertyNames()) if (key.startsWith("data.")) {
|
||||
var encoded = intent.getProperty(key);
|
||||
var expected = switch (encoded.charAt(0)) {
|
||||
case 's' -> new JsonPrimitive(encoded.substring(1));
|
||||
case 'b' -> new JsonPrimitive(Boolean.parseBoolean(encoded.substring(1)));
|
||||
case 'i' -> new JsonPrimitive(Integer.parseInt(encoded.substring(1)));
|
||||
case 'd' -> new JsonPrimitive(Double.parseDouble(encoded.substring(1)));
|
||||
case 'j' -> JsonParser.parseString(encoded.substring(1));
|
||||
default -> throw new AssertionError("Unknown intent encoding: " + key);
|
||||
};
|
||||
var parts = key.substring(5).split("\\.");
|
||||
var actual = parts.length == 1 ? values.get(parts[0]) : values.getAsJsonObject(parts[0]).get(parts[1]);
|
||||
assertEquals(expected, actual, "Every captured property must survive audit-only replay: " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static games.dmg.fantasystick.CombatExperienceNativeTest.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class WitchRewardContractsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void realNativeNonWitchWitherAndBossRecipientsKeepOneUnchangedClassificationAndCoinClass() throws Exception {
|
||||
var expected = Map.of(EntityType.COW, 1, EntityType.BEE, 10, EntityType.EVOKER, 10,
|
||||
EntityType.WITHER, 10, EntityType.WITCH, 20, EntityType.WARDEN, 100, EntityType.ENDER_DRAGON, 100);
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor); int n = 0;
|
||||
for (var type : List.of(EntityType.COW, EntityType.BEE, EntityType.EVOKER, EntityType.WITHER,
|
||||
EntityType.WITCH, EntityType.WARDEN, EntityType.ENDER_DRAGON)) {
|
||||
var before = record(f, actor); var target = mob(type, new Location(actor.world, n++ * 10, 64, 0));
|
||||
hit(f, actor.player, target, false); die(f, target); settle(f);
|
||||
assertEquals(before.experience().add(BigInteger.valueOf(expected.get(type))), record(f, actor).experience());
|
||||
awaitRewardCount(f, data, n);
|
||||
var row = CombatRewardEventsNativeTest.rewardRows(data).stream().map(r -> r.getAsJsonObject("data"))
|
||||
.filter(r -> r.get("target_id").getAsString().equals(target.getUniqueId().toString())).findFirst().orElseThrow();
|
||||
assertEquals(type.name(), row.get("target_entity_type").getAsString());
|
||||
String classification = type == EntityType.COW ? "passive" : type == EntityType.WITCH ? "hard"
|
||||
: type == EntityType.WARDEN || type == EntityType.ENDER_DRAGON ? "boss" : "ordinary";
|
||||
assertEquals(classification, row.get("reward_class").getAsString());
|
||||
assertEquals(classification, row.get("experience_class").getAsString());
|
||||
assertEquals("native_entity_type_v1", row.get("classifier_version").getAsString());
|
||||
assertEquals(type.name(), row.getAsJsonObject("classifier_evidence").get("native_entity_type").getAsString());
|
||||
assertEquals(Integer.toString(expected.get(type)), row.get("applied_experience").getAsString());
|
||||
String coinClass = type == EntityType.COW ? "passive" : type == EntityType.WARDEN
|
||||
|| type == EntityType.ENDER_DRAGON || type == EntityType.WITHER ? "boss" : type == EntityType.BEE ? "neutral" : "hostile";
|
||||
assertEquals(coinClass, row.get("coin_class").getAsString());
|
||||
assertEquals(type == EntityType.WITCH, row.has("hard_mob_xp"));
|
||||
assertEquals(record(f, actor).coins().subtract(before.coins()).toString(), row.get("applied_coins").getAsString());
|
||||
die(f, target); settle(f); assertEquals(n, CombatRewardEventsNativeTest.rewardRows(data).size());
|
||||
}
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
@Test void mutedRealWitchesRetainBlockedHistoryAndExactlyThreeBlockBoundaryAuditWithoutRoutineCombatReports() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
f.playerCommand.execute(actor.player, "fstick", new String[]{"feedback", "off"});
|
||||
ReskillEventsNativeTest.await(f, () -> actor.messages.contains("Fantasy feedback is now off.")); actor.messages.clear();
|
||||
var before = record(f, actor); int n = 0;
|
||||
for (double x : new double[]{0, 2, 5}) {
|
||||
var target = mob(EntityType.WITCH, new Location(actor.world, x, 64, 0));
|
||||
if (n == 0) {
|
||||
actor.core.inventory.getInventory().setSelectedSlot(1);
|
||||
org.mockito.Mockito.doAnswer(call -> {
|
||||
assertFalse(hit(f, actor.player, target, false).isCancelled()); return null;
|
||||
}).when(target).damage(org.mockito.ArgumentMatchers.anyDouble(), org.mockito.ArgumentMatchers.same(actor.player));
|
||||
var input = new io.papermc.paper.event.player.PrePlayerAttackEntityEvent(actor.player, target, true);
|
||||
f.dispatcher.accept(input); assertTrue(input.isCancelled(), "Bound stick contains vanilla continuation");
|
||||
org.mockito.Mockito.verify(target).damage(org.mockito.ArgumentMatchers.anyDouble(), org.mockito.ArgumentMatchers.same(actor.player));
|
||||
} else hit(f, actor.player, target, false);
|
||||
die(f, target); settle(f); awaitRewardCount(f, data, ++n);
|
||||
assertEquals(java.math.BigDecimal.valueOf(x), record(f, actor).restrictedKills().get(actor.world.getUID()).x());
|
||||
}
|
||||
var rows = CombatRewardEventsNativeTest.rewardRows(data).stream().map(r -> r.getAsJsonObject("data")).toList();
|
||||
assertEquals(List.of("20", "0", "20"), rows.stream().map(r -> r.get("applied_experience").getAsString()).toList());
|
||||
assertEquals("ordinary_spacing_blocked", rows.get(1).get("resolution").getAsString());
|
||||
assertEquals("20", rows.get(1).get("suppressed_experience").getAsString());
|
||||
assertEquals(rows.get(1).get("allocated_coins"), rows.get(1).get("suppressed_coins"));
|
||||
assertEquals("0", rows.get(1).get("applied_coins").getAsString());
|
||||
assertEquals("2.0", rows.get(2).getAsJsonObject("ordinary_history_before").get("x").getAsString());
|
||||
assertEquals("ordinary_spacing_eligible", rows.get(2).get("resolution").getAsString());
|
||||
assertEquals(before.experience().add(BigInteger.valueOf(40)), record(f, actor).experience());
|
||||
assertFalse(actor.messages.stream().anyMatch(m -> m.startsWith("Basic Attack did")),
|
||||
"Muted real Basic Attack reports stay suppressed while audit and progression remain visible");
|
||||
assertTrue(actor.messages.stream().anyMatch(m -> m.contains("Earned 20 fantasy XP")));
|
||||
assertTrue(actor.messages.stream().anyMatch(m -> m.contains("too close to your previous one")));
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
@Test void cappedWitchLosesOnlyExactXpAllocationAndStillConsumesOrdinaryHistoryAndHostileCoins() throws Exception {
|
||||
Files.writeString(data.resolve("config.yml"), "maximum-level: 1\n");
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor); var before = record(f, actor);
|
||||
var target = mob(EntityType.WITCH, new Location(actor.world, 0, 64, 0));
|
||||
hit(f, actor.player, target, false); die(f, target); settle(f); awaitRewardCount(f, data, 1);
|
||||
var after = record(f, actor); var row = CombatRewardEventsNativeTest.rewardRows(data).getFirst().getAsJsonObject("data");
|
||||
assertEquals(before.experience(), after.experience()); assertEquals("20", row.get("allocated_experience").getAsString());
|
||||
assertEquals("20", row.get("suppressed_experience").getAsString()); assertEquals("0", row.get("applied_experience").getAsString());
|
||||
assertEquals("at_cap", row.get("storage_status").getAsString()); assertEquals("maximum_level", row.get("loss_reason").getAsString());
|
||||
assertEquals("hostile", row.get("coin_class").getAsString());
|
||||
assertEquals(after.coins().subtract(before.coins()).toString(), row.get("allocated_coins").getAsString());
|
||||
assertTrue(after.coins().compareTo(before.coins()) > 0); assertEquals("0", row.get("suppressed_coins").getAsString());
|
||||
assertEquals(java.math.BigDecimal.valueOf(0.0), after.restrictedKills().get(actor.world.getUID()).x());
|
||||
assertTrue(after.rewardReceipts().contains(UUID.fromString(row.get("reward_receipt").getAsString())));
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
@Test void eachCapturedManagedWitchOriginBypassesSpacingWithoutAdvancingOrdinaryHistory() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
var ordinary = mob(EntityType.WITCH, new Location(actor.world, 0, 64, 0));
|
||||
hit(f, actor.player, ordinary, false); die(f, ordinary); settle(f); awaitRewardCount(f, data, 1);
|
||||
var history = record(f, actor).restrictedKills();
|
||||
var origins = new java.util.HashMap<UUID, games.dmg.fantasystick.progression.ManagedEncounterOrigin>();
|
||||
var combat = (games.dmg.fantasystick.party.CombatExperience) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "combatExperience");
|
||||
combat.attachManagedOrigins(entity -> java.util.Optional.ofNullable(origins.get(entity.getUniqueId())));
|
||||
int n = 1;
|
||||
for (var kind : games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.values()) {
|
||||
var target = mob(EntityType.WITCH, new Location(actor.world, 0, 64, 0));
|
||||
var authority = UUID.randomUUID(); origins.put(target.getUniqueId(), new games.dmg.fantasystick.progression.ManagedEncounterOrigin(kind, authority));
|
||||
var before = record(f, actor); hit(f, actor.player, target, false); die(f, target); settle(f); awaitRewardCount(f, data, ++n);
|
||||
assertEquals(history, record(f, actor).restrictedKills());
|
||||
assertEquals(before.experience().add(BigInteger.valueOf(20)), record(f, actor).experience());
|
||||
var row = CombatRewardEventsNativeTest.rewardRows(data).stream().map(r -> r.getAsJsonObject("data"))
|
||||
.filter(r -> r.get("target_id").getAsString().equals(target.getUniqueId().toString())).findFirst().orElseThrow();
|
||||
assertEquals("hard", row.get("reward_class").getAsString()); assertEquals("managed_origin_exempt", row.get("resolution").getAsString());
|
||||
assertEquals(authority.toString(), row.get("managed_origin_source").getAsString());
|
||||
assertEquals(kind.name().toLowerCase(java.util.Locale.ROOT), row.get("managed_origin_kind").getAsString());
|
||||
assertFalse(row.has("ordinary_history_after")); assertFalse(row.has("ordinary_history_before"));
|
||||
die(f, target); combat.retryPending(); settle(f); assertEquals(n, CombatRewardEventsNativeTest.rewardRows(data).size());
|
||||
}
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static games.dmg.fantasystick.CombatExperienceNativeTest.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class WitchRewardEventsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void realDirectAndOwnedProjectileWitchDefeatExportsOneCapturedHardVariantPerSoloAndPersistentPartyRecipient() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var solo = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var b = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var c = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var actors = List.of(solo, a, b, c);
|
||||
for (var actor : actors) {
|
||||
actor.core.location.setWorld(solo.world);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
}
|
||||
var parties = PartyLifecycleNativeTest.parties(f); var party = parties.create(a.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(b.core.id, parties.invite(a.core.id, party.id(), b.core.id).orElseThrow().id()));
|
||||
assertTrue(parties.accept(c.core.id, parties.invite(a.core.id, party.id(), c.core.id).orElseThrow().id()));
|
||||
assertEquals(Set.of(a.core.record.id(), b.core.record.id(), c.core.record.id()),
|
||||
PartyLifecycleNativeTest.directory(f).partyById(party.id()).orElseThrow().members());
|
||||
var beforeCoins = actors.stream().map(actor -> {
|
||||
try { return record(f, actor).coins(); } catch (Exception failure) { throw new AssertionError(failure); }
|
||||
}).reduce(BigInteger.ZERO, BigInteger::add);
|
||||
var witch = mob(EntityType.WITCH, new Location(solo.world, 10, 64, 0)); var target = witch.getUniqueId();
|
||||
assertFalse(hit(f, solo.player, witch, false).isCancelled());
|
||||
var arrow = PartyCombatNativeTest.projectile(f, a.player);
|
||||
assertFalse(hit(f, arrow, witch, false).isCancelled());
|
||||
die(f, witch); settle(f); settle(f); settle(f);
|
||||
assertEquals(BigInteger.TEN, record(f, solo).experience());
|
||||
assertEquals(List.of(BigInteger.valueOf(3), BigInteger.valueOf(3), BigInteger.valueOf(4)),
|
||||
List.of(record(f, a).experience(), record(f, b).experience(), record(f, c).experience()).stream().sorted().toList());
|
||||
var afterCoins = actors.stream().map(actor -> {
|
||||
try { return record(f, actor).coins(); } catch (Exception failure) { throw new AssertionError(failure); }
|
||||
}).reduce(BigInteger.ZERO, BigInteger::add);
|
||||
var awardedCoins = afterCoins.subtract(beforeCoins);
|
||||
assertTrue(awardedCoins.compareTo(BigInteger.ONE) >= 0 && awardedCoins.compareTo(BigInteger.valueOf(5)) <= 0);
|
||||
solo.core.inventory.getInventory().setSelectedSlot(0); PartyBookNativeTest.open(f, solo);
|
||||
assertTrue(PartyBookNativeTest.text(solo).contains("XP: 10/100"));
|
||||
assertTrue(solo.messages.stream().anyMatch(message -> message.contains("Earned 10 fantasy XP")));
|
||||
ReskillEventsNativeTest.await(f, () -> rewards(target).size() == 4);
|
||||
var rows = rewards(target); assertEquals(4, rows.size());
|
||||
assertEquals(4, rows.stream().map(row -> row.get("id").getAsString()).distinct().count());
|
||||
BigInteger xp = BigInteger.ZERO, coins = BigInteger.ZERO;
|
||||
for (var row : rows) {
|
||||
var values = row.getAsJsonObject("data");
|
||||
assertTrue(values.has("reward_class"),
|
||||
"The actual saved shared Witch reward must expose its approved captured hard classifier, not a second reward event");
|
||||
assertEquals("hard", values.get("reward_class").getAsString());
|
||||
assertEquals("WITCH", values.get("target_entity_type").getAsString());
|
||||
assertEquals("native_entity_type_v1", values.get("classifier_version").getAsString());
|
||||
assertTrue(values.has("classifier_evidence"));
|
||||
assertEquals("WITCH", values.getAsJsonObject("classifier_evidence").get("native_entity_type").getAsString());
|
||||
assertEquals("20", values.get("hard_mob_xp").getAsString());
|
||||
assertEquals("20", values.get("experience_pool").getAsString());
|
||||
assertEquals("hostile", values.get("coin_class").getAsString());
|
||||
assertEquals(awardedCoins.toString(), values.get("coin_pool").getAsString());
|
||||
assertEquals(2, values.get("reward_unit_count").getAsInt());
|
||||
assertEquals(Set.of(solo.core.record.id().toString(), a.core.record.id().toString()),
|
||||
values.getAsJsonArray("direct_contributor_character_ids").asList().stream()
|
||||
.map(element -> element.getAsString()).collect(java.util.stream.Collectors.toSet()));
|
||||
assertEquals("10.0", values.getAsJsonObject("kill_location").get("x").getAsString());
|
||||
assertEquals(solo.world.getUID().toString(), values.getAsJsonObject("kill_location").get("world_id").getAsString());
|
||||
assertEquals("ordinary_spacing_eligible", values.get("resolution").getAsString());
|
||||
assertEquals("0", values.get("suppressed_experience").getAsString());
|
||||
assertEquals("0", values.get("suppressed_coins").getAsString());
|
||||
xp = xp.add(new BigInteger(values.get("allocated_experience").getAsString()));
|
||||
coins = coins.add(new BigInteger(values.get("allocated_coins").getAsString()));
|
||||
var actor = actors.stream().filter(value -> value.core.record.id().toString()
|
||||
.equals(values.get("recipient_character_id").getAsString())).findFirst().orElseThrow();
|
||||
var saved = record(f, actor);
|
||||
assertEquals(actor.core.id.toString(), row.get("playerid").getAsString());
|
||||
assertEquals(saved.id().toString(), row.get("characterid").getAsString());
|
||||
assertEquals(saved.revision().toString(), values.get("after_revision").getAsString());
|
||||
assertEquals(saved.experience().toString(), values.get("applied_experience").getAsString());
|
||||
assertEquals("12.0", row.getAsJsonObject("data").getAsJsonObject("location").get("x").getAsString());
|
||||
}
|
||||
assertEquals(BigInteger.valueOf(20), xp); assertEquals(awardedCoins, coins);
|
||||
die(f, witch); settle(f);
|
||||
assertEquals(BigInteger.TEN, record(f, solo).experience());
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
assertEquals(4, rewards(target).size(), "Duplicate native death cannot duplicate shared XP/coin history");
|
||||
}
|
||||
}
|
||||
|
||||
private List<JsonObject> rewards(UUID target) {
|
||||
return CombatRewardEventsNativeTest.rewardRows(data).stream()
|
||||
.filter(row -> row.getAsJsonObject("data").get("target_id").getAsString().equals(target.toString())).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static games.dmg.fantasystick.CombatExperienceNativeTest.*;
|
||||
import static games.dmg.fantasystick.WitchAuditTestSupport.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import games.dmg.fantasystick.events.LocalEventLog;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class WitchRewardReplayNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void actualLockedRecipientFailureKeepsCapturedTwentyXpCoinsReceiptAndPositionThroughRealConfigChangeAndRetry() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor); var before = record(f, actor);
|
||||
var combat = (games.dmg.fantasystick.party.CombatExperience) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "combatExperience");
|
||||
var target = mob(EntityType.WITCH, new Location(actor.world, 0, 64, 0)); var targetId = target.getUniqueId();
|
||||
UUID receipt; BigInteger coins;
|
||||
try (var channel = FileChannel.open(data.resolve("characters/.records.lock"), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
||||
var lock = channel.lock()) {
|
||||
assertTrue(lock.isValid()); hit(f, actor.player, target, false); die(f, target); settle(f);
|
||||
assertEquals(before, record(f, actor)); assertTrue(CombatRewardEventsNativeTest.rewardRows(data).isEmpty());
|
||||
var pending = (Map<?, ?>) FantasyStickLifecycleNativeTest.Fixture.field(combat, "pending");
|
||||
assertEquals(1, pending.size()); var operation = pending.values().iterator().next();
|
||||
receipt = (UUID) FantasyStickLifecycleNativeTest.Fixture.field(operation, "id");
|
||||
coins = (BigInteger) FantasyStickLifecycleNativeTest.Fixture.field(operation, "coins");
|
||||
assertEquals(BigInteger.valueOf(20), FantasyStickLifecycleNativeTest.Fixture.field(operation, "experience"));
|
||||
var sender = new EquipmentAdminCommandNativeTest.Sender();
|
||||
f.adminCommand.execute(sender.port, "fstickadmin", new String[]{"config", "hard-mob-xp", "40"});
|
||||
ReskillEventsNativeTest.await(f, () -> sender.messages.stream().anyMatch(m -> m.startsWith("Saved hard-mob-xp=40")));
|
||||
assertEquals(BigInteger.valueOf(40), combat.rules().pool(games.dmg.fantasystick.progression.CombatRewardRules.Target.HARD, 0));
|
||||
assertEquals(before, record(f, actor)); assertTrue(CombatRewardEventsNativeTest.rewardRows(data).isEmpty());
|
||||
}
|
||||
when(target.getLocation()).thenReturn(new Location(actor.world, 900, 4, 900)); clearInvocations(target);
|
||||
combat.retryPending(); settle(f); awaitRewardCount(f, data, 1);
|
||||
var saved = record(f, actor); assertEquals(before.experience().add(BigInteger.valueOf(20)), saved.experience());
|
||||
assertEquals(before.coins().add(coins), saved.coins()); assertTrue(saved.rewardReceipts().contains(receipt));
|
||||
verifyNoInteractions(target);
|
||||
var values = CombatRewardEventsNativeTest.rewardRows(data).getFirst().getAsJsonObject("data");
|
||||
assertEquals(targetId.toString(), values.get("target_id").getAsString()); assertEquals(receipt.toString(), values.get("reward_receipt").getAsString());
|
||||
assertEquals("20", values.get("hard_mob_xp").getAsString()); assertEquals("20", values.get("applied_experience").getAsString());
|
||||
assertEquals(coins.toString(), values.get("applied_coins").getAsString()); assertEquals("0.0", values.getAsJsonObject("kill_location").get("x").getAsString());
|
||||
die(f, target); combat.retryPending(); settle(f); assertEquals(saved, record(f, actor));
|
||||
var next = mob(EntityType.WITCH, new Location(actor.world, 10, 64, 0));
|
||||
hit(f, actor.player, next, false); die(f, next); settle(f); awaitRewardCount(f, data, 2);
|
||||
assertEquals(saved.experience().add(BigInteger.valueOf(40)), record(f, actor).experience());
|
||||
assertEquals("40", CombatRewardEventsNativeTest.rewardRows(data).getLast().getAsJsonObject("data").get("hard_mob_xp").getAsString());
|
||||
f.plugin.setEnabled(false); f.awaitWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
@Test void rejectedRealWitchExportReplaysOriginalHardEvidenceOfflineWithoutRepaymentOrNativeAccess() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor); var before = record(f, actor);
|
||||
((LocalEventLog) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "eventLog")).close();
|
||||
var target = mob(EntityType.WITCH, new Location(actor.world, 12, 70, -8)); var targetId = target.getUniqueId();
|
||||
hit(f, PartyCombatNativeTest.projectile(f, actor.player), target, false); die(f, target); settle(f);
|
||||
var saved = record(f, actor); assertEquals(before.experience().add(BigInteger.valueOf(20)), saved.experience());
|
||||
die(f, target); settle(f); assertEquals(saved, record(f, actor));
|
||||
f.plugin.setEnabled(false); f.awaitWorkers(); drain(f, "combatRewardAuditDrain"); drain(f, "configurationDrain");
|
||||
var pending = intents(data.resolve("combat-reward-event-intents")); assertEquals(1, pending.size());
|
||||
assertTrue(CombatRewardEventsNativeTest.rewardRows(data).isEmpty()); var inventory = actor.core.snapshot();
|
||||
var feedback = java.util.List.copyOf(actor.messages); var characterBytes = Files.readAllBytes(data.resolve("characters/" + saved.id() + ".yml"));
|
||||
f.players.clear(); clearInvocations(actor.player, target);
|
||||
f.plugin.setEnabled(true); awaitRewardCount(f, data, 1);
|
||||
var storage = (games.dmg.fantasystick.persistence.CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
assertEquals(saved, storage.find(saved.id()).get().orElseThrow()); assertArrayEquals(characterBytes, Files.readAllBytes(data.resolve("characters/" + saved.id() + ".yml")));
|
||||
assertArrayEquals(inventory, actor.core.snapshot()); assertEquals(feedback, actor.messages); verifyNoInteractions(actor.player, target);
|
||||
var row = CombatRewardEventsNativeTest.rewardRows(data).getFirst(); assertOriginal(pending.getFirst(), row);
|
||||
var values = row.getAsJsonObject("data"); assertEquals("hard", values.get("reward_class").getAsString());
|
||||
assertEquals("WITCH", values.get("target_entity_type").getAsString()); assertEquals(targetId.toString(), values.get("target_id").getAsString());
|
||||
assertEquals("native_entity_type_v1", values.get("classifier_version").getAsString()); assertEquals("20", values.get("hard_mob_xp").getAsString());
|
||||
var bridge = (games.dmg.fantasystick.events.CombatRewardEventBridge) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "combatRewardEvents");
|
||||
bridge.replay(); bridge.replay(); f.plugin.setEnabled(false); f.awaitWorkers(); drain(f, "combatRewardAuditDrain");
|
||||
assertEquals(1, CombatRewardEventsNativeTest.rewardRows(data).size()); assertEquals(feedback, actor.messages);
|
||||
}
|
||||
}
|
||||
|
||||
@Test void cancelledNativeWitchAndImpossibleAuditStorageNeverAuthorizeOrInterruptRewards() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor); var before = record(f, actor);
|
||||
var cancelled = mob(EntityType.WITCH, new Location(actor.world, 0, 64, 0));
|
||||
assertTrue(hit(f, actor.player, cancelled, true).isCancelled()); die(f, cancelled); settle(f);
|
||||
assertEquals(before, record(f, actor)); assertTrue(CombatRewardEventsNativeTest.rewardRows(data).isEmpty());
|
||||
// A real invalid intent directory—not a fake successful sink—cannot gate authoritative gameplay.
|
||||
Files.writeString(data.resolve("combat-reward-event-intents"), "not a directory");
|
||||
var accepted = mob(EntityType.WITCH, new Location(actor.world, 0, 64, 0));
|
||||
assertFalse(hit(f, actor.player, accepted, false).isCancelled()); die(f, accepted); settle(f);
|
||||
var saved = record(f, actor); assertEquals(before.experience().add(BigInteger.valueOf(20)), saved.experience());
|
||||
assertTrue(saved.coins().compareTo(before.coins()) > 0); assertTrue(actor.messages.stream().anyMatch(m -> m.contains("Earned 20 fantasy XP")));
|
||||
die(f, accepted); settle(f); assertEquals(saved, record(f, actor));
|
||||
f.plugin.setEnabled(false); f.awaitWorkers(); drain(f, "combatRewardAuditDrain");
|
||||
assertTrue(CombatRewardEventsNativeTest.rewardRows(data).isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user