feat(adventure): export original quest tracking audit events
This commit is contained in:
1 parent
5185501d0d
commit
34e02860c0
10 files changed
+750
-3
No files matched your search
@@ -189,6 +189,8 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
private java.util.concurrent.CompletionStage<Void> combatRewardAuditDrain = java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
private games.dmg.fantasystick.events.StickKnockbackEventBridge stickKnockbackEvents;
|
||||
private java.util.concurrent.CompletionStage<Void> stickKnockbackAuditDrain = java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
private games.dmg.fantasystick.events.QuestTrackingEventBridge questTrackingEvents;
|
||||
private java.util.concurrent.CompletionStage<Void> questTrackingDrain = java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
private games.dmg.fantasystick.events.CharacterBossEventBridge characterBossEvents;
|
||||
private java.util.concurrent.CompletionStage<Void> characterBossDrain = java.util.concurrent.CompletableFuture.completedFuture(null);
|
||||
private games.dmg.fantasystick.events.DungeonAuditEventBridge dungeonOwnerEvents;
|
||||
@@ -929,6 +931,9 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
campAuditEvents = new games.dmg.fantasystick.events.CampEventBridge(eventLog::emit, getDataFolder().toPath(), sessions,
|
||||
failure -> getLogger().log(Level.WARNING, "Camp event intent failed", failure));
|
||||
var dungeonActorCapture = campAuditEvents;
|
||||
questTrackingEvents = new games.dmg.fantasystick.events.QuestTrackingEventBridge(eventLog::emit, getDataFolder().toPath(),
|
||||
sender -> dungeonActorCapture.actor(sender, null), failure -> getLogger().log(Level.WARNING, "Quest tracking event intent failed", failure));
|
||||
adventureQuestController.attachQuestTrackingEvents(questTrackingEvents);
|
||||
dungeonOwnerEvents = new games.dmg.fantasystick.events.DungeonAuditEventBridge(eventLog::emit, getDataFolder().toPath(),
|
||||
sender -> dungeonActorCapture.actor(sender, null), failure -> getLogger().log(Level.WARNING, "Dungeon owner event intent failed", failure));
|
||||
admin.attachDungeonEvents(dungeonOwnerEvents);
|
||||
@@ -1155,6 +1160,11 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Previous hard-mob XP event drain failed", failure);
|
||||
if (replayHardMobXpEvents != null) replayHardMobXpEvents.replay();
|
||||
});
|
||||
var replayQuestTrackingEvents = questTrackingEvents;
|
||||
questTrackingDrain.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Previous quest tracking event drain failed", failure);
|
||||
if (replayQuestTrackingEvents != null) replayQuestTrackingEvents.replay();
|
||||
});
|
||||
var replayCharacterBossEvents = characterBossEvents;
|
||||
characterBossDrain.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Previous character boss event drain failed", failure);
|
||||
@@ -1295,6 +1305,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
var closingGrantEvents = adminExperienceEvents; adminExperienceEvents = null;
|
||||
var closingKnockbackEvents = stickKnockbackEvents; stickKnockbackEvents = null;
|
||||
var closingHardMobXpEvents = hardMobXpAuditEvents; hardMobXpAuditEvents = null;
|
||||
var closingQuestTrackingEvents = questTrackingEvents; questTrackingEvents = null;
|
||||
var closingCharacterBossEvents = characterBossEvents; characterBossEvents = null;
|
||||
var closingDungeonOwnerEvents = dungeonOwnerEvents; dungeonOwnerEvents = null;
|
||||
var closingCampEvents = campAuditEvents; campAuditEvents = null;
|
||||
@@ -1679,6 +1690,14 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
closingHardMobXpEvents.close(); return null;
|
||||
}).thenCompose(ignored -> closingHardMobXpEvents.drained());
|
||||
});
|
||||
if (closingQuestTrackingEvents != null) cleanup("quest tracking event intent drain", () -> {
|
||||
var authorityDrained = closingAdventureQuests == null
|
||||
? java.util.concurrent.CompletableFuture.<Void>completedFuture(null) : closingAdventureQuests.drained();
|
||||
questTrackingDrain = authorityDrained.handle((ignored, failure) -> {
|
||||
if (failure != null) getLogger().log(Level.WARNING, "Quest tracking authority drain failed", failure);
|
||||
closingQuestTrackingEvents.close(); return null;
|
||||
}).thenCompose(ignored -> closingQuestTrackingEvents.drained());
|
||||
});
|
||||
if (closingCharacterBossEvents != null) cleanup("character boss event intent drain", () -> {
|
||||
var dungeonDrained = closingDungeons == null
|
||||
? java.util.concurrent.CompletableFuture.<Void>completedFuture(null) : closingDungeons.drained();
|
||||
|
||||
@@ -105,6 +105,14 @@ public final class AdventureQuestController implements Listener,AutoCloseable {
|
||||
public void attachCampEvents(games.dmg.fantasystick.events.CampEventBridge value){campEvents=Objects.requireNonNull(value);}
|
||||
private games.dmg.fantasystick.events.ChampionEventBridge championEvents;
|
||||
public void attachChampionEvents(games.dmg.fantasystick.events.ChampionEventBridge value){championEvents=Objects.requireNonNull(value);}
|
||||
private games.dmg.fantasystick.events.QuestTrackingEventBridge questTrackingEvents;
|
||||
public void attachQuestTrackingEvents(games.dmg.fantasystick.events.QuestTrackingEventBridge value){questTrackingEvents=Objects.requireNonNull(value);}
|
||||
private games.dmg.fantasystick.events.QuestTrackingEventBridge.Acceptance captureTrackingAcceptance(UUID operation,Player player,Set<UUID> members){
|
||||
var audit=questTrackingEvents;if(audit==null)return null;
|
||||
java.util.Collection<? extends Player> online=List.of();
|
||||
try{online=plugin.getServer().getOnlinePlayers();}catch(RuntimeException unavailable){/* Optional member metadata cannot erase the known initiator. */}
|
||||
try{return audit.acceptance(operation,player,members,online);}catch(RuntimeException unavailable){return null;}
|
||||
}
|
||||
private games.dmg.fantasystick.dungeon.DungeonState.Dungeon captureDungeonDefinition(UUID id){try{return dungeonDefinitions.apply(id).orElse(null);}catch(RuntimeException unavailable){return null;}}
|
||||
private games.dmg.fantasystick.events.DungeonAuditEventBridge dungeonEvents;
|
||||
private java.util.function.Function<UUID,Optional<games.dmg.fantasystick.dungeon.DungeonState.Dungeon>> dungeonDefinitions=ignored->Optional.empty();
|
||||
@@ -130,11 +138,22 @@ public final class AdventureQuestController implements Listener,AutoCloseable {
|
||||
private record Context(EquipmentSessions.Active active,UUID character,UUID partyId,boolean leader,Set<UUID> members){}
|
||||
private Context context(Player player){var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);if(active==null)return null;var record=sessions.currentRecord(active).orElse(null);if(record==null||record.dead()||player.isDead())return null;var party=parties.partyOf(player.getUniqueId()).orElse(null);if(party==null)return new Context(active,record.id(),null,true,Set.of(record.id()));return new Context(active,record.id(),party.id(),party.leaderCharacter().equals(record.id()),Set.copyOf(party.characters()));}
|
||||
@EventHandler(priority=EventPriority.HIGHEST,ignoreCancelled=true)public void interact(PlayerInteractEntityEvent event){if(!giver(event.getRightClicked()))return;event.setCancelled(true);if(event.getHand()!=EquipmentSlot.HAND)return;UUID id=id(event.getRightClicked(),idKey);if(id!=null)directory.findGiver(id).ifPresent(giver->open(event.getPlayer(),0,giver));}
|
||||
@EventHandler(priority=EventPriority.HIGHEST)public void click(InventoryClickEvent event){if(!(event.getInventory().getHolder() instanceof MenuHolder holder))return;event.setCancelled(true);if(!(event.getWhoClicked() instanceof Player player)||event.getCurrentItem()==null)return;var data=event.getCurrentItem().getPersistentDataContainer();Integer page=data.get(menuPageKey,PersistentDataType.INTEGER);if(page!=null){open(player,page,holder.giver);return;}if(data.has(menuFishingKey,PersistentDataType.INTEGER)&&fishing!=null){var now=directory.snapshot();if(now.reset()!=null||now.catalog()==null||!holder.catalog.equals(now.catalog().revision())){player.sendMessage("Quest item entry refused: the Quest Giver menu expired.");return;}fishing.open(player,holder.giver);return;}UUID site=parse(data.get(menuSiteKey,PersistentDataType.STRING)),catalog=parse(data.get(menuCatalogKey,PersistentDataType.STRING));if(site==null||catalog==null||!holder.catalog.equals(catalog))return;var context=context(player);if(context==null){player.sendMessage("Quest action refused: stale fantasy session.");return;}var active=directory.activeFor(context.character,context.partyId).orElse(null);var unclaimed=directory.unclaimed(catalog,site,context.character);if(unclaimed.xp().signum()>0||unclaimed.coins().signum()>0){claim(player,context,catalog,site);return;}if(active!=null){if(!active.site().equals(site)){player.sendMessage("Quest action refused: another quest is active.");return;}if(!event.isRightClick()){player.sendMessage("This quest is active; right-click its Paper to abandon it.");return;}var audit=campEvents;var championAudit=championEvents;var actor=audit==null?null:audit.actor(player,null);var championActor=championQuestActor(actor,holder.giver);UUID operation=UUID.randomUUID();var capturedSite=directory.findSite(active.site()).orElse(null);var dungeonAudit=dungeonEvents;var originalDungeon=captureDungeonDefinition(active.site());directory.abandon(active.id(),active.unit()).whenComplete((changed,failure)->{if(failure==null&&changed){if(audit!=null)audit.questTransition("adventure.quest.abandoned",operation,actor,active,capturedSite,null,"player_abandoned");if(championAudit!=null)championAudit.questTransition("adventure.quest.abandoned",operation,championActor,active,capturedSite,null,"player_abandoned");if(dungeonAudit!=null)try{dungeonAudit.questTransition("adventure.quest.abandoned",operation,championActor,active,capturedSite,originalDungeon,"player_abandoned");}catch(RuntimeException ignored){}}schedule(()->{refreshTracking(player);player.sendMessage(failure==null&&changed?"Adventure quest abandoned.":"Quest abandonment failed; retained state is unchanged.");open(player);});});return;}if(directory.characterClaimed(catalog,site,context.character)){player.sendMessage(directory.rewardClaimed(catalog,site,context.character)?"Adventure rewards were already claimed.":"Adventure complete with no reward owed.");return;}if(!directory.offerOpen(site)||directory.findSite(site).filter(directory::available).isEmpty()){player.sendMessage("Quest action refused: this offer is unavailable.");return;}var offered=directory.findSite(site).orElseThrow();var type=event.isRightClick()?AdventureQuestState.UnitType.PARTY:AdventureQuestState.UnitType.SOLO;if(!offered.participationMode().permits(type)){player.sendMessage("Quest acceptance refused: this site is "+offered.participationMode().value()+".");return;}if(type==AdventureQuestState.UnitType.PARTY&&(context.partyId==null||!context.leader)){player.sendMessage("Only the active party leader may accept a party quest.");return;}var unit=type==AdventureQuestState.UnitType.SOLO?context.character:context.partyId;var members=type==AdventureQuestState.UnitType.SOLO?Set.of(context.character):context.members;var campAudit=campEvents;var championAudit=championEvents;var campActor=campAudit==null?null:campAudit.actor(player,null);var championActor=championQuestActor(campActor,holder.giver);var dungeonAudit=dungeonEvents;var originalDungeon=captureDungeonDefinition(site);UUID operation=UUID.randomUUID();directory.accept(catalog,site,type,unit,members,offered.participationMode()).whenComplete((result,failure)->schedule(()->{if(failure!=null){player.sendMessage("Quest acceptance failed; retained state is unchanged.");return;}if(result.status()!=AdventureQuestDirectory.Acceptance.APPLIED){player.sendMessage("Quest acceptance refused: "+result.status().name().toLowerCase(Locale.ROOT)+".");return;}var quest=result.quest().orElseThrow();var current=context(player);boolean same=current!=null&¤t.active.equals(context.active)&¤t.character.equals(context.character)&&(type==AdventureQuestState.UnitType.SOLO||current.leader&&Objects.equals(current.partyId,context.partyId)&¤t.members.equals(context.members));if(!same){directory.abandon(quest.id(),quest.unit()).whenComplete((ignored,rollbackFailure)->schedule(()->player.sendMessage(rollbackFailure==null?"Quest acceptance refused: the character or party chLine truncated
|
||||
@EventHandler(priority=EventPriority.HIGHEST)public void click(InventoryClickEvent event){if(!(event.getInventory().getHolder() instanceof MenuHolder holder))return;event.setCancelled(true);if(!(event.getWhoClicked() instanceof Player player)||event.getCurrentItem()==null)return;var data=event.getCurrentItem().getPersistentDataContainer();Integer page=data.get(menuPageKey,PersistentDataType.INTEGER);if(page!=null){open(player,page,holder.giver);return;}if(data.has(menuFishingKey,PersistentDataType.INTEGER)&&fishing!=null){var now=directory.snapshot();if(now.reset()!=null||now.catalog()==null||!holder.catalog.equals(now.catalog().revision())){player.sendMessage("Quest item entry refused: the Quest Giver menu expired.");return;}fishing.open(player,holder.giver);return;}UUID site=parse(data.get(menuSiteKey,PersistentDataType.STRING)),catalog=parse(data.get(menuCatalogKey,PersistentDataType.STRING));if(site==null||catalog==null||!holder.catalog.equals(catalog))return;var context=context(player);if(context==null){player.sendMessage("Quest action refused: stale fantasy session.");return;}var active=directory.activeFor(context.character,context.partyId).orElse(null);var unclaimed=directory.unclaimed(catalog,site,context.character);if(unclaimed.xp().signum()>0||unclaimed.coins().signum()>0){claim(player,context,catalog,site);return;}if(active!=null){if(!active.site().equals(site)){player.sendMessage("Quest action refused: another quest is active.");return;}if(!event.isRightClick()){player.sendMessage("This quest is active; right-click its Paper to abandon it.");return;}var audit=campEvents;var championAudit=championEvents;var actor=audit==null?null:audit.actor(player,null);var championActor=championQuestActor(actor,holder.giver);UUID operation=UUID.randomUUID();var capturedSite=directory.findSite(active.site()).orElse(null);var dungeonAudit=dungeonEvents;var originalDungeon=captureDungeonDefinition(active.site());directory.abandon(active.id(),active.unit()).whenComplete((changed,failure)->{if(failure==null&&changed){if(audit!=null)audit.questTransition("adventure.quest.abandoned",operation,actor,active,capturedSite,null,"player_abandoned");if(championAudit!=null)championAudit.questTransition("adventure.quest.abandoned",operation,championActor,active,capturedSite,null,"player_abandoned");if(dungeonAudit!=null)try{dungeonAudit.questTransition("adventure.quest.abandoned",operation,championActor,active,capturedSite,originalDungeon,"player_abandoned");}catch(RuntimeException ignored){}}schedule(()->{refreshTracking(player);player.sendMessage(failure==null&&changed?"Adventure quest abandoned.":"Quest abandonment failed; retained state is unchanged.");open(player);});});return;}if(directory.characterClaimed(catalog,site,context.character)){player.sendMessage(directory.rewardClaimed(catalog,site,context.character)?"Adventure rewards were already claimed.":"Adventure complete with no reward owed.");return;}if(!directory.offerOpen(site)||directory.findSite(site).filter(directory::available).isEmpty()){player.sendMessage("Quest action refused: this offer is unavailable.");return;}var offered=directory.findSite(site).orElseThrow();var type=event.isRightClick()?AdventureQuestState.UnitType.PARTY:AdventureQuestState.UnitType.SOLO;if(!offered.participationMode().permits(type)){player.sendMessage("Quest acceptance refused: this site is "+offered.participationMode().value()+".");return;}if(type==AdventureQuestState.UnitType.PARTY&&(context.partyId==null||!context.leader)){player.sendMessage("Only the active party leader may accept a party quest.");return;}var unit=type==AdventureQuestState.UnitType.SOLO?context.character:context.partyId;var members=type==AdventureQuestState.UnitType.SOLO?Set.of(context.character):context.members;var campAudit=campEvents;var championAudit=championEvents;var campActor=campAudit==null?null:campAudit.actor(player,null);var championActor=championQuestActor(campActor,holder.giver);var dungeonAudit=dungeonEvents;var originalDungeon=captureDungeonDefinition(site);UUID operation=UUID.randomUUID();var trackingAudit=questTrackingEvents;var trackingAcceptance=captureTrackingAcceptance(operation,player,members);directory.accept(catalog,site,type,unit,members,offered.participationMode()).whenComplete((result,failure)->{if(failure==null&&result.status()==AdventureQuestDirectory.Acceptance.APPLIED&&trackingAudit!=null&&trackingAcceptance!=null)try{trackingAudit.accepted(trackingAcceptance,result.quest().orElseThrow());}catch(RuntimeException ignored){}schedule(()->{if(failure!=null){player.sendMessage("Quest acceptance failed; retained state is unchanged.");return;}if(result.status()!=AdventureQuestDirectory.Acceptance.APPLIED){player.sendMessage("Quest acceptance refused: "+result.status().name().toLowerCase(Locale.ROOT)+".");return;}var quest=result.quest().orElseThrow();var current=context(player);boolean same=current!=null&¤t.active.equals(context.active)&¤t.character.equals(context.character)Line truncated
|
||||
private void claim(Player player,Context expected,UUID catalog,UUID site){directory.beginClaim(catalog,site,expected.character).whenComplete((operation,failure)->schedule(()->{if(failure!=null||operation.isEmpty()){player.sendMessage("Reward claim failed; retained rewards are unchanged.");return;}var current=context(player);if(current==null||!current.active.equals(expected.active)||!current.character.equals(operation.orElseThrow().character())){player.sendMessage("Reward claim paused: actively play the exact living character and try again.");return;}var record=sessions.currentRecord(current.active).orElse(null);if(record==null){player.sendMessage("Reward claim paused: the current character changed.");return;}attemptClaim(player,current.active,operation.orElseThrow(),record);}));}
|
||||
private void attemptClaim(Player player,EquipmentSessions.Active active,AdventureQuestState.RewardClaim operation,games.dmg.fantasystick.persistence.CharacterRecord record){characters.awardCombatReward(record.id(),record.revision(),operation.id(),operation.xp(),operation.coins(),progression,games.dmg.fantasystick.progression.CombatRewardRules.defaults(),null).whenComplete((award,failure)->{if(failure!=null){schedule(()->player.sendMessage("Reward claim failed; retained rewards are unchanged."));return;}if(award.status()==CharacterStorage.CombatAwardStatus.STALE&&award.record().isPresent()){attemptClaim(player,active,operation,award.record().orElseThrow());return;}if(award.record().isEmpty()){schedule(()->player.sendMessage("Reward claim failed; retained rewards are unchanged."));return;}var saved=award.record().orElseThrow();directory.finishClaim(operation.id()).whenComplete((finished,finishFailure)->schedule(()->{if(finishFailure!=null||!finished){player.sendMessage("Reward claim is pending recovery; retry at a Quest Giver.");return;}var holder=sessions.activeCharacter(saved.id()).filter(sessions::current).orElse(null);if(award.status()!=CharacterStorage.CombatAwardStatus.DUPLICATE&&holder!=null)sessions.acknowledgeProgress(holder,saved.revision().subtract(BigInteger.ONE),saved);var currentPlayer=holder==null?null:plugin.getServer().getPlayer(holder.player());var current=currentPlayer==null?null:context(currentPlayer);if(current!=null&¤t.character.equals(operation.character())){currentPlayer.sendMessage("Claimed "+operation.xp()+" fantasy XP and "+operation.coins()+" character coins.");open(currentPlayer);}}));});}
|
||||
public boolean trackingAvailable(Player player){var context=context(player);return context!=null&&directory.activeFor(context.character,context.partyId).filter(quest->quest.snapshot().contains(context.character)).isPresent();}
|
||||
public void tracking(Player player,Boolean enabled){var context=context(player);if(context==null){player.sendMessage("Quest tracking refused: actively play a living fantasy character first.");return;}var quest=directory.activeFor(context.character,context.partyId).filter(value->value.snapshot().contains(context.character)).orElse(null);if(quest==null){player.sendMessage("Quest tracking refused: this character has no eligible active quest.");refreshTracking(player);return;}if(enabled==null){player.sendMessage("Quest tracking is "+(directory.trackingEnabled(quest.id(),context.character)?"on":"off")+" for the current adventure quest.");return;}directory.setTracking(quest.id(),context.character,enabled).whenComplete((status,failure)->schedule(()->{if(failure!=null||status==AdventureQuestDirectory.TrackingStatus.INELIGIBLE){player.sendMessage("Quest tracking change failed; retained state is unchanged.");return;}refreshTracking(player);player.sendMessage("Quest tracking is now "+(enabled?"on.":"off."));}));}
|
||||
public void tracking(Player player,Boolean enabled){
|
||||
var audit=questTrackingEvents;var initial=audit==null?null:audit.request(player,enabled);
|
||||
var context=context(player);if(context==null){if(audit!=null)audit.refused(initial,"inactive_or_dead_character");player.sendMessage("Quest tracking refused: actively play a living fantasy character first.");return;}
|
||||
var quest=directory.activeFor(context.character,context.partyId).filter(value->value.snapshot().contains(context.character)).orElse(null);
|
||||
if(quest==null){if(audit!=null)audit.refused(initial,"no_eligible_active_quest");player.sendMessage("Quest tracking refused: this character has no eligible active quest.");refreshTracking(player);return;}
|
||||
var request=audit==null?null:audit.bind(initial,quest,context.character);if(audit!=null)audit.observed(request);
|
||||
if(enabled==null){boolean value=directory.trackingEnabled(quest.id(),context.character);if(audit!=null)audit.read(request,value,directory.snapshot().tracking().stream().anyMatch(entry->entry.quest().equals(quest.id())&&entry.character().equals(context.character)));player.sendMessage("Quest tracking is "+(value?"on":"off")+" for the current adventure quest.");return;}
|
||||
directory.setTrackingWithEvidence(quest.id(),context.character,enabled).whenComplete((publication,failure)->{
|
||||
if(audit!=null)try{audit.changed(request,publication,failure);}catch(RuntimeException ignored){}
|
||||
schedule(()->{var status=publication==null?null:publication.status();if(failure!=null||status==AdventureQuestDirectory.TrackingStatus.INELIGIBLE){player.sendMessage("Quest tracking change failed; retained state is unchanged.");return;}refreshTracking(player);player.sendMessage("Quest tracking is now "+(enabled?"on.":"off."));});
|
||||
});
|
||||
}
|
||||
public void inspect(Player player){var context=context(player);if(context==null){player.sendMessage("No active fantasy character.");return;}var quest=directory.activeFor(context.character,context.partyId).orElse(null);if(quest==null){player.sendMessage("No active adventure quest.");return;}var site=directory.findSite(quest.site()).orElse(null);if(site==null){player.sendMessage("Active quest site is unavailable.");return;}player.sendMessage(site.displayName()+"; state="+(attempts.containsKey(site.id())?"active":"waiting")+"; expires "+quest.expiresAt()+"; Quest Compass guidance="+(directory.trackingEnabled(quest.id(),context.character)?"on":"off")+".");}
|
||||
private void pulse(){if(closed)return;directory.ensureToday().whenComplete((catalog,failure)->{if(failure==null)schedule(()->{if(directory.snapshot().reset()!=null)publishPendingReset();});});if(directory.snapshot().reset()!=null){publishPendingReset();return;}directory.expireParties(parties::exists);refreshTrackingPlayers(true);showArenaBoundaries();for(var attempt:new ArrayList<>(attempts.values())){var catalog=directory.snapshot().catalog();if(catalog==null||!catalog.revision().equals(attempt.catalog)||!clock.instant().isBefore(catalog.expiresAt())){cleanup(attempt,"catalog_or_utc_expired");attempts.remove(attempt.site);readyAt.remove(attempt.site);continue;}contain(attempt);syncBossBar(attempt);boolean present=eligibleInside(attempt.site,attempt.arena).findAny().isPresent();if(present)attempt.absentSince=null;else if(attempt.absentSince==null)attempt.absentSince=clock.instant();else if(!clock.instant().isBefore(attempt.absentSince.plusSeconds(attempt.resetSeconds))){cleanup(attempt,"no_eligible_accepted_character_remained");attempts.remove(attempt.site);readyAt.put(attempt.site,clock.instant());}}for(var quest:directory.active()){var type=directory.findSite(quest.site()).map(AdventureQuestState.Site::type).orElse(null);if(type==AdventureQuestState.SiteType.DUNGEON||type==AdventureQuestState.SiteType.FISHING)continue;if(attempts.containsKey(quest.site())||starting.contains(quest.site()))continue;var ready=readyAt.get(quest.site());if(ready!=null&&clock.instant().isBefore(ready))continue;var activation=eligibleInside(quest.site()).findAny();if(activation.isPresent())start(quest.site(),quest.catalog(),activation.orElseThrow());}}
|
||||
private void showArenaBoundaries(){for(UUID siteId:directory.active().stream().map(AdventureQuestState.Quest::site).distinct().toList()){var site=directory.findSite(siteId).orElse(null);if(site==null||site.type()==AdventureQuestState.SiteType.DUNGEON)continue;var world=plugin.getServer().getWorld(site.world());if(world==null)continue;var center=new Location(world,site.x(),site.y(),site.z());var points=perimeter(arena(site));for(var player:plugin.getServer().getOnlinePlayers()){if(eligible(player,siteId)==null)continue;var viewer=player.getLocation();if(viewer.getWorld()==null||!viewer.getWorld().getUID().equals(site.world())||viewer.distanceSquared(center)>64d*64d)continue;for(var point:points)particles.viewer(player,Particle.END_ROD,new Location(world,site.x()+point[0],site.y()+0.2,site.z()+point[1]),1,0,0,0,0);}}}
|
||||
|
||||
@@ -17,6 +17,18 @@ public final class AdventureQuestDirectory implements AutoCloseable {
|
||||
public enum FishingUpdateStatus { APPLIED, UNCHANGED, STALE, NOT_FOUND }
|
||||
public enum RerollStatus { APPLIED, STALE }
|
||||
public enum TrackingStatus { APPLIED, UNCHANGED, INELIGIBLE }
|
||||
/** Facts from this serialized mutation, never a request-time or later snapshot. */
|
||||
public record TrackingPublication(TrackingStatus status,boolean persistedBefore,Boolean previous,Boolean saved){}
|
||||
/** A failed write retains actual worker-observed before facts, never its candidate saved value. */
|
||||
public static final class TrackingFailure extends IOException {
|
||||
private static final long serialVersionUID=1L;
|
||||
private final transient TrackingPublication before;
|
||||
private TrackingFailure(TrackingPublication candidate,Throwable cause){
|
||||
super("Quest tracking source operation failed",cause);
|
||||
before=candidate==null?null:new TrackingPublication(candidate.status(),candidate.persistedBefore(),candidate.previous(),null);
|
||||
}
|
||||
public TrackingPublication before(){return before;}
|
||||
}
|
||||
public record CatalogPublication(AdventureQuestState.Catalog catalog,UUID operation,String reason,
|
||||
AdventureQuestSettings.Rules settings,List<AdventureQuestState.Site> selected,
|
||||
games.dmg.fantasystick.events.CampEventBridge.Actor actor,boolean originalCapture) {
|
||||
@@ -95,7 +107,25 @@ public final class AdventureQuestDirectory implements AutoCloseable {
|
||||
public boolean claimed(UUID catalog,UUID site,UUID character,UUID party){var state=snapshot;return state.characterClaims().stream().anyMatch(claim->claim.catalog().equals(catalog)&&claim.site().equals(site)&&claim.character().equals(character))||(party!=null&&state.unitClaims().stream().anyMatch(claim->claim.catalog().equals(catalog)&&claim.site().equals(site)&&claim.type()==AdventureQuestState.UnitType.PARTY&&claim.unit().equals(party)));}
|
||||
public Optional<AdventureQuestState.Quest> activeFor(UUID character,UUID party){return active().stream().filter(q->q.unitType()==AdventureQuestState.UnitType.SOLO?q.unit().equals(character):party!=null&&q.unit().equals(party)&&q.snapshot().contains(character)).findFirst();}
|
||||
public boolean trackingEnabled(UUID quest,UUID character){var active=findQuest(quest).filter(value->value.snapshot().contains(character));if(active.isEmpty())return false;return snapshot.tracking().stream().filter(value->value.quest().equals(quest)&&value.character().equals(character)).map(AdventureQuestState.Tracking::enabled).findFirst().orElse(true);}
|
||||
public CompletableFuture<TrackingStatus> setTracking(UUID quest,UUID character,boolean enabled){return mutate(original->{var state=expire(original);var active=state.quests().stream().filter(value->value.id().equals(quest)&&value.snapshot().contains(character)).findFirst();if(active.isEmpty())return result(state,TrackingStatus.INELIGIBLE);var current=state.tracking().stream().filter(value->value.quest().equals(quest)&&value.character().equals(character)).findFirst();if(current.map(AdventureQuestState.Tracking::enabled).orElse(true)==enabled&¤t.isPresent())return result(state,TrackingStatus.UNCHANGED);var tracking=new HashSet<>(state.tracking());tracking.removeIf(value->value.quest().equals(quest)&&value.character().equals(character));tracking.add(new AdventureQuestState.Tracking(quest,character,enabled));return result(copyTracking(state,state.sites(),state.givers(),state.catalog(),state.quests(),state.unitClaims(),state.characterClaims(),state.rosters(),tracking),TrackingStatus.APPLIED);});}
|
||||
public CompletableFuture<TrackingStatus> setTracking(UUID quest,UUID character,boolean enabled){
|
||||
var future=new CompletableFuture<TrackingStatus>();setTrackingWithEvidence(quest,character,enabled).whenComplete((publication,failure)->{
|
||||
if(failure==null)future.complete(publication.status());else future.completeExceptionally(failure instanceof TrackingFailure observed?observed.getCause():failure);
|
||||
});return future;
|
||||
}
|
||||
public CompletableFuture<TrackingPublication> setTrackingWithEvidence(UUID quest,UUID character,boolean enabled){
|
||||
var captured=new java.util.concurrent.atomic.AtomicReference<TrackingPublication>();
|
||||
var future=new CompletableFuture<TrackingPublication>();
|
||||
this.<TrackingPublication>mutate(original->{
|
||||
var state=expire(original);var active=state.quests().stream().filter(value->value.id().equals(quest)&&value.snapshot().contains(character)).findFirst();
|
||||
if(active.isEmpty()){var publication=new TrackingPublication(TrackingStatus.INELIGIBLE,false,null,null);captured.set(publication);return result(state,publication);}
|
||||
var current=state.tracking().stream().filter(value->value.quest().equals(quest)&&value.character().equals(character)).findFirst();boolean before=current.map(AdventureQuestState.Tracking::enabled).orElse(true);
|
||||
if(before==enabled&¤t.isPresent()){var publication=new TrackingPublication(TrackingStatus.UNCHANGED,true,before,before);captured.set(publication);return result(state,publication);}
|
||||
var tracking=new HashSet<>(state.tracking());tracking.removeIf(value->value.quest().equals(quest)&&value.character().equals(character));tracking.add(new AdventureQuestState.Tracking(quest,character,enabled));
|
||||
var publication=new TrackingPublication(TrackingStatus.APPLIED,current.isPresent(),before,enabled);captured.set(publication);
|
||||
return result(copyTracking(state,state.sites(),state.givers(),state.catalog(),state.quests(),state.unitClaims(),state.characterClaims(),state.rosters(),tracking),publication);
|
||||
}).whenComplete((publication,failure)->{if(failure==null)future.complete(publication);else future.completeExceptionally(new TrackingFailure(captured.get(),failure));});
|
||||
return future;
|
||||
}
|
||||
public CompletableFuture<Boolean> bank(UUID quest,UUID source,Map<UUID,Reward> rewards){return bank(quest,source,rewards,false);}
|
||||
public CompletableFuture<Boolean> bankAndComplete(UUID quest,UUID source,Map<UUID,Reward> rewards){return bank(quest,source,rewards,true);}
|
||||
private CompletableFuture<Boolean> bank(UUID questId,UUID source,Map<UUID,Reward> rewards,boolean complete){Objects.requireNonNull(source);var requested=Map.copyOf(rewards);return mutate(original->{var state=expire(original);var quest=state.quests().stream().filter(value->value.id().equals(questId)).findFirst().orElse(null);if(quest==null||!quest.snapshot().containsAll(requested.keySet())||complete&&requested.isEmpty())return result(state,false);var allocations=new ArrayList<>(state.rewards().allocations());for(var entry:requested.entrySet()){var reward=Objects.requireNonNull(entry.getValue());if(reward.xp().signum()==0&&reward.coins().signum()==0)continue;UUID id=rewardId(quest,source,entry.getKey());var existing=allocations.stream().filter(value->value.id().equals(id)).findFirst().orElse(null);if(existing!=null){if(!existing.xp().equals(reward.xp())||!existing.coins().equals(reward.coins()))throw new IllegalStateException("Conflicting adventure reward allocation");continue;}allocations.add(new AdventureQuestState.RewardAllocation(id,quest.catalog(),quest.site(),quest.id(),source,entry.getKey(),reward.xp(),reward.coins()));}var quests=state.quests();var units=state.unitClaims();var characters=state.characterClaims();if(complete){quests=state.quests().stream().filter(value->!value.id().equals(quest.id())).toList();var changedUnits=new HashSet<>(units);changedUnits.add(new AdventureQuestState.UnitClaim(quest.catalog(),quest.site(),quest.unitType(),quest.unit()));units=Set.copyOf(changedUnits);var changedCharacters=new HashSet<>(characters);for(UUID character:requested.keySet())changedCharacters.add(new AdventureQuestState.CharacterClaim(quest.catalog(),quest.site(),character));characters=Set.copyOf(changedCharacters);}var ledger=new AdventureQuestState.RewardLedger(allocations,state.rewards().operations(),state.rewards().receipts());return result(copyRewards(state,state.sites(),state.givers(),state.catalog(),quests,units,characters,state.rosters(),ledger),true);});}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import games.dmg.fantasystick.adventure.AdventureQuestDirectory;
|
||||
import games.dmg.fantasystick.adventure.AdventureQuestState;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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;
|
||||
|
||||
/** Original per-character/quest preference facts; persistence and presentation remain their own authority. */
|
||||
public final class QuestTrackingEventBridge implements AutoCloseable {
|
||||
public record Request(UUID action,Instant time,CampEventBridge.Actor actor,AdventureQuestState.Quest quest,
|
||||
UUID character,Boolean requested) { }
|
||||
public record Acceptance(UUID action,Instant time,CampEventBridge.Actor initiator,Map<UUID,CampEventBridge.Actor> owners) {
|
||||
public Acceptance { owners=Map.copyOf(owners); }
|
||||
}
|
||||
private final FeatureEventOutbox outbox;
|
||||
private final Function<CommandSender,CampEventBridge.Actor> capture;
|
||||
public QuestTrackingEventBridge(Function<EventInput,CompletionStage<LocalEventLog.Delivery>> sink,Path data,
|
||||
Function<CommandSender,CampEventBridge.Actor> capture,Consumer<Throwable> failures){
|
||||
this.capture=Objects.requireNonNull(capture);
|
||||
outbox=new FeatureEventOutbox(data,"quest-tracking-event-intents",Set.of("preference.read","preference.changed","action.observed","action.resolved"),sink,failures);
|
||||
}
|
||||
public Request request(Player player,Boolean requested){return new Request(UUID.randomUUID(),Instant.now(),actor(player),null,null,requested);}
|
||||
public Request bind(Request request,AdventureQuestState.Quest quest,UUID character){
|
||||
return new Request(request.action(),request.time(),request.actor(),quest,character,request.requested());
|
||||
}
|
||||
public CampEventBridge.Actor actor(CommandSender sender){
|
||||
try{var value=capture.apply(sender);if(value!=null)return value;}catch(RuntimeException ignored){}
|
||||
EventInput.Context context=null;
|
||||
if(sender instanceof Player player)try{context=new EventInput.Context(player.getUniqueId(),player.getName(),null,null,null,null);}catch(RuntimeException ignored){}
|
||||
return new CampEventBridge.Actor(context,Map.of("actor_kind",sender instanceof Player?"player":"console","actor_capture_quality","optional_capture_unavailable"));
|
||||
}
|
||||
/** All native holder facts are captured before the acceptance worker; absent holders stay absent. */
|
||||
public Acceptance acceptance(UUID operation,Player initiator,Set<UUID> members,Collection<? extends Player> online){
|
||||
var original=actor(initiator);var owners=new LinkedHashMap<UUID,CampEventBridge.Actor>();
|
||||
if(original.context()!=null&&original.context().characterId()!=null&&members.contains(original.context().characterId()))owners.put(original.context().characterId(),original);
|
||||
for(var player:online)try{var value=actor(player);var context=value.context();if(context!=null&&context.characterId()!=null&&members.contains(context.characterId()))owners.putIfAbsent(context.characterId(),value);}catch(RuntimeException ignored){}
|
||||
return new Acceptance(operation,Instant.now(),original,owners);
|
||||
}
|
||||
public void accepted(Acceptance acceptance,AdventureQuestState.Quest quest){
|
||||
for(var character:quest.snapshot().stream().sorted().toList()){
|
||||
var owner=acceptance.owners().get(character);
|
||||
var action=UUID.nameUUIDFromBytes((acceptance.action()+":"+character).getBytes(StandardCharsets.UTF_8));
|
||||
var request=new Request(action,acceptance.time(),owner,quest,character,true);
|
||||
var values=common(request);values.put("origin","quest_acceptance");values.put("previous_status","absent");
|
||||
values.put("saved_value",true);values.put("outcome","acknowledged");values.put("source_acknowledgement","durable_quest_acceptance");
|
||||
values.put("origin_acceptance_action_id",acceptance.action().toString());values.put("actor_role","preference_owner");
|
||||
values.put("preference_owner_actor_status",owner==null?"unavailable_at_acceptance":"captured_original_holder");
|
||||
var initiator=acceptance.initiator().context();
|
||||
if(initiator!=null){values.put("originating_account_id",initiator.playerId().toString());if(initiator.characterId()!=null){values.put("originating_character_id",initiator.characterId().toString());values.put("originating_session_id",initiator.sessionId().toString());}}
|
||||
putActionLocation(values,acceptance.initiator());
|
||||
emit("preference.changed",request,values);
|
||||
}
|
||||
}
|
||||
public void observed(Request request){var values=common(request);values.put("outcome","observed");emit("action.observed",request,values);}
|
||||
public void refused(Request request,String reason){observed(request);resolved(request,"refused",reason,null);}
|
||||
public void read(Request request,boolean value,boolean persisted){
|
||||
var values=common(request);values.put("previous_status",persisted?"persisted":"legacy_default");values.put("previous_value",value);
|
||||
values.put("saved_value",value);values.put("outcome","acknowledged_read");emit("preference.read",request,values);
|
||||
resolved(request,"acknowledged_read",null,null);
|
||||
}
|
||||
public void changed(Request request,AdventureQuestDirectory.TrackingPublication publication,Throwable failure){
|
||||
if(failure!=null){
|
||||
var values=common(request);var proof=failure instanceof AdventureQuestDirectory.TrackingFailure observed?observed.before():null;
|
||||
if(proof!=null)publication(values,proof);else values.put("previous_status","unavailable_before_source_decision");
|
||||
values.put("outcome","failed");values.put("reason","source_operation_failed");values.put("saved_value_status","not_acknowledged");
|
||||
emit("action.resolved",request,values);return;
|
||||
}
|
||||
if(publication.status()==AdventureQuestDirectory.TrackingStatus.INELIGIBLE){resolved(request,"stale","quest_no_longer_eligible",publication);return;}
|
||||
if(publication.status()==AdventureQuestDirectory.TrackingStatus.UNCHANGED){resolved(request,"unchanged",null,publication);return;}
|
||||
var values=common(request);publication(values,publication);values.put("outcome","acknowledged");
|
||||
values.put("source_acknowledgement","durable_quest_tracking_save");emit("preference.changed",request,values);
|
||||
resolved(request,"acknowledged",null,publication);
|
||||
}
|
||||
private void resolved(Request request,String outcome,String reason,AdventureQuestDirectory.TrackingPublication publication){
|
||||
var values=common(request);if(publication!=null)publication(values,publication);values.put("outcome",outcome);if(reason!=null)values.put("reason",reason);emit("action.resolved",request,values);
|
||||
}
|
||||
private static void publication(Map<String,Object> values,AdventureQuestDirectory.TrackingPublication publication){
|
||||
values.put("previous_status",publication.previous()==null?"not_applicable_no_eligible_quest":publication.persistedBefore()?"persisted":"legacy_default");
|
||||
if(publication.previous()!=null)values.put("previous_value",publication.previous());
|
||||
if(publication.saved()!=null)values.put("saved_value",publication.saved());
|
||||
}
|
||||
private static Map<String,Object> common(Request request){
|
||||
var values=new LinkedHashMap<String,Object>();var actor=request.actor();
|
||||
if(actor!=null)values.putAll(actor.captured());
|
||||
if(actor==null||actor.context()==null){
|
||||
var original=values.getOrDefault("actor_kind","unavailable");values.put("original_actor_kind",original);
|
||||
values.put("actor_kind","console".equals(original)||"command_block".equals(original)?original:"system");
|
||||
values.put("actor_identity_status","unavailable_original_context");
|
||||
}
|
||||
values.put("preference_key","quest-tracking");values.put("scope","character_quest");values.put("origin","command");
|
||||
values.put("operation_id",request.action().toString());values.put("source_revision_status","no_persisted_tracking_revision");
|
||||
if(request.requested()!=null)values.put("requested_value",request.requested());else values.put("requested_value_status","not_applicable_query");
|
||||
if(request.character()!=null)values.put("preference_owner_character_id",request.character().toString());
|
||||
if(request.quest()!=null){var quest=request.quest();values.put("quest_id",quest.id().toString());values.put("site_id",quest.site().toString());values.put("catalog_id",quest.catalog().toString());values.put("deadline",quest.expiresAt().toString());}
|
||||
putActionLocation(values,actor);return values;
|
||||
}
|
||||
private static void putActionLocation(Map<String,Object> values,CampEventBridge.Actor actor){
|
||||
var context=actor==null?null:actor.context();var at=context==null?null:context.location();
|
||||
if(at==null){
|
||||
var partial=actor==null?null:actor.captured().get("actor_location");
|
||||
if(partial instanceof Map<?,?>){values.put("action_location",partial);values.put("action_location_status","partial_original_capture");}
|
||||
else values.put("action_location_status","unavailable");
|
||||
return;
|
||||
}
|
||||
values.put("action_location_status","native");
|
||||
values.put("action_location",Map.of("world_id",at.worldId().toString(),"world_name",at.worldName(),"x",at.x(),"y",at.y(),"z",at.z(),"captured_at",at.capturedAt().toString(),"quality",at.quality()));
|
||||
}
|
||||
private void emit(String type,Request request,Map<String,Object> values){
|
||||
try{var id=UUID.nameUUIDFromBytes(("fantasystick:quest-tracking:"+request.action()+":"+type).getBytes(StandardCharsets.UTF_8));
|
||||
outbox.accepted(new EventInput(id,request.action(),request.time(),type,request.actor()==null?null:request.actor().context(),values));
|
||||
}catch(RuntimeException ignored){/* Audit cannot authorize or interrupt preference publication. */}
|
||||
}
|
||||
public void replay(){outbox.replay();}
|
||||
@Override public void close(){outbox.close();}
|
||||
public CompletionStage<Void> drained(){return outbox.drained();}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import games.dmg.fantasystick.adventure.AdventureQuestController;
|
||||
import games.dmg.fantasystick.adventure.AdventureQuestDirectory;
|
||||
import java.nio.file.Path;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
|
||||
/** Reuses the strict installed camp/giver ports and waits only this owner's disposal boundary. */
|
||||
final class QuestTrackingAuditNativeFixture implements AutoCloseable {
|
||||
final CampAuditNativeFixture base;
|
||||
final FantasyStickLifecycleNativeTest.Fixture f;
|
||||
final FantasyStickLifecycleNativeTest.Actor actor;
|
||||
final AdventureQuestDirectory directory;
|
||||
final AdventureQuestController controller;
|
||||
QuestTrackingAuditNativeFixture(Path data)throws Exception{
|
||||
base=new CampAuditNativeFixture(data);f=base.f;actor=base.actor;directory=base.directory;controller=base.controller;
|
||||
}
|
||||
void camp(String mode)throws Exception{base.camp(mode);}
|
||||
void giver()throws Exception{base.giver();}
|
||||
void resetCatalog()throws Exception{base.resetCatalog();}
|
||||
Inventory open(FantasyStickLifecycleNativeTest.Actor holder)throws Exception{return base.open(holder);}
|
||||
void click(FantasyStickLifecycleNativeTest.Actor holder,Inventory menu,boolean right){base.click(holder,menu,right);}
|
||||
void pulse(){base.pulse();}
|
||||
/** One feature-local failing getter; every other server call delegates to the existing strict port. */
|
||||
void withoutOnlineMetadata(Runnable action)throws Exception{
|
||||
var field=org.bukkit.plugin.java.JavaPlugin.class.getDeclaredField("server");field.setAccessible(true);
|
||||
var original=field.get(f.plugin);
|
||||
var server=(org.bukkit.Server)java.lang.reflect.Proxy.newProxyInstance(org.bukkit.Server.class.getClassLoader(),new Class<?>[]{org.bukkit.Server.class},(proxy,method,args)->{
|
||||
if(method.getName().equals("getOnlinePlayers")&&method.getParameterCount()==0)throw new IllegalStateException("test-only optional online actor snapshot unavailable");
|
||||
try{return method.invoke(original,args);}catch(java.lang.reflect.InvocationTargetException failure){throw failure.getCause();}
|
||||
});
|
||||
field.set(f.plugin,server);try{action.run();}finally{field.set(f.plugin,original);}
|
||||
}
|
||||
@Override public void close()throws java.io.IOException{
|
||||
try{base.close();}finally{
|
||||
try{WitchAuditTestSupport.drain(f,"questTrackingDrain");}
|
||||
catch(Exception failure){throw new java.io.IOException("Quest tracking owner disposal did not drain",failure);}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static games.dmg.fantasystick.QuestTrackingEventsNativeTest.*;
|
||||
import static games.dmg.fantasystick.QuestTrackingSafetyEventsNativeTest.acceptSolo;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/** Existing legacy and best-effort authority controls, not newly delivered gameplay policy. */
|
||||
class QuestTrackingBoundaryControlsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap()throws Exception{NativeRuntime.bootstrap();}
|
||||
|
||||
@Test void actualLegacyMissingTrackingEntryLoadsByteExactlyReadsDefaultAndMaterializesOnlyOnAcknowledgedNativeCommand()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;var actor=fixture.actor;var quest=fixture.directory.active().getFirst();
|
||||
f.plugin.setEnabled(false);f.awaitWorkers();WitchAuditTestSupport.drain(f,"questTrackingDrain");
|
||||
var source=data.resolve("adventure-quests.yml");var yaml=new org.yaml.snakeyaml.Yaml();Map<String,Object> root=yaml.load(Files.readString(source));root.remove("tracking");Files.writeString(source,yaml.dump(root));var legacy=Files.readAllBytes(source);
|
||||
f.players.clear();f.plugin.setEnabled(true);f.players.put(actor.core.id,actor.player);f.reenter(actor);
|
||||
var directory=(games.dmg.fantasystick.adventure.AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");
|
||||
assertTrue(directory.snapshot().tracking().isEmpty());assertTrue(directory.trackingEnabled(quest.id(),actor.core.record.id()));assertArrayEquals(legacy,Files.readAllBytes(source));
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking"});assertEquals("Quest tracking is on for the current adventure quest.",actor.messages.getLast());
|
||||
awaitRows(f,data,"preference.read",1);var query=rows(data,"preference.read").getFirst().getAsJsonObject("data");assertEquals("legacy_default",query.get("previous_status").getAsString());assertTrue(query.get("previous_value").getAsBoolean());assertTrue(query.get("saved_value").getAsBoolean());assertEquals("no_persisted_tracking_revision",query.get("source_revision_status").getAsString());
|
||||
assertTrue(directory.snapshot().tracking().isEmpty());assertArrayEquals(legacy,Files.readAllBytes(source));
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","on"});ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now on."));
|
||||
assertEquals(1,directory.snapshot().tracking().size());assertTrue(directory.trackingEnabled(quest.id(),actor.core.record.id()));awaitRows(f,data,"preference.changed",2);
|
||||
var saved=rows(data,"preference.changed").stream().map(row->row.getAsJsonObject("data")).filter(values->values.get("origin").getAsString().equals("command")).findFirst().orElseThrow();assertEquals("legacy_default",saved.get("previous_status").getAsString());assertTrue(saved.get("previous_value").getAsBoolean());assertTrue(saved.get("saved_value").getAsBoolean());assertEquals("durable_quest_tracking_save",saved.get("source_acknowledgement").getAsString());awaitRows(f,data,"action.resolved",2);
|
||||
}
|
||||
}
|
||||
|
||||
@Test void unusableFeatureIntentStorageCannotAuthorizeRefuseOrUndoRealReadOffOnAndNoncontrolInventory()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;var actor=fixture.actor;var quest=fixture.directory.active().getFirst();var inventory=actor.core.snapshot();var source=Files.readAllBytes(data.resolve("adventure-quests.yml"));
|
||||
var bridge=FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"questTrackingEvents");var outbox=FantasyStickLifecycleNativeTest.Fixture.field(bridge,"outbox");var worker=(ExecutorService)FantasyStickLifecycleNativeTest.Fixture.field(outbox,"worker");worker.submit(()->{}).get(5,TimeUnit.SECONDS);
|
||||
var folder=data.resolve("quest-tracking-event-intents");var proof=data.resolve("quest-tracking-event-intents-proof");Files.move(folder,proof);Files.writeString(folder,"test-only non-directory audit storage");
|
||||
try{
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking"});assertEquals("Quest tracking is on for the current adventure quest.",actor.messages.getLast());
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now off."));assertFalse(fixture.directory.trackingEnabled(quest.id(),actor.core.record.id()));assertEquals(Material.BOOK,actor.core.inventory.getItem(0).getType());
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","on"});ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now on."));assertTrue(fixture.directory.trackingEnabled(quest.id(),actor.core.record.id()));assertEquals(Material.COMPASS,actor.core.inventory.getItem(0).getType());
|
||||
worker.submit(()->{}).get(5,TimeUnit.SECONDS);assertEquals("test-only non-directory audit storage",Files.readString(folder));
|
||||
assertArrayEquals(source,Files.readAllBytes(data.resolve("adventure-quests.yml")));var after=actor.core.snapshot();for(int index=1;index<inventory.length;index++)assertEquals(inventory[index],after[index],"Audit failure cannot alter non-control inventory slot "+index);
|
||||
assertEquals(1,rows(data,"preference.changed").size());assertTrue(rows(data,"preference.read").isEmpty());assertTrue(rows(data,"action.observed").isEmpty());
|
||||
}finally{Files.delete(folder);Files.move(proof,folder);}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import games.dmg.fantasystick.adventure.AdventureQuestState;
|
||||
import games.dmg.fantasystick.session.EquipmentSessions;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/** Actual registered giver/command path; existing tracking and native controls precede audit assertions. */
|
||||
class QuestTrackingEventsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap()throws Exception{NativeRuntime.bootstrap();}
|
||||
|
||||
@Test void registeredSoloAcceptanceAcknowledgesItsExactDefaultOnPreferenceWithoutPresentationSpam()throws Exception{
|
||||
AdventureQuestState.Quest quest;UUID account,character,session;
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
var f=fixture.f;var actor=fixture.actor;fixture.camp("individual-only");fixture.giver();fixture.resetCatalog();f.activate(actor);
|
||||
account=actor.core.id;character=actor.core.record.id();session=active(f,account).session();
|
||||
fixture.click(actor,fixture.open(actor),false);
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.stream().anyMatch(v->v.startsWith("Accepted Same Camp;")));
|
||||
quest=fixture.directory.active().getFirst();
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(),character));
|
||||
assertEquals(Material.COMPASS,actor.core.inventory.getItem(0).getType());
|
||||
for(int i=0;i<8;i++)fixture.pulse();
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(),character));
|
||||
awaitRows(f,data,"preference.changed",1);
|
||||
}
|
||||
var changes=rows(data,"preference.changed");
|
||||
assertEquals(1,changes.size(),"Actual durable registered quest acceptance must export its own default-on preference, not just the quest owner or presentation refresh");
|
||||
var row=changes.getFirst();assertIdentity(row,account,character,session,quest);
|
||||
var values=row.getAsJsonObject("data");assertEquals("quest_acceptance",values.get("origin").getAsString());
|
||||
assertEquals("absent",values.get("previous_status").getAsString());assertTrue(values.get("requested_value").getAsBoolean());
|
||||
assertTrue(values.get("saved_value").getAsBoolean());assertEquals(12,values.getAsJsonObject("location").get("x").getAsDouble());
|
||||
}
|
||||
|
||||
@Test void registeredQueryAndOffOnExportOriginalAccountSessionQuestLocationAndExactValues()throws Exception{
|
||||
AdventureQuestState.Quest quest;UUID account,character,session;
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
var f=fixture.f;var actor=fixture.actor;fixture.camp("individual-only");fixture.giver();fixture.resetCatalog();f.activate(actor);
|
||||
account=actor.core.id;character=actor.core.record.id();session=active(f,account).session();
|
||||
fixture.click(actor,fixture.open(actor),false);
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.stream().anyMatch(v->v.startsWith("Accepted Same Camp;")));
|
||||
quest=fixture.directory.active().getFirst();var before=CombatExperienceNativeTest.record(f,actor);
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking"});
|
||||
assertEquals("Quest tracking is on for the current adventure quest.",actor.messages.getLast());
|
||||
actor.core.location.setX(13);
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});
|
||||
actor.core.location.setX(99);
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now off."));
|
||||
assertFalse(fixture.directory.trackingEnabled(quest.id(),character));assertEquals(Material.BOOK,actor.core.inventory.getItem(0).getType());
|
||||
actor.core.location.setX(14);
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","on"});
|
||||
actor.core.location.setX(100);
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now on."));
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(),character));assertEquals(Material.COMPASS,actor.core.inventory.getItem(0).getType());
|
||||
assertEquals(before,CombatExperienceNativeTest.record(f,actor));
|
||||
for(int i=0;i<8;i++)fixture.pulse();
|
||||
awaitRows(f,data,"preference.read",1);awaitRows(f,data,"preference.changed",3);awaitRows(f,data,"action.resolved",3);
|
||||
}
|
||||
var reads=rows(data,"preference.read");
|
||||
assertEquals(1,reads.size(),"A real registered nonmutating quest-tracking read needs its independent successful read owner");
|
||||
assertIdentity(reads.getFirst(),account,character,session,quest);assertTrue(reads.getFirst().getAsJsonObject("data").get("saved_value").getAsBoolean());
|
||||
var changes=rows(data,"preference.changed").stream().filter(row->row.getAsJsonObject("data").get("origin").getAsString().equals("command")).toList();
|
||||
assertEquals(2,changes.size());
|
||||
for(boolean enabled:List.of(false,true)){
|
||||
var row=changes.stream().filter(value->value.getAsJsonObject("data").get("saved_value").getAsBoolean()==enabled).findFirst().orElseThrow();
|
||||
assertIdentity(row,account,character,session,quest);var values=row.getAsJsonObject("data");
|
||||
assertEquals(!enabled,values.get("previous_value").getAsBoolean());assertEquals(enabled,values.get("requested_value").getAsBoolean());
|
||||
assertEquals("persisted",values.get("previous_status").getAsString());assertEquals(enabled?14:13,values.getAsJsonObject("location").get("x").getAsDouble());
|
||||
var action=values.get("action_id").getAsString();
|
||||
assertEquals(1,rows(data,"action.observed").stream().filter(value->value.getAsJsonObject("data").get("action_id").getAsString().equals(action)).count());
|
||||
assertEquals(1,rows(data,"action.resolved").stream().filter(value->value.getAsJsonObject("data").get("action_id").getAsString().equals(action)).count());
|
||||
}
|
||||
}
|
||||
|
||||
static void awaitRows(FantasyStickLifecycleNativeTest.Fixture f,Path data,String type,int count)throws Exception{
|
||||
ReskillEventsNativeTest.await(f,()->{try{return rows(data,type).size()==count;}catch(Exception failure){throw new AssertionError(failure);}});
|
||||
}
|
||||
static EquipmentSessions.Active active(FantasyStickLifecycleNativeTest.Fixture f,UUID account)throws Exception{
|
||||
return ((EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions")).active(account).orElseThrow();
|
||||
}
|
||||
static List<JsonObject> rows(Path data,String type)throws Exception{
|
||||
return ReskillEventsNativeTest.records(data).stream().filter(row->row.get("type").getAsString().endsWith(type))
|
||||
.filter(row->row.getAsJsonObject("data").has("preference_key")&&row.getAsJsonObject("data").get("preference_key").getAsString().equals("quest-tracking")).toList();
|
||||
}
|
||||
static void assertIdentity(JsonObject row,UUID account,UUID character,UUID session,AdventureQuestState.Quest quest){
|
||||
var values=row.getAsJsonObject("data");assertEquals(account.toString(),row.get("playerid").getAsString());
|
||||
assertEquals(character.toString(),row.get("characterid").getAsString());assertEquals(session.toString(),values.get("session_id").getAsString());
|
||||
assertEquals("character_quest",values.get("scope").getAsString());assertEquals(quest.id().toString(),values.get("quest_id").getAsString());
|
||||
assertEquals(quest.site().toString(),values.get("site_id").getAsString());assertEquals(quest.catalog().toString(),values.get("catalog_id").getAsString());
|
||||
assertNotNull(values.get("action_id"));
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static games.dmg.fantasystick.QuestTrackingEventsNativeTest.*;
|
||||
import static games.dmg.fantasystick.QuestTrackingSafetyEventsNativeTest.acceptSolo;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class QuestTrackingPresentationSafetyNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap()throws Exception{NativeRuntime.bootstrap();}
|
||||
|
||||
@Test void optionalOnlineMetadataFailureCannotHideTheKnownOriginalSoloDefaultAfterActualAcceptance()throws Exception{
|
||||
java.util.UUID account,character,session;games.dmg.fantasystick.adventure.AdventureQuestState.Quest quest;
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
var f=fixture.f;var actor=fixture.actor;fixture.camp("individual-only");fixture.giver();fixture.resetCatalog();f.activate(actor);
|
||||
account=actor.core.id;character=actor.core.record.id();session=active(f,account).session();var menu=fixture.open(actor);
|
||||
fixture.withoutOnlineMetadata(()->fixture.click(actor,menu,false));
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.stream().anyMatch(value->value.startsWith("Accepted Same Camp;")));
|
||||
quest=fixture.directory.active().getFirst();assertTrue(fixture.directory.trackingEnabled(quest.id(),character));assertEquals(Material.COMPASS,actor.core.inventory.getItem(0).getType());
|
||||
awaitRows(f,data,"preference.changed",1);
|
||||
assertIdentity(rows(data,"preference.changed").getFirst(),account,character,session,quest);
|
||||
f.plugin.setEnabled(false);f.awaitWorkers();WitchAuditTestSupport.drain(f,"questTrackingDrain");
|
||||
}
|
||||
var changes=rows(data,"preference.changed");
|
||||
assertEquals(1,changes.size(),"A failed optional online metadata snapshot cannot erase the independently known original accepting character's acknowledged default");
|
||||
assertIdentity(changes.getFirst(),account,character,session,quest);
|
||||
}
|
||||
|
||||
@Test void actualRegisteredFishingAcceptanceRodCompassBookCadenceAndAutocompleteAreSilentAcrossMutedPreferences()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
var f=fixture.f;var actor=fixture.actor;
|
||||
f.adminCommand.execute(actor.player,"fstickadmin",new String[]{"adventure-site","place","fishing","--name","Quiet Lake","--item","Quiet Scale","--required","1","--chance","100","--xp","10","--coins","4","--radius","4","--mode","individual-only"});
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.stream().anyMatch(value->value.startsWith("Placed fishing site")));
|
||||
fixture.giver();fixture.resetCatalog();f.activate(actor);
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"feedback","off"});ReskillEventsNativeTest.await(f,()->actor.messages.contains("Fantasy feedback is now off."));
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"particles","all","off"});ReskillEventsNativeTest.await(f,()->actor.messages.contains("All Fantasy Stick particles are now off."));
|
||||
var preferences=(games.dmg.fantasystick.persistence.PlayerFeedbackPreferences)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"feedbackPreferences");assertFalse(preferences.enabled(actor.core.id));assertFalse(preferences.allParticlesEnabled(actor.core.id));
|
||||
fixture.click(actor,fixture.open(actor),false);ReskillEventsNativeTest.await(f,()->actor.messages.stream().anyMatch(value->value.startsWith("Accepted Quiet Lake;")));
|
||||
var quest=fixture.directory.active().getFirst();assertNotNull(quest.fishing());
|
||||
for(int i=0;i<8;i++)fixture.pulse();assertEquals(Material.FISHING_ROD,actor.core.inventory.getItem(0).getType());
|
||||
awaitRows(f,data,"preference.changed",1);var bytes=Files.readAllBytes(data.resolve("adventure-quests.yml"));
|
||||
assertEquals(List.of("tracking"),f.playerCommand.tabComplete(actor.player,"fstick",new String[]{"adventure","t"}));
|
||||
assertEquals(List.of("off","on"),f.playerCommand.tabComplete(actor.player,"fstick",new String[]{"adventure","tracking",""}));
|
||||
assertTrue(f.playerCommand.tabComplete(actor.player,"fstick",new String[]{"adventure","tracking","on","extra"}).isEmpty());
|
||||
actor.core.location.setX(30);for(int i=0;i<8;i++)fixture.pulse();assertEquals(Material.COMPASS,actor.core.inventory.getItem(0).getType());
|
||||
assertArrayEquals(bytes,Files.readAllBytes(data.resolve("adventure-quests.yml")));assertTrue(rows(data,"action.observed").isEmpty());assertTrue(rows(data,"preference.read").isEmpty());assertEquals(1,rows(data,"preference.changed").size());
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now off."));assertEquals(Material.BOOK,actor.core.inventory.getItem(0).getType());
|
||||
actor.core.location.setX(12);for(int i=0;i<8;i++)fixture.pulse();assertEquals(Material.FISHING_ROD,actor.core.inventory.getItem(0).getType());
|
||||
actor.core.location.setX(30);for(int i=0;i<8;i++)fixture.pulse();assertEquals(Material.BOOK,actor.core.inventory.getItem(0).getType());
|
||||
awaitRows(f,data,"preference.changed",2);awaitRows(f,data,"action.resolved",1);assertTrue(rows(data,"preference.read").isEmpty());assertFalse(preferences.enabled(actor.core.id));assertFalse(preferences.allParticlesEnabled(actor.core.id));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void realRegisteredPaperAbandonmentMakesTheQueuedTrackingRequestStaleWithoutMutatingOrInventingAnotherPreference()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;var actor=fixture.actor;var quest=fixture.directory.active().getFirst();var menu=fixture.open(actor);
|
||||
var worker=(ExecutorService)FantasyStickLifecycleNativeTest.Fixture.field(fixture.directory,"worker");var entered=new CountDownLatch(1);var release=new CountDownLatch(1);
|
||||
worker.execute(()->{entered.countDown();try{release.await();}catch(InterruptedException interrupted){Thread.currentThread().interrupt();}});assertTrue(entered.await(5,TimeUnit.SECONDS));
|
||||
try{fixture.click(actor,menu,true);f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});}finally{release.countDown();}
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.contains("Adventure quest abandoned.")&&actor.messages.contains("Quest tracking change failed; retained state is unchanged."));
|
||||
assertTrue(fixture.directory.findQuest(quest.id()).isEmpty());assertTrue(fixture.directory.snapshot().tracking().isEmpty());assertEquals(Material.BOOK,actor.core.inventory.getItem(0).getType());
|
||||
awaitRows(f,data,"action.resolved",1);assertEquals(1,rows(data,"preference.changed").size());
|
||||
var values=rows(data,"action.resolved").getFirst().getAsJsonObject("data");assertEquals("stale",values.get("outcome").getAsString());assertEquals("not_applicable_no_eligible_quest",values.get("previous_status").getAsString());assertFalse(values.has("saved_value"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static games.dmg.fantasystick.QuestTrackingEventsNativeTest.*;
|
||||
import static games.dmg.fantasystick.QuestTrackingSafetyEventsNativeTest.acceptSolo;
|
||||
import static games.dmg.fantasystick.WitchAuditTestSupport.*;
|
||||
|
||||
import games.dmg.fantasystick.events.LocalEventLog;
|
||||
import games.dmg.fantasystick.events.QuestTrackingEventBridge;
|
||||
import games.dmg.fantasystick.items.EquipmentStorage;
|
||||
import games.dmg.fantasystick.session.EquipmentSessions;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
class QuestTrackingRecoveryEventsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap()throws Exception{NativeRuntime.bootstrap();}
|
||||
|
||||
@Test void actualHolderTransferAndPluginRestartPreserveChoiceOriginalOmittedChangeAndNewHolderSession()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;var a=fixture.actor;var quest=fixture.directory.active().getFirst();
|
||||
var original=active(f,a.core.id);var sessions=(EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");
|
||||
((LocalEventLog)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"eventLog")).close();
|
||||
f.playerCommand.execute(a.player,"fstick",new String[]{"adventure","tracking","off"});
|
||||
ReskillEventsNativeTest.await(f,()->a.messages.contains("Quest tracking is now off."));assertFalse(fixture.directory.trackingEnabled(quest.id(),original.character()));
|
||||
assertEquals(EquipmentStorage.RestoreResult.RESTORED,sessions.exit(a.player));f.awaitCharacterAndReceipt(a);
|
||||
var b=new FantasyStickLifecycleNativeTest.Actor(data);b.core.location.setWorld(a.world);f.players.put(b.core.id,b.player);
|
||||
b.core.inventory.setItemInOffHand(a.core.inventory.getItemInOffHand().clone());f.activate(b);
|
||||
var transferred=active(f,b.core.id);assertEquals(original.character(),transferred.character());assertNotEquals(original.session(),transferred.session());
|
||||
var trackingBytes=Files.readAllBytes(data.resolve("adventure-quests.yml"));
|
||||
f.playerCommand.execute(b.player,"fstick",new String[]{"adventure","tracking"});assertEquals("Quest tracking is off for the current adventure quest.",b.messages.getLast());
|
||||
fixture.pulse();assertEquals(Material.BOOK,b.core.inventory.getItem(0).getType());assertArrayEquals(trackingBytes,Files.readAllBytes(data.resolve("adventure-quests.yml")));
|
||||
f.plugin.setEnabled(false);f.awaitWorkers();drain(f,"questTrackingDrain");
|
||||
var originals=intents(data.resolve("quest-tracking-event-intents"));assertEquals(6,originals.size());
|
||||
var off=originals.stream().filter(p->p.getProperty("type").equals("preference.changed")).findFirst().orElseThrow();
|
||||
assertEquals(a.core.id.toString(),off.getProperty("player"));assertEquals(original.session().toString(),off.getProperty("session"));
|
||||
var afterDisable=Files.readAllBytes(data.resolve("adventure-quests.yml"));var inventory=b.core.snapshot();var messages=List.copyOf(b.messages);
|
||||
f.players.clear();clearInvocations(b.player);f.plugin.setEnabled(true);
|
||||
ReskillEventsNativeTest.await(f,()->{try{var exported=ReskillEventsNativeTest.records(data).stream().map(row->row.get("id").getAsString()).collect(java.util.stream.Collectors.toSet());return originals.stream().allMatch(intent->exported.contains(intent.getProperty("id")));}catch(Exception failure){throw new AssertionError(failure);}});
|
||||
assertArrayEquals(afterDisable,Files.readAllBytes(data.resolve("adventure-quests.yml")));assertArrayEquals(inventory,b.core.snapshot());assertEquals(messages,b.messages);verifyNoInteractions(b.player);
|
||||
var all=ReskillEventsNativeTest.records(data);for(var intent:originals)assertOriginal(intent,all.stream().filter(row->row.get("id").getAsString().equals(intent.getProperty("id"))).findFirst().orElseThrow());
|
||||
var recovered=(games.dmg.fantasystick.adventure.AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");
|
||||
assertFalse(recovered.trackingEnabled(quest.id(),original.character()));
|
||||
f.players.put(b.core.id,b.player);f.reenter(b);var reentered=active(f,b.core.id);
|
||||
f.playerCommand.execute(b.player,"fstick",new String[]{"adventure","tracking","on"});
|
||||
ReskillEventsNativeTest.await(f,()->b.messages.contains("Quest tracking is now on."));assertTrue(recovered.trackingEnabled(quest.id(),original.character()));
|
||||
awaitRows(f,data,"preference.changed",3);
|
||||
var on=rows(data,"preference.changed").stream().filter(row->row.getAsJsonObject("data").get("origin").getAsString().equals("command")&&row.getAsJsonObject("data").get("saved_value").getAsBoolean()).findFirst().orElseThrow();
|
||||
assertIdentity(on,b.core.id,original.character(),reentered.session(),quest);
|
||||
var audit=(QuestTrackingEventBridge)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"questTrackingEvents");audit.replay();audit.replay();
|
||||
f.plugin.setEnabled(false);f.awaitWorkers();drain(f,"questTrackingDrain");assertEquals(3,rows(data,"preference.changed").size());
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest @ValueSource(booleans={false,true})
|
||||
void rejectedQueryAndChangesReplayEveryOriginalPropertyOfflineWithoutAnyAuthorityOrNativeCapture(boolean unavailableLocation)throws Exception{
|
||||
List<Properties> originals;var sourceBytes=new LinkedHashMap<Path,byte[]>();FantasyStickLifecycleNativeTest.Actor actor;org.bukkit.inventory.ItemStack[] inventory;List<String> messages;
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;actor=fixture.actor;var quest=fixture.directory.active().getFirst();
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"feedback","off"});ReskillEventsNativeTest.await(f,()->actor.messages.contains("Fantasy feedback is now off."));
|
||||
((LocalEventLog)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"eventLog")).close();
|
||||
if(unavailableLocation)doThrow(new IllegalStateException("test-only optional request location unavailable")).when(actor.player).getLocation();
|
||||
try{f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking"});assertEquals("Quest tracking is on for the current adventure quest.",actor.messages.getLast());}
|
||||
finally{doAnswer(call->actor.core.location.clone()).when(actor.player).getLocation();}
|
||||
actor.core.location.setX(13);f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now off."));
|
||||
actor.core.location.setX(14);f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","on"});
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking is now on."));assertTrue(fixture.directory.trackingEnabled(quest.id(),actor.core.record.id()));
|
||||
f.plugin.setEnabled(false);f.awaitWorkers();drain(f,"questTrackingDrain");originals=intents(data.resolve("quest-tracking-event-intents"));assertEquals(9,originals.size());
|
||||
if(unavailableLocation){var query=originals.stream().filter(p->p.getProperty("type").equals("preference.read")).findFirst().orElseThrow();assertEquals("sunavailable",query.getProperty("data.action_location_status"));assertNull(query.getProperty("world"));}
|
||||
inventory=actor.core.snapshot();messages=List.copyOf(actor.messages);saveYaml(sourceBytes);
|
||||
}
|
||||
replayOriginals(originals);for(var entry:sourceBytes.entrySet())assertArrayEquals(entry.getValue(),Files.readAllBytes(data.resolve(entry.getKey())));
|
||||
assertArrayEquals(inventory,actor.core.snapshot());assertEquals(messages,actor.messages);
|
||||
}
|
||||
|
||||
@Test void actualQueuedAbandonmentProducesOriginalStaleResolutionWithoutAFictionalLegacyDefaultOrSavedChoice()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;var actor=fixture.actor;var quest=fixture.directory.active().getFirst();
|
||||
var worker=(ExecutorService)FantasyStickLifecycleNativeTest.Fixture.field(fixture.directory,"worker");var entered=new CountDownLatch(1);var release=new CountDownLatch(1);
|
||||
worker.execute(()->{entered.countDown();try{release.await();}catch(InterruptedException interrupted){Thread.currentThread().interrupt();}});assertTrue(entered.await(5,TimeUnit.SECONDS));
|
||||
java.util.concurrent.CompletableFuture<Boolean> abandoned;
|
||||
try{abandoned=fixture.directory.abandon(quest.id(),quest.unit());f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});}finally{release.countDown();}
|
||||
assertTrue(abandoned.get(5,TimeUnit.SECONDS));ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking change failed; retained state is unchanged."));
|
||||
assertTrue(fixture.directory.findQuest(quest.id()).isEmpty());assertTrue(fixture.directory.snapshot().tracking().isEmpty());
|
||||
awaitRows(f,data,"action.resolved",1);assertEquals(1,rows(data,"preference.changed").size());
|
||||
var stale=rows(data,"action.resolved").getFirst().getAsJsonObject("data");assertEquals("stale",stale.get("outcome").getAsString());
|
||||
assertEquals(quest.id().toString(),stale.get("quest_id").getAsString());assertFalse(stale.has("previous_value"));assertFalse(stale.has("saved_value"));
|
||||
assertEquals("not_applicable_no_eligible_quest",stale.get("previous_status").getAsString(),"A real lost quest is not an existing legacy default-on preference");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void unavailableOriginalPartyMemberHolderStaysUnavailableAndReplaysTheExactOriginalFacts()throws Exception{
|
||||
List<Properties> originals;var sourceBytes=new LinkedHashMap<Path,byte[]>();
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
var f=fixture.f;var leader=fixture.actor;var member=new FantasyStickLifecycleNativeTest.Actor(data);member.core.location.setWorld(leader.world);f.players.put(member.core.id,member.player);f.activate(leader);f.activate(member);
|
||||
var parties=PartyLifecycleNativeTest.parties(f);var party=parties.create(leader.core.id).orElseThrow();assertTrue(parties.accept(member.core.id,parties.invite(leader.core.id,party.id(),member.core.id).orElseThrow().id()));
|
||||
var sessions=(EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");assertEquals(EquipmentStorage.RestoreResult.RESTORED,sessions.exit(member.player));f.awaitCharacterAndReceipt(member);f.players.remove(member.core.id);
|
||||
fixture.camp("party-required");fixture.giver();fixture.resetCatalog();((LocalEventLog)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"eventLog")).close();
|
||||
fixture.click(leader,fixture.open(leader),true);ReskillEventsNativeTest.await(f,()->leader.messages.stream().anyMatch(value->value.startsWith("Accepted Same Camp;")));
|
||||
var quest=fixture.directory.active().getFirst();assertTrue(quest.snapshot().contains(member.core.record.id()));assertTrue(fixture.directory.trackingEnabled(quest.id(),member.core.record.id()));
|
||||
f.plugin.setEnabled(false);f.awaitWorkers();drain(f,"questTrackingDrain");originals=intents(data.resolve("quest-tracking-event-intents"));assertEquals(2,originals.size());
|
||||
var unavailable=originals.stream().filter(p->p.getProperty("data.preference_owner_character_id").equals("s"+member.core.record.id())).findFirst().orElseThrow();
|
||||
assertNull(unavailable.getProperty("player"));assertNull(unavailable.getProperty("character"));assertNull(unavailable.getProperty("session"));
|
||||
saveYaml(sourceBytes);
|
||||
}
|
||||
replayOriginals(originals);for(var entry:sourceBytes.entrySet())assertArrayEquals(entry.getValue(),Files.readAllBytes(data.resolve(entry.getKey())));
|
||||
}
|
||||
|
||||
private void saveYaml(Map<Path,byte[]> bytes)throws Exception{
|
||||
try(var paths=Files.walk(data)){for(var path:paths.filter(p->Files.isRegularFile(p)&&p.toString().endsWith(".yml")&&!p.getFileName().toString().equals("config.yml")).toList())bytes.put(data.relativize(path),Files.readAllBytes(path));}
|
||||
}
|
||||
private void replayOriginals(List<Properties> originals)throws Exception{
|
||||
var failures=new ArrayList<Throwable>();
|
||||
try(var writer=LocalEventLog.open(data,java.time.Clock.systemUTC(),new LocalEventLog.RuntimeInfo("offline metadata","pinned-native","26.2","test"),128,1_048_576,message->{throw new AssertionError(message);})){
|
||||
writer.ready().toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
var audit=new QuestTrackingEventBridge(writer::emit,data,sender->{throw new AssertionError("Replay cannot consult present native actors or tracking authority");},failures::add);
|
||||
try(audit){audit.replay();audit.replay();}audit.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
}
|
||||
assertTrue(failures.isEmpty(),failures.toString());var all=ReskillEventsNativeTest.records(data);
|
||||
for(var intent:originals){var matches=all.stream().filter(row->row.get("id").getAsString().equals(intent.getProperty("id"))).toList();assertEquals(1,matches.size());assertOriginal(intent,matches.getFirst());}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static games.dmg.fantasystick.QuestTrackingEventsNativeTest.*;
|
||||
|
||||
import games.dmg.fantasystick.adventure.AdventureQuestState;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
class QuestTrackingSafetyEventsNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap()throws Exception{NativeRuntime.bootstrap();}
|
||||
|
||||
@ParameterizedTest @ValueSource(strings={"party-available","party-required"})
|
||||
void registeredPartyDefaultsAndIndependentMemberChoiceRetainOriginalActorsAcrossLeadershipTransfer(String mode)throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
var f=fixture.f;var leader=fixture.actor;var member=new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
member.core.location.setWorld(leader.world);member.core.location.setX(22);f.players.put(member.core.id,member.player);f.activate(leader);f.activate(member);
|
||||
var leaderSession=active(f,leader.core.id).session();var memberSession=active(f,member.core.id).session();
|
||||
var parties=PartyLifecycleNativeTest.parties(f);var party=parties.create(leader.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(member.core.id,parties.invite(leader.core.id,party.id(),member.core.id).orElseThrow().id()));
|
||||
fixture.camp(mode);fixture.giver();fixture.resetCatalog();fixture.click(leader,fixture.open(leader),true);
|
||||
ReskillEventsNativeTest.await(f,()->leader.messages.stream().anyMatch(value->value.startsWith("Accepted Same Camp;")));
|
||||
var quest=fixture.directory.active().getFirst();assertEquals(AdventureQuestState.UnitType.PARTY,quest.unitType());
|
||||
assertEquals(Set.of(leader.core.record.id(),member.core.record.id()),quest.snapshot());
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(),leader.core.record.id()));assertTrue(fixture.directory.trackingEnabled(quest.id(),member.core.record.id()));
|
||||
awaitRows(f,data,"preference.changed",2);
|
||||
var defaults=rows(data,"preference.changed");
|
||||
for(var owner:List.of(leader,member)){
|
||||
var row=defaults.stream().filter(value->value.get("characterid").getAsString().equals(owner.core.record.id().toString())).findFirst().orElseThrow();
|
||||
assertIdentity(row,owner.core.id,owner.core.record.id(),owner==leader?leaderSession:memberSession,quest);
|
||||
var values=row.getAsJsonObject("data");assertEquals("quest_acceptance",values.get("origin").getAsString());
|
||||
assertEquals(leader.core.id.toString(),values.get("originating_account_id").getAsString());
|
||||
assertEquals(owner==leader?12:22,values.getAsJsonObject("location").get("x").getAsDouble());
|
||||
assertEquals(owner==leader?12:22,values.getAsJsonObject("actor_location").get("x").getAsDouble());
|
||||
assertTrue(values.has("action_location"),"Each acknowledged party-member preference needs its original accepting action location separately from the member's native actor location");
|
||||
assertEquals(12,values.getAsJsonObject("action_location").get("x").getAsDouble());
|
||||
}
|
||||
f.playerCommand.execute(member.player,"fstick",new String[]{"adventure","tracking","off"});
|
||||
ReskillEventsNativeTest.await(f,()->member.messages.contains("Quest tracking is now off."));
|
||||
assertFalse(fixture.directory.trackingEnabled(quest.id(),member.core.record.id()));assertTrue(fixture.directory.trackingEnabled(quest.id(),leader.core.record.id()));
|
||||
var durable=(games.dmg.fantasystick.party.CharacterPartyDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characterParties");
|
||||
assertEquals(games.dmg.fantasystick.party.CharacterPartyDirectory.Status.APPLIED,durable.transfer(leader.core.record.id(),party.id(),member.core.record.id()).get(5,TimeUnit.SECONDS).status());
|
||||
var added=new FantasyStickLifecycleNativeTest.Actor(data);added.core.location.setWorld(leader.world);f.players.put(added.core.id,added.player);f.activate(added);
|
||||
assertTrue(parties.accept(added.core.id,parties.invite(member.core.id,party.id(),added.core.id).orElseThrow().id()));
|
||||
assertFalse(quest.snapshot().contains(added.core.record.id()));
|
||||
f.playerCommand.execute(added.player,"fstick",new String[]{"adventure","tracking","on"});
|
||||
assertEquals("Quest tracking refused: this character has no eligible active quest.",added.messages.getLast());
|
||||
f.playerCommand.execute(member.player,"fstick",new String[]{"adventure","tracking"});assertEquals("Quest tracking is off for the current adventure quest.",member.messages.getLast());
|
||||
for(int i=0;i<8;i++)fixture.pulse();
|
||||
assertEquals(Material.BOOK,member.core.inventory.getItem(0).getType());assertEquals(Material.COMPASS,leader.core.inventory.getItem(0).getType());
|
||||
assertFalse(fixture.directory.trackingEnabled(quest.id(),added.core.record.id()));assertEquals(quest,fixture.directory.findQuest(quest.id()).orElseThrow());
|
||||
awaitRows(f,data,"preference.changed",3);awaitRows(f,data,"preference.read",1);awaitRows(f,data,"action.resolved",3);
|
||||
assertEquals(2,rows(data,"preference.changed").stream().filter(value->value.getAsJsonObject("data").get("origin").getAsString().equals("quest_acceptance")).count());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void queuedRegisteredOffOnAndUnchangedUseActualSerializedBeforeValuesNotTheSharedRequestSnapshot()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;var actor=fixture.actor;var quest=fixture.directory.active().getFirst();
|
||||
var worker=(ExecutorService)FantasyStickLifecycleNativeTest.Fixture.field(fixture.directory,"worker");
|
||||
var entered=new CountDownLatch(1);var release=new CountDownLatch(1);
|
||||
worker.execute(()->{entered.countDown();try{release.await();}catch(InterruptedException interrupted){Thread.currentThread().interrupt();}});
|
||||
assertTrue(entered.await(5,TimeUnit.SECONDS));
|
||||
try{
|
||||
for(var request:List.of("off","on","on")){
|
||||
actor.core.location.setX(request.equals("off")?13:actor.core.location.getX()+1);
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking",request});
|
||||
}
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(),actor.core.record.id()));actor.core.location.setX(99);
|
||||
}finally{release.countDown();}
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.stream().filter(value->value.equals("Quest tracking is now on.")).count()==2);
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(),actor.core.record.id()));
|
||||
awaitRows(f,data,"preference.changed",3);awaitRows(f,data,"action.resolved",3);
|
||||
var changes=rows(data,"preference.changed").stream().filter(row->row.getAsJsonObject("data").get("origin").getAsString().equals("command")).toList();
|
||||
assertEquals(2,changes.size());
|
||||
var on=changes.stream().filter(row->row.getAsJsonObject("data").get("saved_value").getAsBoolean()).findFirst().orElseThrow().getAsJsonObject("data");
|
||||
assertFalse(on.get("previous_value").getAsBoolean());assertEquals(14,on.getAsJsonObject("location").get("x").getAsDouble());
|
||||
var unchanged=rows(data,"action.resolved").stream().filter(row->row.getAsJsonObject("data").get("outcome").getAsString().equals("unchanged")).findFirst().orElseThrow().getAsJsonObject("data");
|
||||
assertTrue(unchanged.get("previous_value").getAsBoolean());assertTrue(unchanged.get("saved_value").getAsBoolean());assertEquals(15,unchanged.getAsJsonObject("location").get("x").getAsDouble());
|
||||
assertTrue(changes.stream().noneMatch(row->row.getAsJsonObject("data").get("action_id").equals(unchanged.get("action_id"))));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest @ValueSource(strings={"inactive","no_quest","dead"})
|
||||
void registeredIneligibleReadAndChangeCorrelateRefusalsWithoutCreatingPreferences(String mode)throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
var f=fixture.f;var actor=fixture.actor;if(!mode.equals("inactive"))f.activate(actor);
|
||||
if(mode.equals("dead"))doReturn(true).when(actor.player).isDead();
|
||||
var inventory=actor.core.snapshot();var tracking=Set.copyOf(fixture.directory.snapshot().tracking());
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking"});
|
||||
f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});
|
||||
assertTrue(actor.messages.getLast().startsWith("Quest tracking refused:"));
|
||||
assertArrayEquals(inventory,actor.core.snapshot());assertEquals(tracking,fixture.directory.snapshot().tracking());
|
||||
awaitRows(f,data,"action.observed",2);awaitRows(f,data,"action.resolved",2);
|
||||
assertTrue(rows(data,"preference.read").isEmpty());assertTrue(rows(data,"preference.changed").isEmpty());
|
||||
for(var row:rows(data,"action.resolved")){
|
||||
var values=row.getAsJsonObject("data");assertEquals("refused",values.get("outcome").getAsString());
|
||||
assertEquals(actor.core.id.toString(),row.get("playerid").getAsString());assertFalse(values.has("saved_value"));
|
||||
assertEquals(1,rows(data,"action.observed").stream().filter(value->value.getAsJsonObject("data").get("action_id").equals(values.get("action_id"))).count());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test void actualFailedSaveRetainsTrackingControlAndOriginalSerializedBeforeWithoutInventingSavedValue()throws Exception{
|
||||
try(var fixture=new QuestTrackingAuditNativeFixture(data)){
|
||||
acceptSolo(fixture);var f=fixture.f;var actor=fixture.actor;var quest=fixture.directory.active().getFirst();
|
||||
var file=data.resolve("adventure-quests.yml");var backup=data.resolve("adventure-quests-proof.yml");var bytes=Files.readAllBytes(file);var inventory=actor.core.snapshot();
|
||||
Files.move(file,backup);Files.createDirectory(file);Files.writeString(file.resolve("nonreplaceable"),"failed-save fixture");
|
||||
try{
|
||||
actor.core.location.setX(13);f.playerCommand.execute(actor.player,"fstick",new String[]{"adventure","tracking","off"});actor.core.location.setX(99);
|
||||
ReskillEventsNativeTest.await(f,()->actor.messages.contains("Quest tracking change failed; retained state is unchanged."));
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(),actor.core.record.id()));assertArrayEquals(inventory,actor.core.snapshot());
|
||||
awaitRows(f,data,"action.resolved",1);assertEquals(1,rows(data,"preference.changed").size());
|
||||
var failed=rows(data,"action.resolved").getFirst().getAsJsonObject("data");assertEquals("failed",failed.get("outcome").getAsString());
|
||||
assertFalse(failed.has("saved_value"));assertEquals(13,failed.getAsJsonObject("location").get("x").getAsDouble());
|
||||
assertTrue(failed.has("previous_status"),"A real failed native-request source save must retain its captured serialized-before evidence, without claiming candidate after-value was saved");
|
||||
assertEquals("persisted",failed.get("previous_status").getAsString());assertTrue(failed.get("previous_value").getAsBoolean());
|
||||
assertEquals("not_acknowledged",failed.get("saved_value_status").getAsString());
|
||||
}finally{Files.delete(file.resolve("nonreplaceable"));Files.delete(file);Files.move(backup,file);}
|
||||
assertArrayEquals(bytes,Files.readAllBytes(file));
|
||||
}
|
||||
}
|
||||
|
||||
static void acceptSolo(QuestTrackingAuditNativeFixture fixture)throws Exception{
|
||||
fixture.camp("individual-only");fixture.giver();fixture.resetCatalog();fixture.f.activate(fixture.actor);
|
||||
fixture.click(fixture.actor,fixture.open(fixture.actor),false);
|
||||
ReskillEventsNativeTest.await(fixture.f,()->fixture.actor.messages.stream().anyMatch(value->value.startsWith("Accepted Same Camp;")));
|
||||
awaitRows(fixture.f,fixture.f.plugin.getDataFolder().toPath(),"preference.changed",1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user