feat(events): audit stored Magic Core credit journeys
Release / release (push) Failing after 10m9s

This commit is contained in:
dmg committed 2026-09-30 21:45:16 -04:00
1 parent 42cf9e160f
commit 92d3afc2f0
8 files changed
+323 -12

No files matched your search

@@ -834,7 +834,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
downedEvents = new games.dmg.fantasystick.events.DownedEventBridge(eventLog::emit, Clock.systemUTC());
rareEvents = new games.dmg.fantasystick.events.RareEventBridge(eventLog::emit, Clock.systemUTC(),
getDataFolder().toPath(), failure->getLogger().log(Level.WARNING,"Rare event intent failed",failure));
rareShop.attachEvents(rareEvents);rarePotions.attachEvents(rareEvents);rareReskills.attachEvents(rareEvents);admin.attachRareEvents(rareEvents);
rareShop.attachEvents(rareEvents);rarePotions.attachEvents(rareEvents);rareReskills.attachEvents(rareEvents);rareMagicCores.attachEvents(rareEvents);admin.attachRareEvents(rareEvents);
rareEvents.replay();
games.dmg.fantasystick.shop.RareCatalog committedRareCatalog;
while((committedRareCatalog=pendingRareCatalogs.poll())!=null)
@@ -127,6 +127,20 @@ public final class RareEventBridge implements AutoCloseable {
data.put("outcome","acknowledged");
emit(stable(operation,"potion-used"),operation,clock.instant(),"rare_item.used",context,data);
}
public void magicCoreUsed(UUID operation,EventInput.Context context,BigInteger quantityBefore,
games.dmg.fantasystick.persistence.CharacterRecord before,
games.dmg.fantasystick.persistence.CharacterRecord after){
var data=new LinkedHashMap<String,Object>();data.put("actor_kind","player");data.put("product_id","magic-core");
data.put("use_operation_id",operation.toString());data.put("quantity_before",quantityBefore.toString());
data.put("quantity_after",quantityBefore.subtract(BigInteger.ONE).toString());
data.put("points_before",before.unspentPoints().toString());data.put("points_after",after.unspentPoints().toString());
data.put("credited_points",after.unspentPoints().subtract(before.unspentPoints()).toString());
data.put("revision_before",before.revision().toString());data.put("revision_after",after.revision().toString());
data.put("level_before",Integer.toString(before.level()));data.put("level_after",Integer.toString(after.level()));
data.put("xp_before",before.experience().toString());data.put("xp_after",after.experience().toString());
data.put("outcome","acknowledged");
emit(stable(operation,"magic-core-used"),operation,clock.instant(),"rare_item.used",context,data);
}
public void reskillAction(UUID operation,EventInput.Context context,String phase,String outcome,String reason){
var data=new LinkedHashMap<String,Object>();data.put("actor_kind","player");data.put("product_id","reskill");
data.put("action_kind","reskill_use");data.put("use_operation_id",operation.toString());
@@ -1910,7 +1910,7 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
}
return false;
}
private void rareConfiguration(CommandSender sender,String[] args){String key=args[1],before=rareSettings.value(key),actorKind=sender instanceof org.bukkit.entity.Player?"player":sender instanceof org.bukkit.command.BlockCommandSender?"command_block":"console";UUID actorPlayer=sender instanceof org.bukkit.entity.Player player?player.getUniqueId():null;String actorName=sender instanceof org.bukkit.entity.Player?sender.getName():null;boolean phoenix=key.contains("phoenix-down");if(args.length==2){if(phoenix&&downedEvents!=null)downedEvents.configRead(actorKind,actorPlayer,actorName,key,before);else if((potionConfig(key)||key.endsWith("-reskill")||key.startsWith("rare-stock-reskill-"))&&rareAuditEvents!=null)rareAuditEvents.configRead(actorKind,actorPlayer,actorName,key,before);sender.sendMessage(key+"="+before+(key.endsWith("-healing-percent")||key.equals(games.dmg.fantasystick.persistence.RareSettings.PHOENIX_PROTECTION)?" (applies to future accepted uses)":" (applies to the next rare restock only)"));return;}java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.RareSettings.Update> operation;try{operation=rareSettings.set(key,args[2]);}catch(IllegalArgumentException invalid){sender.sendMessage("Rare setting is invalid; percentages are 0–100, values are nonnegative, and stock minima cannot exceed maxima.");return;}operation.whenComplete((saved,failure)->{if(failure!=null)report(null,"config-"+key,failure);if(closed)return;nextTick.accept(()->{boolean applied=failure==null&&rareSettings.publish(saved);if(applied&&phoenix&&downedEvents!=null)downedEvents.configChanged(actorKind,actorPlayer,actorName,key,before,saved.value());else if(applied&&(potionConfig(key)||key.endsWith("-reskill")||key.startsWith("rare-stock-reskill-"))&&rareAuditEvents!=null)rareAuditEvents.configChanged(actorKind,actorPlayer,actorName,key,before,saved.value());if(!sender.hasPermission(PERMISSION)||sender instanceof org.bukkit.entity.Player player&&!player.isOnline())return;if(failure!=null)sender.sendMessage("Rare setting save failed; prior effective state is unchanged.");else if(!applied)sender.sendMessage("Saved request was superseded; effective "+key+"="+rareSettings.value(key)+".");else sender.sendMessage("Saved "+key+"="+saved.value()+"; current catalog and accepted uses are unchanged.");});});}
private void rareConfiguration(CommandSender sender,String[] args){String key=args[1],before=rareSettings.value(key),actorKind=sender instanceof org.bukkit.entity.Player?"player":sender instanceof org.bukkit.command.BlockCommandSender?"command_block":"console";UUID actorPlayer=sender instanceof org.bukkit.entity.Player player?player.getUniqueId():null;String actorName=sender instanceof org.bukkit.entity.Player?sender.getName():null;boolean phoenix=key.contains("phoenix-down");if(args.length==2){if(phoenix&&downedEvents!=null)downedEvents.configRead(actorKind,actorPlayer,actorName,key,before);else if((potionConfig(key)||key.endsWith("-reskill")||key.startsWith("rare-stock-reskill-")||key.endsWith("-magic-core")||key.startsWith("rare-stock-magic-core-"))&&rareAuditEvents!=null)rareAuditEvents.configRead(actorKind,actorPlayer,actorName,key,before);sender.sendMessage(key+"="+before+(key.endsWith("-healing-percent")||key.equals(games.dmg.fantasystick.persistence.RareSettings.PHOENIX_PROTECTION)?" (applies to future accepted uses)":" (applies to the next rare restock only)"));return;}java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.RareSettings.Update> operation;try{operation=rareSettings.set(key,args[2]);}catch(IllegalArgumentException invalid){sender.sendMessage("Rare setting is invalid; percentages are 0–100, values are nonnegative, and stock minima cannot exceed maxima.");return;}operation.whenComplete((saved,failure)->{if(failure!=null)report(null,"config-"+key,failure);if(closed)return;nextTick.accept(()->{boolean applied=failure==null&&rareSettings.publish(saved);if(applied&&phoenix&&downedEvents!=null)downedEvents.configChanged(actorKind,actorPlayer,actorName,key,before,saved.value());else if(applied&&(potionConfig(key)||key.endsWith("-reskill")||key.startsWith("rare-stock-reskill-")||key.endsWith("-magic-core")||key.startsWith("rare-stock-magic-core-"))&&rareAuditEvents!=null)rareAuditEvents.configChanged(actorKind,actorPlayer,actorName,key,before,saved.value());if(!sender.hasPermission(PERMISSION)||sender instanceof org.bukkit.entity.Player player&&!player.isOnline())return;if(failure!=null)sender.sendMessage("Rare setting save failed; prior effective state is unchanged.");else if(!applied)sender.sendMessage("Saved request was superseded; effective "+key+"="+rareSettings.value(key)+".");else sender.sendMessage("Saved "+key+"="+saved.value()+"; current catalog and accepted uses are unchanged.");});});}
private void downedConfiguration(CommandSender sender,String[] args){String key=args[1],before=downedSettings.value(key),actorKind=sender instanceof org.bukkit.entity.Player?"player":sender instanceof org.bukkit.command.BlockCommandSender?"command_block":"console";UUID actorPlayer=sender instanceof org.bukkit.entity.Player player?player.getUniqueId():null;String actorName=sender instanceof org.bukkit.entity.Player?sender.getName():null;if(args.length==2){if(downedEvents!=null)downedEvents.configRead(actorKind,actorPlayer,actorName,key,before);sender.sendMessage(key+"="+before+" (applies to future downed outcomes only)");return;}java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.DownedSettings.Update> operation;try{operation=downedSettings.set(key,args[2]);}catch(IllegalArgumentException invalid){sender.sendMessage("Downed setting is invalid; timeout must be positive and protection may be zero, both at most 365 days.");return;}operation.whenComplete((saved,failure)->{if(failure!=null)report(null,"config-"+key,failure);if(closed)return;nextTick.accept(()->{boolean applied=failure==null&&downedSettings.publish(saved);if(applied&&downedEvents!=null)downedEvents.configChanged(actorKind,actorPlayer,actorName,key,before,saved.value());if(!sender.hasPermission(PERMISSION)||sender instanceof org.bukkit.entity.Player player&&!player.isOnline())return;if(failure!=null)sender.sendMessage("Downed setting save failed; prior effective state is unchanged.");else if(!applied)sender.sendMessage("Saved request was superseded; effective "+key+"="+downedSettings.value(key)+".");else sender.sendMessage("Saved "+key+"="+saved.value()+"; existing deadlines are unchanged.");});});}
private void coinConfiguration(CommandSender sender,String[] args){String key=args[1],before=coinSettings.value(key),actorKind=sender instanceof org.bukkit.entity.Player?"player":sender instanceof org.bukkit.command.BlockCommandSender?"command_block":"console";UUID actorPlayer=sender instanceof org.bukkit.entity.Player player?player.getUniqueId():null;String actorName=sender instanceof org.bukkit.entity.Player?sender.getName():null;if(args.length==2){if(coinEvents!=null)coinEvents.configRead(actorKind,actorPlayer,actorName,key,before);sender.sendMessage(key+"="+before+" (applies to future creation or defeats only)");return;}java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.CoinSettings.Update> operation;try{operation=coinSettings.set(key,args[2]);}catch(IllegalArgumentException invalid){sender.sendMessage("Coin setting is invalid; ranges must be ordered whole numbers from 0 through 2147483647.");return;}operation.whenComplete((saved,failure)->{if(failure!=null)report(null,"config-"+key,failure);if(closed)return;try{nextTick.accept(()->{if(closed)return;boolean applied=failure==null&&coinSettings.publish(saved);if(applied&&coinEvents!=null)coinEvents.configChanged(actorKind,actorPlayer,actorName,key,before,saved.value());if(!sender.hasPermission(PERMISSION)||sender instanceof org.bukkit.entity.Player player&&!player.isOnline())return;if(failure!=null)sender.sendMessage("Coin configuration save failed; effective ranges are unchanged.");else if(!applied)sender.sendMessage("Saved request was superseded; effective "+key+"="+coinSettings.value(key)+".");else sender.sendMessage("Saved "+key+"="+saved.value()+" for future creation or defeats; existing balances and captured rewards are unchanged.");});}catch(RuntimeException rejected){report(null,"config-response",rejected);}});}
@@ -21,6 +21,12 @@ public final class RareMagicCoreService {
private final EquipmentSessions sessions;
private final Consumer<Runnable> nextTick;
private final Consumer<Throwable> failures;
private games.dmg.fantasystick.events.RareEventBridge events;
public void attachEvents(games.dmg.fantasystick.events.RareEventBridge events){
if(this.events!=null)throw new IllegalStateException("Magic Core events already attached");
this.events=Objects.requireNonNull(events);
}
public RareMagicCoreService(RareInventoryDirectory inventory, CharacterStorage characters,
EquipmentSessions sessions, Consumer<Runnable> nextTick,
@@ -38,22 +44,33 @@ public final class RareMagicCoreService {
BigInteger quantity=quantity(captured.record().id());
if(quantity.signum()<=0){result.accept(new Outcome(Status.NOT_AVAILABLE,Optional.of(captured.record())));return;}
UUID operation=UUID.randomUUID();
var actor=events==null?null:safeActor(player,captured);
inventory.reserveUse(captured.record().id(),RareProduct.MAGIC_CORE,operation,quantity).whenComplete((reservation,reserveFailure)->{
if(reserveFailure!=null){fail(reserveFailure,result);return;}
if(reservation.status()!=RareInventoryDirectory.UseStatus.RESERVED){publish(()->result.accept(new Outcome(reservation.status()==RareInventoryDirectory.UseStatus.NOT_AVAILABLE?Status.NOT_AVAILABLE:Status.STALE,Optional.empty())));return;}
characters.consumeMagicCore(captured.record().id(),captured.record().revision(),operation).whenComplete((credit,creditFailure)->complete(player,captured,operation,credit,creditFailure,result));
characters.consumeMagicCore(captured.record().id(),captured.record().revision(),operation).whenComplete((credit,creditFailure)->complete(player,captured,operation,quantity,actor,credit,creditFailure,result));
});
}
/** Resolve interrupted credits from authoritative character proof without publishing a second point. */
public void install(){for(var use:inventory.unresolvedUses()){if(!use.product().equals(RareProduct.MAGIC_CORE.id()))continue;characters.find(use.character()).whenComplete((record,failure)->{if(failure!=null){report(failure);return;}boolean applied=record.isPresent()&&record.orElseThrow().rewardReceipts().contains(use.operation());var work=applied?inventory.commitUse(use.operation()):inventory.refundUse(use.operation());work.exceptionally(problem->{report(problem);return RareInventoryDirectory.UseStatus.RESERVED;});});}}
private void complete(Player player,Context captured,UUID operation,CharacterStorage.MagicCore credit,Throwable failure,Consumer<Outcome> result){
private void complete(Player player,Context captured,UUID operation,BigInteger quantity,
games.dmg.fantasystick.events.EventInput.Context actor,CharacterStorage.MagicCore credit,Throwable failure,Consumer<Outcome> result){
if(failure!=null||credit==null){if(failure!=null)report(failure);refund(operation,()->result.accept(new Outcome(Status.FAILED,Optional.empty())));return;}
if(credit.status()!=CharacterStorage.MagicCoreStatus.APPLIED&&credit.status()!=CharacterStorage.MagicCoreStatus.DUPLICATE){var status=credit.status()==CharacterStorage.MagicCoreStatus.UNAVAILABLE?Status.NOT_AVAILABLE:Status.STALE;refund(operation,()->result.accept(new Outcome(status,credit.record())));return;}
publish(()->{boolean acknowledged=false;try{var current=context(player);acknowledged=current.isPresent()&&current.orElseThrow().active().session().equals(captured.active().session())&&(credit.status()==CharacterStorage.MagicCoreStatus.DUPLICATE||sessions.acknowledgeProgress(captured.active(),captured.record().revision(),credit.record().orElseThrow()));}catch(RuntimeException uncertain){report(uncertain);}boolean currentSession=acknowledged;inventory.commitUse(operation).whenComplete((ignored,commitFailure)->{if(commitFailure!=null){report(commitFailure);publish(()->result.accept(new Outcome(Status.FAILED,credit.record())));}else publish(()->result.accept(new Outcome(currentSession?Status.APPLIED:Status.STALE,credit.record())));});});
publish(()->{boolean acknowledged=false;try{var current=context(player);acknowledged=current.isPresent()&&current.orElseThrow().active().session().equals(captured.active().session())&&(credit.status()==CharacterStorage.MagicCoreStatus.DUPLICATE||sessions.acknowledgeProgress(captured.active(),captured.record().revision(),credit.record().orElseThrow()));}catch(RuntimeException uncertain){report(uncertain);}boolean currentSession=acknowledged;inventory.commitUse(operation).whenComplete((commitStatus,commitFailure)->{if(commitFailure!=null){report(commitFailure);publish(()->result.accept(new Outcome(Status.FAILED,credit.record())));}else {
if(currentSession&&actor!=null&&credit.status()==CharacterStorage.MagicCoreStatus.APPLIED
&&commitStatus==RareInventoryDirectory.UseStatus.COMMITTED)
events.magicCoreUsed(operation,actor,quantity,captured.record(),credit.record().orElseThrow());
publish(()->result.accept(new Outcome(currentSession?Status.APPLIED:Status.STALE,credit.record())));
}});});
}
private games.dmg.fantasystick.events.EventInput.Context safeActor(Player player,Context captured){
try{return events.potionContext(player,captured.record().id(),captured.record().name(),captured.active().session());}
catch(RuntimeException failure){report(failure);return null;}
}
private void refund(UUID operation,Runnable after){inventory.refundUse(operation).whenComplete((ignored,failure)->{if(failure!=null)report(failure);publish(after);});}
private Optional<Context> context(Player player){var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);var record=active==null?null:sessions.currentRecord(active).filter(value->!value.dead()).orElse(null);return record==null?Optional.empty():Optional.of(new Context(active,record));}
private void fail(Throwable failure,Consumer<Outcome> result){report(failure);publish(()->result.accept(new Outcome(Status.FAILED,Optional.empty())));}
@@ -20,7 +20,7 @@ public final class RareShop {
public void attachEvents(games.dmg.fantasystick.events.RareEventBridge events){if(this.events!=null)throw new IllegalStateException("Rare events already attached");this.events=Objects.requireNonNull(events);}
public Optional<CharacterRecord> active(Player player,UUID shop){return context(player,shop).map(Context::record);}public Optional<UUID> activeSession(Player player,UUID shop){return context(player,shop).map(value->value.active().session());}public Instant now(){return market.now();}
public void view(Player player,UUID shopId,Consumer<Optional<View>> result){var captured=context(player,shopId);if(captured.isEmpty()){result.accept(Optional.empty());return;}market.current().whenComplete((catalog,failure)->{if(failure!=null){report(failure);publish(()->result.accept(Optional.empty()));return;}publish(()->{var current=context(player,shopId);if(current.isEmpty()||!current.orElseThrow().active().session().equals(captured.orElseThrow().active().session()))result.accept(Optional.empty());else result.accept(Optional.of(new View(current.orElseThrow().record(),current.orElseThrow().active().session(),catalog)));});});}
public void buy(Player player,UUID shopId,UUID catalogRevision,RareProduct product,Consumer<Outcome> result){var lease=shops.acquire(shopId);if(lease.isEmpty()){result.accept(new Outcome(Status.INVALID_SESSION,Optional.empty()));return;}var held=lease.orElseThrow();var context=context(player,shopId);if(context.isEmpty()){held.close();result.accept(new Outcome(Status.INVALID_SESSION,Optional.empty()));return;}var captured=context.orElseThrow();var auditLocation=product==RareProduct.RESKILL?purchaseLocation(player,captured):null;market.purchase(captured.record().id(),captured.record().revision(),catalogRevision,product).whenComplete((purchase,failure)->{if(failure!=null){report(failure);publishHeld(held,()->result.accept(new Outcome(Status.FAILED,Optional.empty())));return;}publishHeld(held,()->{var current=context(player,shopId);if(current.isEmpty()||!current.orElseThrow().active().session().equals(captured.active().session())){result.accept(new Outcome(Status.STALE,purchase.record()));return;}var status=switch(purchase.status()){case APPLIED->Status.APPLIED;case SOLD_OUT->Status.SOLD_OUT;case INSUFFICIENT->Status.INSUFFICIENT;case STALE->Status.STALE;case UNAVAILABLE,FAILED->Status.FAILED;};if(purchase.status()==RareMarket.PurchaseStatus.APPLIED&&purchase.record().isPresent()){sessions.acknowledgeProgress(captured.active(),captured.record().revision(),purchase.record().orElseThrow());if((product.heals()||product==RareProduct.PHOENIX_DOWN||product==RareProduct.RESKILL)&&purchase.proof()!=null&&events!=null){var after=inventory.quantity(captured.record().id(),product);var saved=purchase.record().orElseThrow();events.purchased(product,purchase.proof().operation(),purchase.catalog().revision(),shopId,player.getUniqueId(),player.getName(),captured.record().id(),captured.record().name(),captured.active().session(),purchase.proof().price(),purchase.proof().stockAfter()+1,purchase.proof().stockAfter(),after.subtract(java.math.BigInteger.ONE),after,captured.record().coins(),saved.coins(),captured.record().revision(),saved.revision(),auditLocation);}}result.accept(new Outcome(status,purchase.record()));});});}
public void buy(Player player,UUID shopId,UUID catalogRevision,RareProduct product,Consumer<Outcome> result){var lease=shops.acquire(shopId);if(lease.isEmpty()){result.accept(new Outcome(Status.INVALID_SESSION,Optional.empty()));return;}var held=lease.orElseThrow();var context=context(player,shopId);if(context.isEmpty()){held.close();result.accept(new Outcome(Status.INVALID_SESSION,Optional.empty()));return;}var captured=context.orElseThrow();var auditLocation=(product==RareProduct.RESKILL||product==RareProduct.MAGIC_CORE)?purchaseLocation(player,captured):null;market.purchase(captured.record().id(),captured.record().revision(),catalogRevision,product).whenComplete((purchase,failure)->{if(failure!=null){report(failure);publishHeld(held,()->result.accept(new Outcome(Status.FAILED,Optional.empty())));return;}publishHeld(held,()->{var current=context(player,shopId);if(current.isEmpty()||!current.orElseThrow().active().session().equals(captured.active().session())){result.accept(new Outcome(Status.STALE,purchase.record()));return;}var status=switch(purchase.status()){case APPLIED->Status.APPLIED;case SOLD_OUT->Status.SOLD_OUT;case INSUFFICIENT->Status.INSUFFICIENT;case STALE->Status.STALE;case UNAVAILABLE,FAILED->Status.FAILED;};if(purchase.status()==RareMarket.PurchaseStatus.APPLIED&&purchase.record().isPresent()){sessions.acknowledgeProgress(captured.active(),captured.record().revision(),purchase.record().orElseThrow());if((product.heals()||product==RareProduct.PHOENIX_DOWN||product==RareProduct.RESKILL||product==RareProduct.MAGIC_CORE)&&purchase.proof()!=null&&events!=null){var after=inventory.quantity(captured.record().id(),product);var saved=purchase.record().orElseThrow();events.purchased(product,purchase.proof().operation(),purchase.catalog().revision(),shopId,player.getUniqueId(),player.getName(),captured.record().id(),captured.record().name(),captured.active().session(),purchase.proof().price(),purchase.proof().stockAfter()+1,purchase.proof().stockAfter(),after.subtract(java.math.BigInteger.ONE),after,captured.record().coins(),saved.coins(),captured.record().revision(),saved.revision(),auditLocation);}}result.accept(new Outcome(status,purchase.record()));});});}
private games.dmg.fantasystick.events.EventInput.Location purchaseLocation(Player player,Context captured){
if(events==null)return null;
try{return events.potionContext(player,captured.record().id(),captured.record().name(),captured.active().session()).location();}
@@ -0,0 +1,203 @@
package games.dmg.fantasystick;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
import com.google.gson.JsonObject;
import games.dmg.fantasystick.persistence.*;
import games.dmg.fantasystick.shop.*;
import games.dmg.fantasystick.session.EquipmentSessions;
import games.dmg.fantasystick.progression.Stat;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class MagicCoreEventsNativeTest {
@TempDir Path data;
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
@Test void registeredCoreConfigurationExportsReadsAndOnlyCommittedChanges() throws Exception {
try (var f=new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
((games.dmg.fantasystick.events.LocalEventLog)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"eventLog"))
.ready().toCompletableFuture().get(5,TimeUnit.SECONDS);
var sender=new EquipmentAdminCommandNativeTest.Sender();
var keys=List.of("rare-price-magic-core","rare-stock-magic-core-min","rare-stock-magic-core-max");
for(var key:keys){
assertTrue(f.command.tabComplete(sender.port,"fstickadmin",new String[]{"config","rare-"}).contains(key));
f.command.execute(sender.port,"fstickadmin",new String[]{"config",key});
}
int before=sender.messages.size();
f.command.execute(sender.port,"fstickadmin",new String[]{"config",keys.getFirst(),"3001"});
ReskillEventsNativeTest.await(f,()->sender.messages.size()>before);
try(var channel=java.nio.channels.FileChannel.open(data.resolve(".config.lock"),
StandardOpenOption.CREATE,StandardOpenOption.WRITE);var lock=channel.lock()){
assertTrue(lock.isValid());int messages=sender.messages.size();
f.command.execute(sender.port,"fstickadmin",new String[]{"config",keys.getFirst(),"3002"});
ReskillEventsNativeTest.await(f,()->sender.messages.size()>messages);
assertTrue(sender.messages.getLast().contains("save failed"));
}
f.command.execute(sender.port,"fstickadmin",new String[]{"config",keys.getFirst(),"-1"});
sender.allowed=false;
f.command.execute(sender.port,"fstickadmin",new String[]{"config",keys.getFirst(),"3003"});
ReskillEventsNativeTest.await(f,()->config().size()==4);
f.plugin.setEnabled(false);f.awaitWorkers();
}
var rows=config();assertEquals(3,rows.stream().filter(row->row.get("type").getAsString().endsWith("config.read")).count());
var changes=rows.stream().filter(row->row.get("type").getAsString().endsWith("config.changed")).toList();
assertEquals(1,changes.size());var proof=changes.getFirst().getAsJsonObject("data");
assertEquals("3000",proof.get("before").getAsString());assertEquals("3001",proof.get("after").getAsString());
assertEquals("next_rare_restock",proof.get("application").getAsString());
}
@Test void purchasedCoresAtMaxLevelReachRealBookSpendingResetTransferAndJsonl() throws Exception {
Files.writeString(data.resolve("config.yml"),"rare-price-variance-percent: 0\nrare-price-magic-core: 3000\n"
+"rare-stock-magic-core-min: 5\nrare-stock-magic-core-max: 5\nrare-price-reskill: 2000\n"
+"rare-stock-reskill-min: 1\nrare-stock-reskill-max: 1\n");
var initial=BigInteger.TWO.pow(100);UUID character,newHolder;
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
f.plugin.setEnabled(true);var a=eligibleActor(f,initial);character=a.core.record.id();
var inventory=(RareInventoryDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareInventory");
var directory=(RareMerchantDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMerchants");
UUID world=UUID.randomUUID();when(a.world.getUID()).thenReturn(world);
var shop=directory.place("Core market",world,a.core.location.getX(),a.core.location.getY(),a.core.location.getZ(),0,0).join();
for(int count=1;count<=5;count++){
buy(f,a,shop.id(),14);int expected=count;
ReskillEventsNativeTest.await(f,()->inventory.quantity(character,RareProduct.MAGIC_CORE).equals(BigInteger.valueOf(expected))
&&current(f,a).coins().equals(BigInteger.valueOf(17000L-expected*3000L)));
assertEquals(initial,current(f,a).unspentPoints(),"Buying a Core grants no point");
}
var rare=(RareShop)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareShop");
var noStock=new java.util.concurrent.atomic.AtomicReference<RareShop.Outcome>();
var market=(RareMarket)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMarket");
rare.buy(a.player,shop.id(),market.current().toCompletableFuture().join().revision(),RareProduct.MAGIC_CORE,noStock::set);
ReskillEventsNativeTest.await(f,()->noStock.get()!=null);assertEquals(RareShop.Status.SOLD_OUT,noStock.get().status());
useCore(f,a,initial.add(BigInteger.ONE));
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,PartyBookNativeTest.button(a,"[+1 STR]"));
ReskillEventsNativeTest.await(f,()->current(f,a).stats().get(Stat.STR).equals(BigInteger.valueOf(13)));
useCore(f,a,initial.add(BigInteger.ONE));
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Learn]","great_sword"));
ReskillEventsNativeTest.await(f,()->current(f,a).learned().contains("great_sword"));
useCore(f,a,initial.add(BigInteger.ONE));
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Learn]","quicker_regeneration_i"));
ReskillEventsNativeTest.await(f,()->current(f,a).passives().contains("quicker_regeneration_i"));
useCore(f,a,initial.add(BigInteger.ONE));
buy(f,a,shop.id(),13);
ReskillEventsNativeTest.await(f,()->inventory.quantity(character,RareProduct.RESKILL).equals(BigInteger.ONE)
&&current(f,a).coins().signum()==0);
PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Use]","reskill"));
ReskillEventsNativeTest.await(f,()->PartyBookNativeTest.text(a).contains("Confirm Reskill"));
PartyBookNativeTest.click(f,a,PartyBookNativeTest.button(a,"[Confirm Reskill]"));
ReskillEventsNativeTest.await(f,()->inventory.quantity(character,RareProduct.RESKILL).signum()==0
&&current(f,a).unspentPoints().equals(initial.add(BigInteger.valueOf(40))));
assertEquals(BigInteger.ONE,inventory.quantity(character,RareProduct.MAGIC_CORE),"Reset preserves the unused Core");
assertTrue(current(f,a).learned().isEmpty());assertTrue(current(f,a).passives().stream().allMatch(String::isEmpty));
var sessions=(EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");
assertEquals(games.dmg.fantasystick.items.EquipmentStorage.RestoreResult.RESTORED,sessions.exit(a.player));
f.awaitCharacterAndReceipt(a);
var b=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(b.core.id,b.player);newHolder=b.core.id;
b.core.inventory.setItemInOffHand(a.core.inventory.getItemInOffHand().clone());f.activate(b);
assertEquals(character,sessions.active(newHolder).orElseThrow().character());
useCore(f,b,initial.add(BigInteger.valueOf(41)));
ReskillEventsNativeTest.await(f,()->credits().size()==5);
f.plugin.setEnabled(false);f.awaitWorkers();f.plugin.setEnabled(true);
assertEquals(initial.add(BigInteger.valueOf(41)),new YamlCharacterRepository(data).load(character).orElseThrow().unspentPoints());
f.plugin.setEnabled(false);f.awaitWorkers();
}
var rows=ReskillEventsNativeTest.records(data);var purchased=rows.stream().filter(row->row.get("type").getAsString()
.endsWith("rare_item.purchased")&&row.getAsJsonObject("data").get("product_id").getAsString().equals("magic-core")).toList();
assertEquals(5,purchased.size());assertEquals(5,purchased.stream().map(row->row.get("id").getAsString()).distinct().count());
var credits=credits();assertEquals(5,credits.size());assertEquals(5,credits.stream().map(row->row.get("id").getAsString()).distinct().count());
for(var credit:credits){var proof=credit.getAsJsonObject("data");
assertEquals(BigInteger.ONE,new BigInteger(proof.get("points_after").getAsString()).subtract(new BigInteger(proof.get("points_before").getAsString())));
assertEquals("30",proof.get("level_before").getAsString());assertEquals(proof.get("level_before"),proof.get("level_after"));
assertEquals(proof.get("xp_before"),proof.get("xp_after"));assertTrue(proof.has("location"));
}
assertEquals(1,credits.stream().filter(row->row.getAsJsonObject("data").get("player_id").getAsString().equals(newHolder.toString())).count());
assertEquals(1,rows.stream().filter(row->row.get("type").getAsString().endsWith("rare_catalog.refreshed")).count(),
"Restart retains the full global catalog; Core does not emit another refresh");
}
@Test void failedPointSaveRefundsCoreAndThrowingAuditCannotDuplicateOrUndoCredit() throws Exception {
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
f.plugin.setEnabled(true);var a=eligibleActor(f,BigInteger.valueOf(100));
var inventory=(RareInventoryDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareInventory");
inventory.add(a.core.record.id(),RareProduct.MAGIC_CORE,UUID.randomUUID()).join();
try(var channel=java.nio.channels.FileChannel.open(data.resolve("characters/.records.lock"),StandardOpenOption.WRITE);
var lock=channel.lock()){
assertTrue(lock.isValid());PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,button(a,"[Use]","magic-core"));
ReskillEventsNativeTest.await(f,()->a.messages.stream().anyMatch(message->message.contains("Magic Core could not be acknowledged")));
assertEquals(BigInteger.valueOf(100),current(f,a).unspentPoints());
assertEquals(BigInteger.ONE,inventory.quantity(a.core.record.id(),RareProduct.MAGIC_CORE));assertTrue(credits().isEmpty());
}
var characters=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");
var sessions=(EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");
var service=new RareMagicCoreService(inventory,characters,sessions,f.tasks::add,ignored->{});
var attempts=new ArrayList<games.dmg.fantasystick.events.EventInput>();
service.attachEvents(new games.dmg.fantasystick.events.RareEventBridge(event->{attempts.add(event);throw new IllegalStateException("audit failed");},java.time.Clock.systemUTC()));
var outcome=new java.util.concurrent.atomic.AtomicReference<RareMagicCoreService.Outcome>();service.use(a.player,outcome::set);
ReskillEventsNativeTest.await(f,()->outcome.get()!=null);assertEquals(RareMagicCoreService.Status.APPLIED,outcome.get().status());
assertEquals(BigInteger.valueOf(101),current(f,a).unspentPoints());assertEquals(1,attempts.size());
outcome.set(null);service.use(a.player,outcome::set);assertEquals(RareMagicCoreService.Status.NOT_AVAILABLE,outcome.get().status());
assertEquals(BigInteger.valueOf(101),current(f,a).unspentPoints());assertEquals(1,attempts.size());
f.plugin.setEnabled(false);f.awaitWorkers();
}
assertTrue(credits().isEmpty(),"A throwing non-durable test sink makes no export claim");
}
private FantasyStickLifecycleNativeTest.Actor eligibleActor(FantasyStickLifecycleNativeTest.Fixture f,BigInteger points) throws Exception {
var a=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(a.core.id,a.player);
var repository=new YamlCharacterRepository(data);var old=repository.load(a.core.record.id()).orElseThrow();
var stats=new EnumMap<>(old.stats());for(var stat:List.of(Stat.STR,Stat.CON,Stat.WIS))stats.put(stat,BigInteger.valueOf(12));
repository.replace(new CharacterRecord(old.id(),old.name(),old.revision().add(BigInteger.ONE),stats,30,BigInteger.ZERO,
points,new BigDecimal("22"),old.learned(),old.loadout(),old.passives(),old.cooldowns(),old.restrictedKills(),false,
null,null,BigInteger.valueOf(17000),true,old.rewardReceipts(),old.pvpCoinEscrow(),old.gear(),old.sheetGeneration()),old.revision());
f.activate(a);return a;
}
private static CharacterRecord current(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a){
var sessions=(EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");
return sessions.currentRecord(sessions.active(a.core.id).orElseThrow()).orElseThrow();
}
private void useCore(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a,BigInteger expected) throws Exception {
PartyBookNativeTest.open(f,a);var action=button(a,"[Use]","magic-core");PartyBookNativeTest.click(f,a,action);
ReskillEventsNativeTest.await(f,()->current(f,a).unspentPoints().equals(expected));
PartyBookNativeTest.click(f,a,action);
}
private static void buy(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor a,UUID shop,int slot) throws Exception {
var menus=(RareShopMenus)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareShopMenus");int size=a.inventories.size();
menus.open(a.player,shop);ReskillEventsNativeTest.await(f,()->a.inventories.size()>size);
var menu=a.inventories.getLast();var click=mock(InventoryClickEvent.class);
when(click.getInventory()).thenReturn(menu);when(click.getWhoClicked()).thenReturn(a.player);when(click.getCurrentItem()).thenReturn(menu.getItem(slot));
menus.click(click);
}
private static net.minecraft.network.chat.ClickEvent.Custom button(FantasyStickLifecycleNativeTest.Actor a,String label,String identity){
var content=org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(a.books.getLast())
.get(net.minecraft.core.component.DataComponents.WRITTEN_BOOK_CONTENT);
for(var page:content.pages()){var found=find(page.raw(),label,identity);if(found!=null)return found;}
throw new AssertionError("Missing native book action "+label+" for "+identity);
}
private static net.minecraft.network.chat.ClickEvent.Custom find(net.minecraft.network.chat.Component component,String label,String identity){
if(component.getString().equals(label)&&component.getStyle().getClickEvent() instanceof net.minecraft.network.chat.ClickEvent.Custom custom
&&custom.payload().orElse(null) instanceof net.minecraft.nbt.StringTag payload&&payload.value().contains(":"+identity))return custom;
for(var child:component.getSiblings()){var found=find(child,label,identity);if(found!=null)return found;}return null;
}
private List<JsonObject> credits(){
try{return ReskillEventsNativeTest.records(data).stream().filter(row->row.get("type").getAsString().endsWith("rare_item.used")
&&row.getAsJsonObject("data").get("product_id").getAsString().equals("magic-core")).toList();}
catch(Exception failure){throw new IllegalStateException(failure);}
}
private List<JsonObject> config(){
try{return ReskillEventsNativeTest.records(data).stream().filter(row->row.getAsJsonObject("data").has("key")
&&row.getAsJsonObject("data").get("key").getAsString().contains("magic-core")).toList();}
catch(Exception failure){throw new IllegalStateException(failure);}
}
}
@@ -47,7 +47,9 @@ class RareShopNativeTest {
&&hasRareEvent(events,"rare_shop.placed","shop_id",shop.id().toString())
&&hasRareEvent(events,"rare_item.purchased","product_id","health-potion")
&&hasRareEvent(events,"rare_item.purchased","product_id","phoenix-down")
&&hasRareEvent(events,"rare_item.used","product_id","health-potion");
&&hasRareEvent(events,"rare_item.used","product_id","health-potion")
&&hasRareEvent(events,"rare_item.purchased","product_id","magic-core")
&&hasRareEvent(events,"rare_item.used","product_id","magic-core");
});
f.plugin.setEnabled(false);f.awaitWorkers();
var exported=new java.util.ArrayList<com.google.gson.JsonObject>();
@@ -85,14 +87,32 @@ class RareShopNativeTest {
.findFirst().orElseThrow();
assertEquals("20",resetOffer.get("price").getAsString());
assertEquals("2",resetOffer.get("stock").getAsString());
var coreOffer=products.asList().stream().map(com.google.gson.JsonElement::getAsJsonObject)
.filter(product->product.get("product_id").getAsString().equals("magic-core"))
.findFirst().orElseThrow();
assertEquals("30",coreOffer.get("price").getAsString());
assertEquals("1",coreOffer.get("stock").getAsString());
}
assertTrue(exported.stream().anyMatch(event->event.get("type").getAsString()
.endsWith("rare_item.purchased")&&event.getAsJsonObject("data")
.get("product_id").getAsString().equals("phoenix-down")));
assertFalse(exported.stream().anyMatch(event->event.get("type").getAsString()
.endsWith("rare_item.purchased")&&event.getAsJsonObject("data")
.get("product_id").getAsString().equals("magic-core")),
"US-088 purchase events wait for their own implementation");
var corePurchases=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("rare_item.purchased")&&event.getAsJsonObject("data").get("product_id")
.getAsString().equals("magic-core")).toList();
assertEquals(1,corePurchases.size());var corePurchase=corePurchases.getFirst().getAsJsonObject("data");
assertEquals("30",corePurchase.get("price").getAsString());
assertEquals("90",corePurchase.get("coins_before").getAsString());
assertEquals("60",corePurchase.get("coins_after").getAsString());
assertEquals("1",corePurchase.get("stock_before").getAsString());
assertEquals("0",corePurchase.get("stock_after").getAsString());
assertEquals(worldId.toString(),corePurchase.getAsJsonObject("location").get("world_id").getAsString());
var coreUses=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("rare_item.used")&&event.getAsJsonObject("data").get("product_id")
.getAsString().equals("magic-core")).toList();
assertEquals(1,coreUses.size());var coreProof=coreUses.getFirst().getAsJsonObject("data");
assertEquals("39",coreProof.get("points_before").getAsString());
assertEquals("40",coreProof.get("points_after").getAsString());
assertEquals("1",coreProof.get("credited_points").getAsString());
var resetPurchases=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("rare_item.purchased")&&event.getAsJsonObject("data").get("product_id")
.getAsString().equals("reskill")).toList();
@@ -219,7 +239,21 @@ class RareShopNativeTest {
}
@Test void magicCoreUseAddsOneVisibleSpendableOrdinaryPoint()throws Exception{
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);f.activate(actor);var inventory=(RareInventoryDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareInventory");inventory.add(actor.core.record.id(),RareProduct.MAGIC_CORE,UUID.randomUUID()).join();PartyBookNativeTest.open(f,actor);String before=PartyBookNativeTest.text(actor);assertTrue(before.contains("Magic Core: 1"),before);PartyBookNativeTest.click(f,actor,PartyBookNativeTest.button(actor,"[Use]"));var sessions=(games.dmg.fantasystick.session.EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");await(f,()->inventory.quantity(actor.core.record.id(),RareProduct.MAGIC_CORE).signum()==0&&sessions.active(actor.core.id).flatMap(sessions::currentRecord).map(record->record.unspentPoints().equals(BigInteger.valueOf(41))).orElse(false));var active=sessions.active(actor.core.id).orElseThrow();var record=sessions.currentRecord(active).orElseThrow();var characters=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");var spent=characters.purchaseStat(record.id(),record.revision(),games.dmg.fantasystick.progression.Stat.STR,BigDecimal.valueOf(actor.player.getHealth()),new BigDecimal("1024")).join().record().orElseThrow();assertTrue(sessions.acknowledgeProgress(active,record.revision(),spent));assertEquals(BigInteger.ONE,spent.stats().get(games.dmg.fantasystick.progression.Stat.STR));assertEquals(BigInteger.valueOf(40),spent.unspentPoints());}
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);f.activate(actor);var inventory=(RareInventoryDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareInventory");inventory.add(actor.core.record.id(),RareProduct.MAGIC_CORE,UUID.randomUUID()).join();PartyBookNativeTest.open(f,actor);String before=PartyBookNativeTest.text(actor);assertTrue(before.contains("Magic Core: 1"),before);PartyBookNativeTest.click(f,actor,PartyBookNativeTest.button(actor,"[Use]"));var sessions=(games.dmg.fantasystick.session.EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");await(f,()->inventory.quantity(actor.core.record.id(),RareProduct.MAGIC_CORE).signum()==0&&sessions.active(actor.core.id).flatMap(sessions::currentRecord).map(record->record.unspentPoints().equals(BigInteger.valueOf(41))).orElse(false));var active=sessions.active(actor.core.id).orElseThrow();var record=sessions.currentRecord(active).orElseThrow();var characters=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");var spent=characters.purchaseStat(record.id(),record.revision(),games.dmg.fantasystick.progression.Stat.STR,BigDecimal.valueOf(actor.player.getHealth()),new BigDecimal("1024")).join().record().orElseThrow();assertTrue(sessions.acknowledgeProgress(active,record.revision(),spent));assertEquals(BigInteger.ONE,spent.stats().get(games.dmg.fantasystick.progression.Stat.STR));assertEquals(BigInteger.valueOf(40),spent.unspentPoints());
await(f,()->hasRareEvent(exportedRareEvents(data),"rare_item.used","product_id","magic-core"));
f.plugin.setEnabled(false);f.awaitWorkers();
var credits=exportedRareEvents(data).stream().filter(event->event.get("type").getAsString()
.endsWith("rare_item.used")&&event.getAsJsonObject("data").get("product_id").getAsString()
.equals("magic-core")).toList();
assertEquals(1,credits.size());var proof=credits.getFirst().getAsJsonObject("data");
assertEquals("40",proof.get("points_before").getAsString());
assertEquals("41",proof.get("points_after").getAsString());
assertEquals("1",proof.get("quantity_before").getAsString());
assertEquals("0",proof.get("quantity_after").getAsString());
assertEquals(proof.get("level_before"),proof.get("level_after"));
assertEquals(proof.get("xp_before"),proof.get("xp_after"));
assertEquals(actor.core.record.id().toString(),proof.get("character_id").getAsString());
assertTrue(proof.has("location"));}
}
@Test void reskillWithNothingInvestedRefusesWithoutConsumingTheStoredItem()throws Exception{
@@ -161,6 +161,49 @@ final class RareEventBridgeTest {
assertEquals(after,repository.load(before.id()).orElseThrow(),"Audit replay cannot reset/refund a character again");
}
@Test void rejectedCoreCreditReplaysSameIdAndExactLargeBalancesWithoutRecrediting() throws Exception {
var now=Instant.parse("2026-10-01T01:30:00Z");
var base=new games.dmg.fantasystick.crafting.CharacterFactory().create(games.dmg.fantasystick.progression.ProgressionRules.defaults());
var points=BigInteger.TWO.pow(100);var quantity=BigInteger.TWO.pow(80);
var before=new games.dmg.fantasystick.persistence.CharacterRecord(base.id(),base.name(),base.revision(),base.stats(),
30,BigInteger.ZERO,points,base.health(),base.learned(),base.loadout(),base.passives(),base.cooldowns(),
base.restrictedKills(),false,null,null,base.coins(),true,base.rewardReceipts(),base.pvpCoinEscrow(),base.gear(),base.sheetGeneration());
var repository=new games.dmg.fantasystick.persistence.YamlCharacterRepository(data);repository.create(before);
var operation=UUID.randomUUID();games.dmg.fantasystick.persistence.CharacterRecord after;
try(var storage=new games.dmg.fantasystick.persistence.CharacterStorage(repository,Clock.fixed(now,ZoneOffset.UTC))){
after=storage.consumeMagicCore(before.id(),before.revision(),operation).join().record().orElseThrow();
}
var context=new EventInput.Context(UUID.randomUUID(),"Account",before.id(),before.name(),UUID.randomUUID(),
new EventInput.Location(UUID.randomUUID(),"world",1,64,3,now,"native"));
var attempts=new ArrayList<EventInput>();
var refused=new RareEventBridge(event->{attempts.add(event);return CompletableFuture.completedFuture(LocalEventLog.Delivery.REJECTED);},
Clock.fixed(now,ZoneOffset.UTC),data,ignored->{});
refused.magicCoreUsed(operation,context,quantity,before,after);refused.close();
refused.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);assertEquals(1,attempts.size());
var log=LocalEventLog.open(data,Clock.fixed(now,ZoneOffset.UTC),
new LocalEventLog.RuntimeInfo("test","test","test"),128,1_000_000,ignored->{});
log.ready().toCompletableFuture().get(5,TimeUnit.SECONDS);
var recovered=new RareEventBridge(log::emit,Clock.fixed(now,ZoneOffset.UTC),data,ignored->{});
recovered.replay();recovered.close();recovered.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);log.close();
var rows=new ArrayList<com.google.gson.JsonObject>();
try(var paths=java.nio.file.Files.walk(data.resolve("events"))){
for(var path:paths.filter(value->value.toString().endsWith(".jsonl")).toList())
for(var line:java.nio.file.Files.readAllLines(path)){
var event=com.google.gson.JsonParser.parseString(line).getAsJsonObject();
if(event.get("type").getAsString().endsWith("rare_item.used"))rows.add(event);
}
}
assertEquals(1,rows.size());assertEquals(attempts.getFirst().id().toString(),rows.getFirst().get("id").getAsString());
var proof=rows.getFirst().getAsJsonObject("data");assertEquals("magic-core",proof.get("product_id").getAsString());
assertEquals(operation.toString(),proof.get("use_operation_id").getAsString());
assertEquals(points.toString(),proof.get("points_before").getAsString());
assertEquals(points.add(BigInteger.ONE).toString(),proof.get("points_after").getAsString());
assertEquals(quantity.toString(),proof.get("quantity_before").getAsString());
assertEquals(quantity.subtract(BigInteger.ONE).toString(),proof.get("quantity_after").getAsString());
assertEquals("30",proof.get("level_after").getAsString());assertEquals("0",proof.get("xp_after").getAsString());
assertEquals(after,repository.load(before.id()).orElseThrow());
}
@Test void eventSinkFailureCannotAlterMarketCallers() {
var bridge = new RareEventBridge(value -> { throw new IllegalStateException("disk"); }, Clock.systemUTC());
var offers = new EnumMap<RareProduct, RareCatalog.Offer>(RareProduct.class);