This commit is contained in:
1 parent
5923fcaf75
commit
a79814118e
6 files changed
+423
-17
No files matched your search
@@ -162,8 +162,14 @@ public final class AdventureQuestController implements Listener,AutoCloseable {
|
||||
private List<UUID> recipients(AdventureQuestState.Quest quest,UUID site){var value=directory.findSite(site).orElse(null);return value==null?List.of():recipients(quest,site,arena(value));}
|
||||
private List<UUID> recipients(AdventureQuestState.Quest quest,UUID site,AdventureQuestState.Arena arena){var value=directory.findSite(site).orElse(null);if(value==null)return List.of();var result=new ArrayList<UUID>();for(UUID character:quest.snapshot()){if(directory.characterClaimed(quest.catalog(),quest.site(),character))continue;var active=sessions.activeCharacter(character).filter(sessions::current).orElse(null);if(active==null)continue;var player=plugin.getServer().getPlayer(active.player());if(player==null||!inside(player.getLocation(),value,arena))continue;if(quest.unitType()==AdventureQuestState.UnitType.PARTY){var party=parties.partyOf(player.getUniqueId()).orElse(null);if(party==null||!party.id().equals(quest.unit())||!party.characters().contains(character))continue;}result.add(character);}return result.stream().distinct().sorted().toList();}
|
||||
private void award(AdventureQuestState.Quest quest,List<UUID> recipients,Attempt attempt){var unit=new RewardAllocator.Unit<UUID>(quest.unit(),recipients);var xp=RewardAllocator.allocate(quest.rewardXp(),List.of(unit),random);var coins=RewardAllocator.allocate(quest.rewardCoins(),List.of(unit),random);if((attempt.type==AdventureQuestState.SiteType.CHAMPION||attempt.type==AdventureQuestState.SiteType.CAMP)&&fishing!=null){var rewards=new LinkedHashMap<UUID,FishingState.Reward>();for(UUID character:recipients)rewards.put(character,new FishingState.Reward(xp.get(character),coins.get(character)));UUID carrier=quest.unitType()==AdventureQuestState.UnitType.SOLO?quest.unit():recipients.stream().map(parties::partyOfCharacter).flatMap(Optional::stream).filter(value->value.id().equals(quest.unit())).map(PartyRoster.Party::leaderCharacter).findFirst().orElse(null);var site=directory.findSite(quest.site()).orElse(null);if(carrier==null||site==null){plugin.getLogger().warning("Champion trophy carrier unavailable for quest "+quest.id());return;}var trophyType=attempt.type==AdventureQuestState.SiteType.CHAMPION?FishingState.TrophyType.CHAMPION:FishingState.TrophyType.CAMP;UUID trophy=UUID.nameUUIDFromBytes((trophyType.name().toLowerCase(java.util.Locale.ROOT)+":"+quest.id()).getBytes(java.nio.charset.StandardCharsets.UTF_8));String trophyName=trophyType==FishingState.TrophyType.CHAMPION?"Daybreak Champion Head":"Spoils of "+site.displayName();var audit=new LinkedHashMap<String,String>();
|
||||
audit.put("champion_id",attempt.defeatedEntity==null?"unavailable":attempt.defeatedEntity.toString());
|
||||
audit.put(trophyType==FishingState.TrophyType.CHAMPION?"champion_id":"final_enemy_id",
|
||||
attempt.defeatedEntity==null?"unavailable":attempt.defeatedEntity.toString());
|
||||
audit.put("defeated_at",attempt.defeatedAt==null?"unavailable":attempt.defeatedAt.truncatedTo(java.time.temporal.ChronoUnit.SECONDS).toString());
|
||||
if(trophyType==FishingState.TrophyType.CAMP){
|
||||
audit.put("arena_shape",attempt.arena.shape().name().toLowerCase(Locale.ROOT));
|
||||
audit.put("arena_x_blocks",Integer.toString(attempt.arena.xBlocks()));
|
||||
audit.put("arena_z_blocks",Integer.toString(attempt.arena.zBlocks()));
|
||||
}
|
||||
for(var entry:attempt.defeatLocation.entrySet())
|
||||
audit.put("defeat_"+entry.getKey(),String.valueOf(entry.getValue()));
|
||||
audit.put("unit_kind",quest.unitType().name().toLowerCase(Locale.ROOT));
|
||||
|
||||
@@ -51,7 +51,7 @@ public final class FishingController implements Listener,AutoCloseable,Adventure
|
||||
.thenApply(committed->{
|
||||
var earned=committed==FishingDirectory.TrophyCommitStatus.NOT_FOUND?retained:retained.earned();
|
||||
if(committed==FishingDirectory.TrophyCommitStatus.APPLIED&&fishingEvents!=null)
|
||||
fishingEvents.championEarned(earned);
|
||||
fishingEvents.trophyEarned(earned);
|
||||
return new TrophyEarned(committed!=FishingDirectory.TrophyCommitStatus.NOT_FOUND,
|
||||
committed==FishingDirectory.TrophyCommitStatus.APPLIED,earned);
|
||||
});
|
||||
@@ -64,7 +64,7 @@ public final class FishingController implements Listener,AutoCloseable,Adventure
|
||||
.whenComplete((committed,failure)->{
|
||||
if(failure!=null)report(trophy.id(),trophy.carrier(),"trophy-reconcile",failure);
|
||||
else if(committed==FishingDirectory.TrophyCommitStatus.APPLIED&&fishingEvents!=null)
|
||||
fishingEvents.championEarned(trophy.earned());
|
||||
fishingEvents.trophyEarned(trophy.earned());
|
||||
});
|
||||
}
|
||||
public void open(Player player){open(player,0,null);}
|
||||
@@ -265,9 +265,16 @@ public final class FishingController implements Listener,AutoCloseable,Adventure
|
||||
player.sendMessage("Trophy redemption failed; the earned trophy is retained.");return;}
|
||||
var status=receipt==null?null:receipt.status();
|
||||
if(status==FishingDirectory.TrophyRedeemStatus.APPLIED){
|
||||
if(captured!=null&&captured.type()==FishingState.TrophyType.CHAMPION&&fishingEvents!=null){
|
||||
if(captured!=null&&(captured.type()==FishingState.TrophyType.CHAMPION
|
||||
||captured.type()==FishingState.TrophyType.CAMP)&&fishingEvents!=null){
|
||||
var data=new LinkedHashMap<String,Object>();data.put("receipt_id",captured.id().toString());
|
||||
data.put("trophy_type","champion_trophy");data.put("trophy_name",captured.name());
|
||||
data.put("trophy_type",captured.type().name().toLowerCase(java.util.Locale.ROOT)+"_trophy");
|
||||
data.put("trophy_name",captured.name());
|
||||
if(captured.type()==FishingState.TrophyType.CAMP){
|
||||
data.put("final_enemy_id",captured.audit().getOrDefault("final_enemy_id","unavailable_legacy_capture"));
|
||||
data.put("clear_location",games.dmg.fantasystick.events.FishingEventBridge.savedDefeatLocation(captured.audit()));
|
||||
data.put("arena",games.dmg.fantasystick.events.FishingEventBridge.savedArena(captured.audit()));
|
||||
}
|
||||
data.put("source_name",captured.origin());data.put("carrier_character_id",captured.carrier().toString());
|
||||
data.put("catalog_id",captured.catalog().toString());data.put("site_id",captured.site().toString());
|
||||
data.put("quest_id",captured.quest().toString());data.put("encounter_id",captured.source().toString());
|
||||
@@ -294,7 +301,7 @@ public final class FishingController implements Listener,AutoCloseable,Adventure
|
||||
}
|
||||
player.sendMessage("Adventure trophy redeemed; frozen shares are ready for each beneficiary.");
|
||||
}else player.sendMessage("Trophy redemption refused: the receipt is unavailable for this character.");
|
||||
open(player);
|
||||
open(player,0,menuGivers.get(player.getUniqueId()));
|
||||
}));
|
||||
}
|
||||
private void claimLegacy(Player player,Context context){adventures.beginLegacyClaim(context.record().id()).whenComplete((claim,failure)->{if(failure!=null||claim.isEmpty()){if(failure!=null)report(UUID.randomUUID(),context.record().id(),"legacy-claim-prepare",failure);schedule(()->player.sendMessage("Retained adventure reward claim failed; earned shares remain."));return;}awardLegacy(player,claim.orElseThrow(),context.record());});}
|
||||
|
||||
@@ -127,14 +127,15 @@ public final class FishingEventBridge implements AutoCloseable {
|
||||
} catch (RuntimeException unavailable) { return new Actor("system", null); }
|
||||
}
|
||||
|
||||
/** Reconstruct a champion outcome solely from the saved receipt, including after a prepared retry. */
|
||||
public void championEarned(FishingState.Trophy receipt) {
|
||||
if (receipt.type() != FishingState.TrophyType.CHAMPION
|
||||
/** Reconstruct a combat-trophy outcome solely from the saved receipt, including after a prepared retry. */
|
||||
public void trophyEarned(FishingState.Trophy receipt) {
|
||||
if ((receipt.type() != FishingState.TrophyType.CHAMPION
|
||||
&& receipt.type() != FishingState.TrophyType.CAMP)
|
||||
|| receipt.phase() != FishingState.TrophyPhase.EARNED) return;
|
||||
var audit = receipt.audit();
|
||||
var evidence = new LinkedHashMap<String, Object>();
|
||||
evidence.put("receipt_id", receipt.id().toString());
|
||||
evidence.put("trophy_type", "champion_trophy");
|
||||
evidence.put("trophy_type", receipt.type().name().toLowerCase(java.util.Locale.ROOT) + "_trophy");
|
||||
evidence.put("trophy_name", receipt.name());
|
||||
evidence.put("source_name", receipt.origin());
|
||||
evidence.put("carrier_character_id", receipt.carrier().toString());
|
||||
@@ -143,9 +144,16 @@ public final class FishingEventBridge implements AutoCloseable {
|
||||
evidence.put("catalog_id", receipt.catalog().toString());
|
||||
evidence.put("site_id", receipt.site().toString());
|
||||
evidence.put("encounter_id", receipt.source().toString());
|
||||
evidence.put("champion_id", audit.getOrDefault("champion_id", "unavailable_legacy_capture"));
|
||||
evidence.put("defeat_location", savedDefeatLocation(audit));
|
||||
evidence.put("defeated_at", audit.getOrDefault("defeated_at", "unavailable_legacy_capture"));
|
||||
if (receipt.type() == FishingState.TrophyType.CHAMPION) {
|
||||
evidence.put("champion_id", audit.getOrDefault("champion_id", "unavailable_legacy_capture"));
|
||||
evidence.put("defeat_location", savedDefeatLocation(audit));
|
||||
evidence.put("defeated_at", audit.getOrDefault("defeated_at", "unavailable_legacy_capture"));
|
||||
} else {
|
||||
evidence.put("final_enemy_id", audit.getOrDefault("final_enemy_id", "unavailable_legacy_capture"));
|
||||
evidence.put("clear_location", savedDefeatLocation(audit));
|
||||
evidence.put("cleared_at", audit.getOrDefault("defeated_at", "unavailable_legacy_capture"));
|
||||
evidence.put("arena", savedArena(audit));
|
||||
}
|
||||
evidence.put("unit_kind", audit.getOrDefault("unit_kind", "unavailable_legacy_capture"));
|
||||
evidence.put("unit_id", audit.getOrDefault("unit_id", "unavailable_legacy_capture"));
|
||||
var roster = audit.get("accepted_roster");
|
||||
@@ -170,7 +178,22 @@ public final class FishingEventBridge implements AutoCloseable {
|
||||
durable("adventure.trophy_earned", receipt.id(), new Actor("system", null), evidence);
|
||||
}
|
||||
|
||||
private static Map<String, Object> savedDefeatLocation(Map<String, String> audit) {
|
||||
public static Map<String, Object> savedArena(Map<String, String> audit) {
|
||||
try {
|
||||
var shape = audit.get("arena_shape");
|
||||
int x = Integer.parseInt(audit.get("arena_x_blocks"));
|
||||
int z = Integer.parseInt(audit.get("arena_z_blocks"));
|
||||
var expected = "circle".equals(shape)
|
||||
? games.dmg.fantasystick.adventure.AdventureQuestState.Arena.circle(x)
|
||||
: "rectangle".equals(shape)
|
||||
? games.dmg.fantasystick.adventure.AdventureQuestState.Arena.rectangle(x, z)
|
||||
: null;
|
||||
if (expected == null || expected.zBlocks() != z) return Map.of("status", "unavailable");
|
||||
return Map.of("shape", shape, "x_blocks", x, "z_blocks", z);
|
||||
} catch (RuntimeException unavailable) { return Map.of("status", "unavailable"); }
|
||||
}
|
||||
|
||||
public static Map<String, Object> savedDefeatLocation(Map<String, String> audit) {
|
||||
try {
|
||||
if (!audit.containsKey("defeat_world_id")) return Map.of("status", "unavailable");
|
||||
var world = UUID.fromString(audit.get("defeat_world_id"));
|
||||
|
||||
@@ -145,7 +145,21 @@ class AdventureQuestNativeTest {
|
||||
|
||||
for(Runnable pulse:List.copyOf(f.pulses))pulse.run();for(int retry=0;retry<10&&spawned.stream().filter(entity->!(entity instanceof Villager)).count()<4;retry++)drainOne(f);
|
||||
var enemies=spawned.stream().filter(entity->!(entity instanceof Villager)).toList();assertEquals(4,enemies.size());assertTrue(controller.hasActiveEncounter());
|
||||
var enemy=enemies.getFirst();assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.CAMP,controller.encounterOrigin(enemy).orElseThrow().kind());when(enemy.getLocation()).thenReturn(new Location(actor.world,siteRecord.x()+11,siteRecord.y(),siteRecord.z()));when(actor.world.getEntities()).thenReturn(new ArrayList<Entity>(spawned));for(Runnable pulse:List.copyOf(f.pulses))pulse.run();verify(enemy).teleport(argThat((Location location)->location.getX()==siteRecord.x()&&location.getY()==siteRecord.y()&&location.getZ()==siteRecord.z()));
|
||||
var enemy=enemies.getFirst();assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.CAMP,controller.encounterOrigin(enemy).orElseThrow().kind());
|
||||
var unrelated=mock(org.bukkit.entity.Cow.class);
|
||||
when(unrelated.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
var copiedCampTag=enemy.getPersistentDataContainer();
|
||||
when(unrelated.getPersistentDataContainer()).thenReturn(copiedCampTag);
|
||||
assertTrue(controller.encounterOrigin(unrelated).isEmpty(),
|
||||
"a passive with a copied camp tag is not a registered spawn");
|
||||
var nativeDrop=new org.bukkit.inventory.ItemStack(Material.BEEF);
|
||||
var unrelatedDeath=new org.bukkit.event.entity.EntityDeathEvent(unrelated,
|
||||
mock(org.bukkit.damage.DamageSource.class),new ArrayList<>(List.of(nativeDrop)),7);
|
||||
f.dispatcher.accept(unrelatedDeath);
|
||||
assertEquals(7,unrelatedDeath.getDroppedExp());
|
||||
assertEquals(List.of(nativeDrop),unrelatedDeath.getDrops(),
|
||||
"an unrelated in-arena passive cannot consume camp completion or native loot");
|
||||
when(enemy.getLocation()).thenReturn(new Location(actor.world,siteRecord.x()+11,siteRecord.y(),siteRecord.z()));when(actor.world.getEntities()).thenReturn(new ArrayList<Entity>(spawned));for(Runnable pulse:List.copyOf(f.pulses))pulse.run();verify(enemy).teleport(argThat((Location location)->location.getX()==siteRecord.x()&&location.getY()==siteRecord.y()&&location.getZ()==siteRecord.z()));
|
||||
var outsider=new FantasyStickLifecycleNativeTest.Actor(data);outsider.core.location.setWorld(actor.world);f.players.put(outsider.core.id,outsider.player);var outsiderSource=mock(org.bukkit.damage.DamageSource.class);when(outsiderSource.getCausingEntity()).thenReturn(outsider.player);when(outsiderSource.getDirectEntity()).thenReturn(outsider.player);var refusedHit=new org.bukkit.event.entity.EntityDamageByEntityEvent(outsider.player,enemy,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,outsiderSource,1);f.dispatcher.accept(refusedHit);assertTrue(refusedHit.isCancelled());var enemySource=mock(org.bukkit.damage.DamageSource.class);when(enemySource.getCausingEntity()).thenReturn(enemy);when(enemySource.getDirectEntity()).thenReturn(enemy);var refusedAttack=new org.bukkit.event.entity.EntityDamageByEntityEvent(enemy,outsider.player,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,enemySource,1);f.dispatcher.accept(refusedAttack);assertTrue(refusedAttack.isCancelled());var targetEvent=new org.bukkit.event.entity.EntityTargetLivingEntityEvent(enemy,outsider.player,org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER);f.dispatcher.accept(targetEvent);assertTrue(targetEvent.isCancelled());
|
||||
var source=mock(org.bukkit.damage.DamageSource.class);when(source.getCausingEntity()).thenReturn(actor.player);when(source.getDirectEntity()).thenReturn(actor.player);
|
||||
var hit=new org.bukkit.event.entity.EntityDamageByEntityEvent(actor.player,enemy,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1);f.dispatcher.accept(hit);assertFalse(hit.isCancelled());
|
||||
@@ -155,8 +169,240 @@ class AdventureQuestNativeTest {
|
||||
for(int i=0;i<50&&(directory.active().size()!=1||trophies.trophies(actor.core.record.id()).isEmpty()||actor.core.inventory.getItem(0).getType()!=Material.BOOK);i++){var task=f.tasks.poll(100,java.util.concurrent.TimeUnit.MILLISECONDS);if(task!=null)task.run();Thread.sleep(10);}
|
||||
var banked=storage.find(actor.core.record.id()).get().orElseThrow();assertEquals(java.math.BigInteger.ZERO,banked.experience());assertEquals(actorCoins,banked.coins());assertEquals(games.dmg.fantasystick.adventure.FishingState.TrophyType.CAMP,trophies.trophies(actor.core.record.id()).getFirst().type());assertEquals(Material.BOOK,actor.core.inventory.getItem(0).getType(),"completion restores the US-069 book control");assertEquals(1,directory.active().size(),"Unrepresented party retains its quest");assertEquals(java.math.BigInteger.ZERO,storage.find(partyA.core.record.id()).get().orElseThrow().experience());assertEquals(java.math.BigInteger.ZERO,storage.find(partyB.core.record.id()).get().orElseThrow().experience());
|
||||
var soloTrophy=trophies.trophies(actor.core.record.id()).getFirst();assertEquals("Spoils of Dawn Camp",soloTrophy.name());assertEquals("Dawn Camp",soloTrophy.origin());assertEquals(new games.dmg.fantasystick.adventure.FishingState.Reward(java.math.BigInteger.valueOf(100),java.math.BigInteger.valueOf(20)),soloTrophy.total());assertEquals(games.dmg.fantasystick.adventure.AdventureQuestDirectory.Reward.empty(),directory.unclaimedAll(actor.core.record.id()),"camp clear does not immediately unlock rewards");
|
||||
int priorEnemies=enemies.size();for(Runnable pulse:List.copyOf(f.pulses))pulse.run();for(int retry=0;retry<10&&spawned.stream().filter(entity->!(entity instanceof Villager)).count()<priorEnemies+4;retry++)drainOne(f);var second=spawned.stream().filter(entity->!(entity instanceof Villager)).skip(priorEnemies).toList();assertEquals(4,second.size());var partySource=mock(org.bukkit.damage.DamageSource.class);when(partySource.getCausingEntity()).thenReturn(partyA.player);when(partySource.getDirectEntity()).thenReturn(partyA.player);var partyHit=new org.bukkit.event.entity.EntityDamageByEntityEvent(partyA.player,second.getFirst(),org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1);f.dispatcher.accept(partyHit);partyA.core.location.setX(100);doReturn(false).when(partyA.player).isOnline();for(var value:second)f.dispatcher.accept(new org.bukkit.event.entity.EntityDeathEvent(value,partySource,new ArrayList<>(),0));await(f,()->directory.active().isEmpty()&&!trophies.trophies(partyA.core.record.id()).isEmpty());var partyTrophy=trophies.trophies(partyA.core.record.id()).getFirst();assertEquals(games.dmg.fantasystick.adventure.FishingState.TrophyType.CAMP,partyTrophy.type());assertEquals(Set.of(partyB.core.record.id()),partyTrophy.rewards().keySet(),"absent leader carries the spoils without gaining a share");assertEquals(new games.dmg.fantasystick.adventure.FishingState.Reward(java.math.BigInteger.valueOf(100),java.math.BigInteger.valueOf(20)),partyTrophy.total());assertEquals(java.math.BigInteger.ZERO,storage.find(partyA.core.record.id()).get().orElseThrow().experience());assertEquals(java.math.BigInteger.ZERO,storage.find(partyB.core.record.id()).get().orElseThrow().experience());var completedCatalog=directory.snapshot().catalog();assertFalse(directory.snapshot().rosters().isEmpty());f.command.execute(actor.player,"fantasystick",new String[]{"adventure","reroll","confirm"});for(int retry=0;retry<15&&(directory.snapshot().catalog().revision().equals(completedCatalog.revision())||directory.snapshot().reset()!=null);retry++)drainOne(f);var replacement=directory.snapshot().catalog();assertNotEquals(completedCatalog.revision(),replacement.revision());assertTrue(directory.snapshot().rosters().isEmpty());var turn=(games.dmg.fantasystick.adventure.FishingController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishingController");redeemAndClaim(f,turn,trophies,storage,actor,actorCoins,100,20);doReturn(true).when(partyA.player).isOnline();redeem(f,turn,partyA);claim(f,turn,trophies,storage,partyB,partyBCoins,100,20);assertEquals(partyACoins,storage.find(partyA.core.record.id()).get().orElseThrow().coins(),"carrier without a share receives no coins");assertEquals(games.dmg.fantasystick.adventure.AdventureQuestDirectory.Acceptance.APPLIED,directory.accept(replacement.revision(),replacement.sites().getFirst(),games.dmg.fantasystick.adventure.AdventureQuestState.UnitType.SOLO,actor.core.record.id(),Set.of(actor.core.record.id())).get(5,java.util.concurrent.TimeUnit.SECONDS).status());
|
||||
int priorEnemies=enemies.size();for(Runnable pulse:List.copyOf(f.pulses))pulse.run();for(int retry=0;retry<10&&spawned.stream().filter(entity->!(entity instanceof Villager)).count()<priorEnemies+4;retry++)drainOne(f);var second=spawned.stream().filter(entity->!(entity instanceof Villager)).skip(priorEnemies).toList();assertEquals(4,second.size());var partySource=mock(org.bukkit.damage.DamageSource.class);when(partySource.getCausingEntity()).thenReturn(partyA.player);when(partySource.getDirectEntity()).thenReturn(partyA.player);var partyHit=new org.bukkit.event.entity.EntityDamageByEntityEvent(partyA.player,second.getFirst(),org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1);f.dispatcher.accept(partyHit);partyA.core.location.setX(100);doReturn(false).when(partyA.player).isOnline();for(var value:second)f.dispatcher.accept(new org.bukkit.event.entity.EntityDeathEvent(value,partySource,new ArrayList<>(),0));await(f,()->directory.active().isEmpty()&&!trophies.trophies(partyA.core.record.id()).isEmpty());var partyTrophy=trophies.trophies(partyA.core.record.id()).getFirst();assertEquals(games.dmg.fantasystick.adventure.FishingState.TrophyType.CAMP,partyTrophy.type());assertEquals(Set.of(partyB.core.record.id()),partyTrophy.rewards().keySet(),"absent leader carries the spoils without gaining a share");assertEquals(new games.dmg.fantasystick.adventure.FishingState.Reward(java.math.BigInteger.valueOf(100),java.math.BigInteger.valueOf(20)),partyTrophy.total());assertEquals(java.math.BigInteger.ZERO,storage.find(partyA.core.record.id()).get().orElseThrow().experience());assertEquals(java.math.BigInteger.ZERO,storage.find(partyB.core.record.id()).get().orElseThrow().experience());var completedCatalog=directory.snapshot().catalog();assertFalse(directory.snapshot().rosters().isEmpty());f.command.execute(actor.player,"fantasystick",new String[]{"adventure","reroll","confirm"});for(int retry=0;retry<15&&(directory.snapshot().catalog().revision().equals(completedCatalog.revision())||directory.snapshot().reset()!=null);retry++)drainOne(f);var replacement=directory.snapshot().catalog();assertNotEquals(completedCatalog.revision(),replacement.revision());assertTrue(directory.snapshot().rosters().isEmpty());var turn=(games.dmg.fantasystick.adventure.FishingController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishingController");
|
||||
var giverLocation=directory.snapshot().givers().getFirst();
|
||||
actor.core.location.setX(giverLocation.x());actor.core.location.setZ(giverLocation.z());
|
||||
int beforeOffers=actor.inventories.size();
|
||||
f.dispatcher.accept(new PlayerInteractEntityEvent(actor.player,giver,EquipmentSlot.HAND));
|
||||
await(f,()->actor.inventories.size()>beforeOffers);
|
||||
var giverOffers=actor.inventories.getLast();
|
||||
var turnEntry=mock(InventoryClickEvent.class);when(turnEntry.getWhoClicked()).thenReturn(actor.player);
|
||||
when(turnEntry.getInventory()).thenReturn(giverOffers);
|
||||
when(turnEntry.getCurrentItem()).thenReturn(item(giverOffers,"Turn in quest items"));
|
||||
controller.click(turnEntry);
|
||||
var turnMenu=actor.inventories.getLast();int beforeClaimMenu=actor.inventories.size();
|
||||
click(turn,actor,turnMenu,item(turnMenu,"Redeem Spoils of"));
|
||||
await(f,()->trophies.trophies(actor.core.record.id()).isEmpty()
|
||||
&&actor.inventories.size()>beforeClaimMenu);
|
||||
var claimMenu=actor.inventories.getLast();
|
||||
click(turn,actor,claimMenu,item(claimMenu,"Claim earned quest rewards"));
|
||||
await(f,()->trophies.unclaimed(actor.core.record.id()).equals(
|
||||
games.dmg.fantasystick.adventure.FishingState.Reward.empty()));
|
||||
var paid=storage.find(actor.core.record.id()).get().orElseThrow();
|
||||
assertEquals(java.math.BigInteger.valueOf(100),paid.experience());
|
||||
assertEquals(actorCoins.add(java.math.BigInteger.valueOf(20)),paid.coins());doReturn(true).when(partyA.player).isOnline();redeem(f,turn,partyA);claim(f,turn,trophies,storage,partyB,partyBCoins,100,20);assertEquals(partyACoins,storage.find(partyA.core.record.id()).get().orElseThrow().coins(),"carrier without a share receives no coins");assertEquals(games.dmg.fantasystick.adventure.AdventureQuestDirectory.Acceptance.APPLIED,directory.accept(replacement.revision(),replacement.sites().getFirst(),games.dmg.fantasystick.adventure.AdventureQuestState.UnitType.SOLO,actor.core.record.id(),Set.of(actor.core.record.id())).get(5,java.util.concurrent.TimeUnit.SECONDS).status());
|
||||
}
|
||||
var exported=exportedCampEvents(data);
|
||||
var earned=exported.stream().filter(event->event.get("type").getAsString()
|
||||
.endsWith("adventure.trophy_earned")).toList();
|
||||
var redeemed=exported.stream().filter(event->event.get("type").getAsString()
|
||||
.endsWith("adventure.trophy_redeemed")).toList();
|
||||
var claims=exported.stream().filter(event->event.get("type").getAsString()
|
||||
.endsWith("adventure.reward_claimed")&&event.getAsJsonObject("data")
|
||||
.getAsJsonArray("sources").asList().stream().anyMatch(source->source
|
||||
.getAsJsonObject().get("source_kind").getAsString().equals("camp_trophy"))).toList();
|
||||
assertEquals(2,earned.size(),"two native represented camp clears export separate earned receipts");
|
||||
assertEquals(2,redeemed.size(),"exact earned camp spoils unlock once per carrier");
|
||||
assertTrue(redeemed.stream().anyMatch(event->event.getAsJsonObject("data")
|
||||
.get("giver_status").getAsString().equals("entered_via_registered_giver")),
|
||||
"at least one redemption must follow the registered Quest Giver menu");
|
||||
assertEquals(2,claims.size(),"each beneficiary claims only their saved camp allocation");
|
||||
assertTrue(claims.stream().anyMatch(event->event.getAsJsonObject("data")
|
||||
.get("giver_status").getAsString().equals("entered_via_registered_giver")),
|
||||
"the redeemed camp's beneficiary claims through the same registered Giver");
|
||||
assertEquals(2,earned.stream().map(event->event.getAsJsonObject("data")
|
||||
.get("receipt_id").getAsString()).distinct().count());
|
||||
assertTrue(earned.stream().allMatch(event->event.getAsJsonObject("data")
|
||||
.get("trophy_type").getAsString().equals("camp_trophy")));
|
||||
assertTrue(earned.stream().allMatch(event->event.getAsJsonObject("data")
|
||||
.getAsJsonObject("arena").get("shape").getAsString().equals("rectangle")));
|
||||
assertTrue(earned.stream().allMatch(event->event.getAsJsonObject("data")
|
||||
.getAsJsonObject("clear_location").has("world_id")));
|
||||
assertTrue(earned.stream().allMatch(event->event.getAsJsonObject("data")
|
||||
.get("receipt_revision").getAsString().equals("2")));
|
||||
var partyEarned=earned.stream().map(event->event.getAsJsonObject("data"))
|
||||
.filter(value->!value.get("carrier_is_beneficiary").getAsBoolean())
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("party",partyEarned.get("unit_kind").getAsString());
|
||||
assertEquals(2,partyEarned.getAsJsonArray("accepted_roster").size());
|
||||
assertEquals(1,partyEarned.getAsJsonArray("beneficiaries").size());
|
||||
assertEquals("100",partyEarned.get("reward_xp_pool").getAsString());
|
||||
assertEquals("20",partyEarned.get("reward_coin_pool").getAsString());
|
||||
var partyRedemption=redeemed.stream().map(event->event.getAsJsonObject("data"))
|
||||
.filter(value->value.get("receipt_id").getAsString()
|
||||
.equals(partyEarned.get("receipt_id").getAsString())).findFirst().orElseThrow();
|
||||
assertEquals(partyEarned.get("encounter_id").getAsString(),
|
||||
partyRedemption.get("encounter_id").getAsString());
|
||||
assertTrue(partyRedemption.getAsJsonArray("beneficiaries").get(0).getAsJsonObject()
|
||||
.has("allocation_id"),"a positive frozen camp share keeps its exact unlock identity");
|
||||
assertEquals("rectangle",partyRedemption.getAsJsonObject("arena")
|
||||
.get("shape").getAsString());
|
||||
assertEquals(8,exported.stream().filter(event->event.get("type").getAsString()
|
||||
.endsWith("combat.reward_resolved")&&event.getAsJsonObject("data").has("managed_origin_kind")
|
||||
&&event.getAsJsonObject("data").get("managed_origin_kind").getAsString().equals("camp")
|
||||
&&event.getAsJsonObject("data").get("resolution").getAsString()
|
||||
.equals("encounter_suppressed")).count());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Test void registeredCircularCampClearExportsItsFrozenArenaAndFinalEnemy() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"adventure-daily-site-count: 1\nadventure-site-reset-seconds: 0\nadventure-camp-enemies: 4\n");
|
||||
UUID finalEnemy;
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id,actor.player);doReturn(true).when(actor.player)
|
||||
.hasPermission("fantasystick.admin");
|
||||
var world=actor.world;var worldId=UUID.randomUUID();when(world.getUID()).thenReturn(worldId);
|
||||
when(world.getName()).thenReturn("Circle Realm");when(world.isChunkLoaded(anyInt(),anyInt()))
|
||||
.thenReturn(true);
|
||||
var chunk=mock(Chunk.class);when(chunk.getWorld()).thenReturn(world);
|
||||
when(chunk.getX()).thenReturn(actor.core.location.getBlockX()>>4);
|
||||
when(chunk.getZ()).thenReturn(actor.core.location.getBlockZ()>>4);
|
||||
when(chunk.getEntities()).thenReturn(new Entity[0]);
|
||||
when(world.getChunkAt(anyInt(),anyInt())).thenReturn(chunk);
|
||||
when(world.getLoadedChunks()).thenReturn(new Chunk[]{chunk});
|
||||
var spawned=new ArrayList<LivingEntity>();
|
||||
doAnswer(call->{Class<?> type=call.getArgument(1);
|
||||
var entity=mock(type.asSubclass(LivingEntity.class));
|
||||
when(entity.getPersistentDataContainer()).thenReturn(
|
||||
new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(
|
||||
new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry()));
|
||||
when(entity.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(entity.getWorld()).thenReturn(world);
|
||||
when(entity.getLocation()).thenReturn(((Location)call.getArgument(0)).clone());
|
||||
((java.util.function.Consumer<LivingEntity>)call.getArgument(2)).accept(entity);
|
||||
spawned.add(entity);return entity;
|
||||
}).when(world).spawn(any(Location.class),any(Class.class),any(java.util.function.Consumer.class));
|
||||
f.command.execute(actor.player,"fantasystick",
|
||||
new String[]{"adventure-site","place","camp","10","Round Trial"});drainOne(f);
|
||||
var directory=(games.dmg.fantasystick.adventure.AdventureQuestDirectory)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");
|
||||
var old=directory.snapshot();var snapshot=directory.getClass().getDeclaredField("snapshot");
|
||||
snapshot.setAccessible(true);snapshot.set(directory,
|
||||
new games.dmg.fantasystick.adventure.AdventureQuestState(old.sites(),old.givers(),
|
||||
null,old.quests(),old.unitClaims(),old.characterClaims(),old.rosters()));
|
||||
var catalog=directory.ensureToday().get(5,java.util.concurrent.TimeUnit.SECONDS);
|
||||
var site=catalog.sites().getFirst();f.activate(actor);
|
||||
assertEquals(games.dmg.fantasystick.adventure.AdventureQuestState.Arena.circle(10),
|
||||
directory.findSite(site).orElseThrow().arena());
|
||||
assertEquals(games.dmg.fantasystick.adventure.AdventureQuestDirectory.Acceptance.APPLIED,
|
||||
directory.accept(catalog.revision(),site,
|
||||
games.dmg.fantasystick.adventure.AdventureQuestState.UnitType.SOLO,
|
||||
actor.core.record.id(),Set.of(actor.core.record.id()))
|
||||
.get(5,java.util.concurrent.TimeUnit.SECONDS).status());
|
||||
for(Runnable pulse:List.copyOf(f.pulses))pulse.run();
|
||||
await(f,()->spawned.size()==4);
|
||||
var source=mock(org.bukkit.damage.DamageSource.class);
|
||||
when(source.getCausingEntity()).thenReturn(actor.player);
|
||||
when(source.getDirectEntity()).thenReturn(actor.player);
|
||||
f.dispatcher.accept(new org.bukkit.event.entity.EntityDamageByEntityEvent(actor.player,
|
||||
spawned.getFirst(),org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,4));
|
||||
finalEnemy=spawned.getLast().getUniqueId();
|
||||
for(var enemy:spawned)f.dispatcher.accept(new org.bukkit.event.entity.EntityDeathEvent(
|
||||
enemy,source,new ArrayList<>(),0));
|
||||
var trophies=(games.dmg.fantasystick.adventure.FishingDirectory)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishing");
|
||||
await(f,()->!trophies.trophies(actor.core.record.id()).isEmpty());
|
||||
assertEquals("Round Trial",trophies.trophies(actor.core.record.id()).getFirst().origin());
|
||||
f.command.execute(actor.player,"fantasystick",
|
||||
new String[]{"adventure-site","remove",site.toString()});
|
||||
await(f,()->directory.findSite(site).isEmpty());
|
||||
var storage=(games.dmg.fantasystick.persistence.CharacterStorage)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");
|
||||
var beforeCoins=storage.find(actor.core.record.id()).get().orElseThrow().coins();
|
||||
var turn=(games.dmg.fantasystick.adventure.FishingController)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishingController");
|
||||
redeem(f,turn,actor);
|
||||
claim(f,turn,trophies,storage,actor,beforeCoins,100,20);
|
||||
}
|
||||
var earned=exportedCampEvents(data).stream().filter(event->event.get("type").getAsString()
|
||||
.endsWith("adventure.trophy_earned")).toList();
|
||||
assertEquals(1,earned.size());
|
||||
var evidence=earned.getFirst().getAsJsonObject("data");
|
||||
assertEquals("circle",evidence.getAsJsonObject("arena").get("shape").getAsString());
|
||||
assertEquals(10,evidence.getAsJsonObject("arena").get("x_blocks").getAsInt());
|
||||
assertEquals(finalEnemy.toString(),evidence.get("final_enemy_id").getAsString());
|
||||
assertTrue(evidence.getAsJsonObject("clear_location").has("world_id"));
|
||||
var redemption=exportedCampEvents(data).stream().filter(event->event.get("type")
|
||||
.getAsString().endsWith("adventure.trophy_redeemed")).toList();
|
||||
assertEquals(1,redemption.size(),"deleted-circle site cannot expire an earned camp item");
|
||||
assertEquals(evidence.get("site_id").getAsString(),redemption.getFirst()
|
||||
.getAsJsonObject("data").get("site_id").getAsString());
|
||||
}
|
||||
|
||||
@Test void failedCampCommitRestartsWithSavedArenaAndOneExportedEarnedEvent() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"adventure-daily-site-count: 1\nadventure-camp-enemies: 4\n");
|
||||
UUID carrier,receiptId=UUID.randomUUID(),finalEnemy=UUID.randomUUID();
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id,actor.player);f.activate(actor);
|
||||
doReturn(true).when(actor.player).hasPermission("fantasystick.admin");
|
||||
when(actor.world.getUID()).thenReturn(UUID.randomUUID());
|
||||
f.command.execute(actor.player,"fantasystick",
|
||||
new String[]{"adventure-site","place","camp","10","Restart Circle"});drainOne(f);
|
||||
var quests=(games.dmg.fantasystick.adventure.AdventureQuestDirectory)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");
|
||||
var old=quests.snapshot();var snapshot=quests.getClass().getDeclaredField("snapshot");
|
||||
snapshot.setAccessible(true);snapshot.set(quests,
|
||||
new games.dmg.fantasystick.adventure.AdventureQuestState(old.sites(),old.givers(),
|
||||
null,old.quests(),old.unitClaims(),old.characterClaims(),old.rosters()));
|
||||
var catalog=quests.ensureToday().get(5,java.util.concurrent.TimeUnit.SECONDS);
|
||||
carrier=actor.core.record.id();
|
||||
var site=catalog.sites().getFirst();
|
||||
assertEquals(games.dmg.fantasystick.adventure.AdventureQuestDirectory.Acceptance.APPLIED,
|
||||
quests.accept(catalog.revision(),site,
|
||||
games.dmg.fantasystick.adventure.AdventureQuestState.UnitType.SOLO,carrier,
|
||||
Set.of(carrier)).get(5,java.util.concurrent.TimeUnit.SECONDS).status());
|
||||
var quest=quests.activeFor(carrier,null).orElseThrow();
|
||||
var audit=new java.util.LinkedHashMap<String,String>();
|
||||
audit.put("final_enemy_id",finalEnemy.toString());audit.put("unit_kind","solo");
|
||||
audit.put("unit_id",carrier.toString());audit.put("accepted_roster",carrier.toString());
|
||||
audit.put("arena_shape","circle");audit.put("arena_x_blocks","10");
|
||||
audit.put("arena_z_blocks","10");audit.put("defeat_world_id",actor.world.getUID().toString());
|
||||
audit.put("defeat_world_name","Circle Realm");audit.put("defeat_x","12.0");
|
||||
audit.put("defeat_y","64.0");audit.put("defeat_z","-8.0");
|
||||
audit.put("defeated_at",java.time.Instant.now().truncatedTo(
|
||||
java.time.temporal.ChronoUnit.SECONDS).toString());
|
||||
var plan=new games.dmg.fantasystick.adventure.FishingState.Trophy(receiptId,
|
||||
games.dmg.fantasystick.adventure.FishingState.TrophyType.CAMP,carrier,
|
||||
quest.catalog(),quest.site(),quest.id(),UUID.randomUUID(),
|
||||
"Spoils of Restart Circle","Restart Circle",
|
||||
Map.of(carrier,new games.dmg.fantasystick.adventure.FishingState.Reward(
|
||||
java.math.BigInteger.valueOf(100),java.math.BigInteger.valueOf(20))),
|
||||
games.dmg.fantasystick.adventure.FishingState.TrophyPhase.PREPARED,audit);
|
||||
var fishing=(games.dmg.fantasystick.adventure.FishingDirectory)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishing");
|
||||
assertEquals(games.dmg.fantasystick.adventure.FishingDirectory.TrophyPrepareStatus.APPLIED,
|
||||
fishing.prepareTrophy(plan).get(5,java.util.concurrent.TimeUnit.SECONDS));
|
||||
var file=data.resolve("fishing.yml");var saved=java.nio.file.Files.readAllBytes(file);
|
||||
java.nio.file.Files.delete(file);java.nio.file.Files.createDirectory(file);
|
||||
var controller=(games.dmg.fantasystick.adventure.FishingController)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishingController");
|
||||
assertThrows(java.util.concurrent.ExecutionException.class,()->controller
|
||||
.earnTrophyWithEvidence(plan).toCompletableFuture().get(5,java.util.concurrent.TimeUnit.SECONDS));
|
||||
assertTrue(fishing.trophies(carrier).isEmpty(),"failed commit cannot turn prepared proof into stock");
|
||||
java.nio.file.Files.delete(file);java.nio.file.Files.write(file,saved);
|
||||
f.plugin.setEnabled(false);
|
||||
}
|
||||
try(var restarted=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
restarted.plugin.setEnabled(true);
|
||||
var fishing=(games.dmg.fantasystick.adventure.FishingDirectory)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(restarted.plugin,"fishing");
|
||||
await(restarted,()->!fishing.trophies(carrier).isEmpty());
|
||||
assertEquals(finalEnemy.toString(),fishing.trophies(carrier).getFirst().audit()
|
||||
.get("final_enemy_id"));
|
||||
restarted.plugin.setEnabled(false);
|
||||
}
|
||||
var earned=exportedCampEvents(data).stream().filter(event->event.get("type").getAsString()
|
||||
.endsWith("adventure.trophy_earned")).toList();
|
||||
assertEquals(1,earned.size());
|
||||
var details=earned.getFirst().getAsJsonObject("data");
|
||||
assertEquals(receiptId.toString(),details.get("receipt_id").getAsString());
|
||||
assertEquals(finalEnemy.toString(),details.get("final_enemy_id").getAsString());
|
||||
assertEquals("circle",details.getAsJsonObject("arena").get("shape").getAsString());
|
||||
assertEquals("12.0",details.getAsJsonObject("clear_location").get("x").getAsString());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@@ -533,6 +779,15 @@ class AdventureQuestNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<com.google.gson.JsonObject> exportedCampEvents(Path root)throws Exception{
|
||||
try(var files=java.nio.file.Files.walk(root.resolve("events"))){
|
||||
return files.filter(path->path.toString().endsWith(".jsonl"))
|
||||
.flatMap(path->{try{return java.nio.file.Files.lines(path);}
|
||||
catch(java.io.IOException failure){throw new java.io.UncheckedIOException(failure);}})
|
||||
.map(line->com.google.gson.JsonParser.parseString(line).getAsJsonObject()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
private static void redeemAndClaim(FantasyStickLifecycleNativeTest.Fixture f,games.dmg.fantasystick.adventure.FishingController turn,games.dmg.fantasystick.adventure.FishingDirectory trophies,games.dmg.fantasystick.persistence.CharacterStorage storage,FantasyStickLifecycleNativeTest.Actor actor,java.math.BigInteger initialCoins,long xp,long coins)throws Exception{redeem(f,turn,actor);claim(f,turn,trophies,storage,actor,initialCoins,xp,coins);}
|
||||
private static void redeem(FantasyStickLifecycleNativeTest.Fixture f,games.dmg.fantasystick.adventure.FishingController turn,FantasyStickLifecycleNativeTest.Actor actor)throws Exception{turn.open(actor.player);var menu=actor.inventories.getLast();click(turn,actor,menu,item(menu,"Redeem Spoils of"));await(f,()->actor.inventories.size()>0&&menu!=actor.inventories.getLast());}
|
||||
private static void claim(FantasyStickLifecycleNativeTest.Fixture f,games.dmg.fantasystick.adventure.FishingController turn,games.dmg.fantasystick.adventure.FishingDirectory trophies,games.dmg.fantasystick.persistence.CharacterStorage storage,FantasyStickLifecycleNativeTest.Actor actor,java.math.BigInteger initialCoins,long xp,long coins)throws Exception{turn.open(actor.player);var menu=actor.inventories.getLast();click(turn,actor,menu,item(menu,"Claim earned quest rewards"));await(f,()->trophies.unclaimed(actor.core.record.id()).equals(games.dmg.fantasystick.adventure.FishingState.Reward.empty()));var saved=storage.find(actor.core.record.id()).get().orElseThrow();assertEquals(java.math.BigInteger.valueOf(xp),saved.experience());assertEquals(initialCoins.add(java.math.BigInteger.valueOf(coins)),saved.coins());}
|
||||
|
||||
@@ -37,7 +37,7 @@ class FishingEventBridgePartsNativeTest {
|
||||
carrier+","+zero,"unit_kind","party","unit_id",UUID.randomUUID().toString()));
|
||||
var refused=new FishingEventBridge(event->CompletableFuture.completedFuture(
|
||||
LocalEventLog.Delivery.REJECTED),Clock.systemUTC(),data,mock(EquipmentSessions.class),ignored->{});
|
||||
refused.championEarned(receipt);refused.close();refused.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
refused.trophyEarned(receipt);refused.close();refused.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
var log=LocalEventLog.open(data,Clock.systemUTC(),
|
||||
new LocalEventLog.RuntimeInfo("test","test","test"),128,1_000_000,ignored->{});
|
||||
assertEquals(LocalEventLog.Delivery.EXPORTED,log.ready().toCompletableFuture().get(5,TimeUnit.SECONDS));
|
||||
@@ -63,6 +63,59 @@ class FishingEventBridgePartsNativeTest {
|
||||
.equals("zero_share_completion")).count());
|
||||
}
|
||||
|
||||
@Test void rejectedCampIntentReplaysSavedArenaAndZeroShareIntoExportedJsonl() throws Exception {
|
||||
var zero=UUID.randomUUID();var positive=UUID.randomUUID();var receiptId=UUID.randomUUID();
|
||||
var receipt=new games.dmg.fantasystick.adventure.FishingState.Trophy(receiptId,
|
||||
games.dmg.fantasystick.adventure.FishingState.TrophyType.CAMP,zero,
|
||||
UUID.randomUUID(),UUID.randomUUID(),UUID.randomUUID(),UUID.randomUUID(),
|
||||
"Spoils of Twin Camp","Twin Camp",Map.of(zero,
|
||||
games.dmg.fantasystick.adventure.FishingState.Reward.empty(),positive,
|
||||
new games.dmg.fantasystick.adventure.FishingState.Reward(
|
||||
java.math.BigInteger.ONE,java.math.BigInteger.ZERO)),
|
||||
games.dmg.fantasystick.adventure.FishingState.TrophyPhase.EARNED,
|
||||
Map.of("final_enemy_id",UUID.randomUUID().toString(),"accepted_roster",
|
||||
zero+","+positive,"unit_kind","party","unit_id",UUID.randomUUID().toString(),
|
||||
"arena_shape","circle","arena_x_blocks","10","arena_z_blocks","10"));
|
||||
var refused=new FishingEventBridge(event->CompletableFuture.completedFuture(
|
||||
LocalEventLog.Delivery.REJECTED),Clock.systemUTC(),data,mock(EquipmentSessions.class),ignored->{});
|
||||
var other=new games.dmg.fantasystick.adventure.FishingState.Trophy(UUID.randomUUID(),
|
||||
receipt.type(),receipt.carrier(),UUID.randomUUID(),UUID.randomUUID(),UUID.randomUUID(),
|
||||
UUID.randomUUID(),receipt.name(),receipt.origin(),receipt.rewards(),receipt.phase(),receipt.audit());
|
||||
refused.trophyEarned(receipt);refused.trophyEarned(other);
|
||||
refused.close();refused.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
var log=LocalEventLog.open(data,Clock.systemUTC(),
|
||||
new LocalEventLog.RuntimeInfo("test","test","test"),128,1_000_000,ignored->{});
|
||||
log.ready().toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
var recovered=new FishingEventBridge(log::emit,Clock.systemUTC(),data,
|
||||
mock(EquipmentSessions.class),ignored->{});
|
||||
recovered.replay();recovered.close();recovered.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);
|
||||
log.close();
|
||||
var rows=new ArrayList<com.google.gson.JsonObject>();
|
||||
try(var files=java.nio.file.Files.walk(data.resolve("events"))){
|
||||
for(var path:files.filter(value->value.toString().endsWith(".jsonl")).toList())
|
||||
for(var line:java.nio.file.Files.readAllLines(path))
|
||||
rows.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
|
||||
}
|
||||
var earned=rows.stream().filter(value->value.get("type").getAsString()
|
||||
.endsWith("adventure.trophy_earned")).toList();
|
||||
assertEquals(2,earned.size(),"same-name sites retain independent exported receipts");
|
||||
assertEquals(2,earned.stream().map(event->event.getAsJsonObject("data")
|
||||
.get("site_id").getAsString()).distinct().count());
|
||||
assertEquals(2,earned.stream().map(event->event.getAsJsonObject("data")
|
||||
.get("receipt_id").getAsString()).distinct().count());
|
||||
assertEquals(1,earned.stream().map(event->event.getAsJsonObject("data")
|
||||
.get("trophy_name").getAsString()).distinct().count());
|
||||
var evidence=earned.stream().map(value->value.getAsJsonObject("data"))
|
||||
.filter(value->value.get("receipt_id").getAsString().equals(receiptId.toString()))
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("camp_trophy",evidence.get("trophy_type").getAsString());
|
||||
assertEquals("circle",evidence.getAsJsonObject("arena").get("shape").getAsString());
|
||||
assertEquals(2,evidence.getAsJsonArray("beneficiaries").size());
|
||||
assertEquals(1,evidence.getAsJsonArray("beneficiaries").asList().stream()
|
||||
.filter(value->value.getAsJsonObject().get("allocation_status").getAsString()
|
||||
.equals("zero_share_completion")).count());
|
||||
}
|
||||
|
||||
@Test void largeChampionRosterAndFrozenBeneficiariesKeepSeparateLinkedProof() throws Exception {
|
||||
var published=new ArrayList<EventInput>();var failures=new ArrayList<Throwable>();
|
||||
var bridge=new FishingEventBridge(event->{published.add(event);
|
||||
|
||||
@@ -364,6 +364,68 @@ final class FishingQuestNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void transferredCampTrophyShareIsClaimedOnlyByItsCharacterAtNewHolder() throws Exception {
|
||||
var identities=new UUID[2];var receiptId=UUID.randomUUID();
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
f.plugin.setEnabled(true);var original=new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(original.core.id,original.player);f.activate(original);
|
||||
var character=original.core.record.id();identities[0]=character;
|
||||
var fishing=(FishingDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishing");
|
||||
var trophy=new FishingState.Trophy(receiptId,FishingState.TrophyType.CAMP,
|
||||
character,UUID.randomUUID(),UUID.randomUUID(),UUID.randomUUID(),UUID.randomUUID(),
|
||||
"Spoils of Transfer Camp","Transfer Camp",
|
||||
Map.of(character,new FishingState.Reward(BigInteger.valueOf(7),BigInteger.valueOf(5))));
|
||||
assertEquals(FishingDirectory.TrophyPrepareStatus.APPLIED,
|
||||
fishing.prepareTrophy(trophy).get(5,TimeUnit.SECONDS));
|
||||
assertEquals(FishingDirectory.TrophyCommitStatus.APPLIED,
|
||||
fishing.commitTrophy(receiptId).get(5,TimeUnit.SECONDS));
|
||||
var controller=(FishingController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishingController");
|
||||
controller.open(original.player);var stale=original.inventories.getLast();
|
||||
controller.open(original.player);var menu=original.inventories.getLast();
|
||||
clickFishing(controller,original,stale,offer(stale,"Redeem Spoils of Transfer Camp"));
|
||||
assertEquals(1,fishing.trophies(character).size(),"stale menu cannot consume camp receipt");
|
||||
var file=data.resolve("fishing.yml");var savedFishing=java.nio.file.Files.readAllBytes(file);
|
||||
java.nio.file.Files.delete(file);java.nio.file.Files.createDirectory(file);
|
||||
clickFishing(controller,original,menu,offer(menu,"Redeem Spoils of Transfer Camp"));
|
||||
await(f,()->original.messages.stream().anyMatch(message->message.contains("Trophy redemption failed")));
|
||||
assertEquals(1,fishing.trophies(character).size(),"failed write preserves the exact camp receipt");
|
||||
assertEquals(FishingState.Reward.empty(),fishing.unclaimed(character));
|
||||
java.nio.file.Files.delete(file);java.nio.file.Files.write(file,savedFishing);
|
||||
controller.open(original.player);var retry=original.inventories.getLast();
|
||||
clickFishing(controller,original,retry,offer(retry,"Redeem Spoils of Transfer Camp"));
|
||||
await(f,()->fishing.trophies(character).isEmpty()
|
||||
&&fishing.unclaimed(character).xp().equals(BigInteger.valueOf(7)));
|
||||
var sessions=(games.dmg.fantasystick.session.EquipmentSessions)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");
|
||||
assertEquals(games.dmg.fantasystick.items.EquipmentStorage.RestoreResult.RESTORED,
|
||||
sessions.exit(original.player));f.awaitCharacterAndReceipt(original);
|
||||
var newHolder=new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(newHolder.core.id,newHolder.player);identities[1]=newHolder.core.id;
|
||||
newHolder.core.inventory.setItemInOffHand(original.core.inventory.getItemInOffHand().clone());
|
||||
f.activate(newHolder);
|
||||
assertEquals(character,sessions.active(newHolder.core.id).orElseThrow().character());
|
||||
controller.open(newHolder.player);var claims=newHolder.inventories.getLast();
|
||||
clickFishing(controller,newHolder,claims,offer(claims,"Claim earned quest rewards"));
|
||||
await(f,()->fishing.unclaimed(character).equals(FishingState.Reward.empty()));
|
||||
var saved=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");
|
||||
await(f,()->saved.find(character).join().orElseThrow().experience()
|
||||
.equals(BigInteger.valueOf(7)));
|
||||
assertEquals(original.core.record.coins().add(BigInteger.valueOf(5)),
|
||||
saved.find(character).get(5,TimeUnit.SECONDS).orElseThrow().coins());
|
||||
await(f,()->{try{return exportedClaims().size()==1;}
|
||||
catch(java.io.IOException failure){throw new java.io.UncheckedIOException(failure);}});
|
||||
f.plugin.setEnabled(false);
|
||||
}
|
||||
var claims=exportedClaims();assertEquals(1,claims.size());
|
||||
var claim=claims.getFirst().getAsJsonObject("data");
|
||||
assertEquals(identities[0].toString(),claim.get("character_id").getAsString());
|
||||
assertEquals(identities[1].toString(),claim.get("player_id").getAsString());
|
||||
assertEquals("camp_trophy",claim.getAsJsonArray("sources").get(0)
|
||||
.getAsJsonObject().get("source_kind").getAsString());
|
||||
assertEquals(receiptId.toString(),claim.getAsJsonArray("sources").get(0)
|
||||
.getAsJsonObject().get("source_receipt_id").getAsString());
|
||||
}
|
||||
|
||||
@Test void exactHorizontalRadiusCreditsOnlyTheValidNativeBobberAndExportsItsGeometry() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 1\nfishing-catch-xp-min: 2\nfishing-catch-xp-max: 2\n");
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
|
||||
|
||||
Reference in new issue
Block a user