Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ccb34ab6 | ||
|
|
0bdb43b2bb |
No files matched your search
@@ -166,6 +166,8 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
private games.dmg.fantasystick.events.EventCaptureListener eventCapture;
|
||||
private games.dmg.fantasystick.events.DownedEventBridge downedEvents;
|
||||
private games.dmg.fantasystick.events.RareEventBridge rareEvents;
|
||||
private games.dmg.fantasystick.events.BookEventBridge bookEvents;
|
||||
private games.dmg.fantasystick.events.ProgressionEventBridge progressionEvents;
|
||||
private java.util.concurrent.CompletionStage<Void> configurationDrain = java.util.concurrent.CompletableFuture.completedStage(null);
|
||||
|
||||
@Override public void onEnable() {
|
||||
@@ -703,13 +705,15 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
eventCapture = new games.dmg.fantasystick.events.EventCaptureListener(eventLog, sessions, Clock.systemUTC());
|
||||
downedEvents = new games.dmg.fantasystick.events.DownedEventBridge(eventLog::emit, Clock.systemUTC());
|
||||
rareEvents = new games.dmg.fantasystick.events.RareEventBridge(eventLog::emit, Clock.systemUTC());rareShop.attachEvents(rareEvents);
|
||||
bookEvents = new games.dmg.fantasystick.events.BookEventBridge(eventLog::emit, Clock.systemUTC());books.attachEvents(bookEvents);
|
||||
progressionEvents = new games.dmg.fantasystick.events.ProgressionEventBridge(eventLog::emit, Clock.systemUTC());books.attachProgressionEvents(progressionEvents);
|
||||
sessions.attachDownedEvents(downedEvents);
|
||||
downedController.attachEvents(downedEvents);
|
||||
revive.attachDownedEvents(downedEvents);
|
||||
admin.attachDownedEvents(downedEvents);
|
||||
getServer().getPluginManager().registerEvents(eventCapture, this);
|
||||
} catch (java.io.IOException failure) {
|
||||
eventLog = null; eventCapture = null; downedEvents = null; rareEvents = null;
|
||||
eventLog = null; eventCapture = null; downedEvents = null; rareEvents = null; bookEvents = null; progressionEvents = null;
|
||||
getLogger().log(Level.WARNING,"Fantasy Stick local events are unavailable; gameplay remains enabled",failure);
|
||||
}
|
||||
getServer().getPluginManager().registerEvents(crafting, this);
|
||||
@@ -812,7 +816,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
combatFeedback = null;
|
||||
var closingEvents = sessionEvents; sessionEvents = null;
|
||||
var closingEventLog = eventLog; eventLog = null;
|
||||
var closingEventCapture = eventCapture; eventCapture = null; downedEvents = null; rareEvents = null;
|
||||
var closingEventCapture = eventCapture; eventCapture = null; downedEvents = null; rareEvents = null; bookEvents = null; progressionEvents = null;
|
||||
var closingBooks = books; books = null;
|
||||
var closingPartyEvents = partyEvents; partyEvents = null;
|
||||
var closingPartyCombat = partyCombat; partyCombat = null;
|
||||
|
||||
@@ -36,6 +36,9 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
private games.dmg.fantasystick.shop.RareMagicCoreService rareMagicCores;
|
||||
private final org.bukkit.Server server;
|
||||
private final Map<UUID, Object> pending = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, games.dmg.fantasystick.events.BookEventBridge.Request> pendingBookEvents = new ConcurrentHashMap<>();
|
||||
private games.dmg.fantasystick.events.BookEventBridge bookEvents;
|
||||
private games.dmg.fantasystick.events.ProgressionEventBridge progressionEvents;
|
||||
private final Map<UUID, Object> adminPending = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, AdminView> adminViews = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, SavedView> savedViews = new ConcurrentHashMap<>();
|
||||
@@ -61,7 +64,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
games.dmg.fantasystick.session.EquipmentSessions.Active active,
|
||||
int previous, int next) { }
|
||||
private record SavedView(String nonce, UUID administrator, UUID character, int previous, int next) { }
|
||||
private record StatView(String nonce, ControlInput input, java.math.BigInteger revision) { }
|
||||
private record StatView(String nonce, ControlInput input, java.math.BigInteger revision, CharacterRecord before) { }
|
||||
private record AbilityView(String nonce, ControlInput input, java.math.BigInteger revision, java.util.Set<String> ids) { }
|
||||
private record PassiveView(String nonce, ControlInput input, java.math.BigInteger revision, java.util.Set<String> ids) { }
|
||||
private record RareView(String nonce,ControlInput input,java.util.Map<String,java.math.BigInteger> quantities){}
|
||||
@@ -117,7 +120,10 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
public void attachFishingInventory(games.dmg.fantasystick.adventure.FishingDirectory fishing){if(closed||this.fishing!=null)throw new IllegalStateException("Fishing inventory already attached or books closed");this.fishing=Objects.requireNonNull(fishing);}
|
||||
public void attachRareInventory(games.dmg.fantasystick.shop.RareInventoryDirectory inventory,games.dmg.fantasystick.shop.RarePotionService potions,games.dmg.fantasystick.shop.RareReskillService reskills,games.dmg.fantasystick.shop.RareMagicCoreService magicCores){if(closed||rareInventory!=null)throw new IllegalStateException("Rare inventory already attached or books closed");rareInventory=Objects.requireNonNull(inventory);rarePotions=Objects.requireNonNull(potions);rareReskills=Objects.requireNonNull(reskills);rareMagicCores=Objects.requireNonNull(magicCores);}
|
||||
|
||||
@Override public void accept(ControlInput input) { load(input, 0); }
|
||||
public void attachEvents(games.dmg.fantasystick.events.BookEventBridge events){if(closed||bookEvents!=null)throw new IllegalStateException("Book events already attached or controller closed");bookEvents=Objects.requireNonNull(events);}
|
||||
public void attachProgressionEvents(games.dmg.fantasystick.events.ProgressionEventBridge events){if(closed||progressionEvents!=null)throw new IllegalStateException("Progression events already attached or controller closed");progressionEvents=Objects.requireNonNull(events);}
|
||||
|
||||
@Override public void accept(ControlInput input) { load(input, 0, true); }
|
||||
|
||||
public void openReadOnly(org.bukkit.entity.Player administrator, org.bukkit.entity.Player target) {
|
||||
Objects.requireNonNull(administrator); Objects.requireNonNull(target);
|
||||
@@ -233,7 +239,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
});
|
||||
}
|
||||
|
||||
private void load(ControlInput input, int offset) {
|
||||
private void load(ControlInput input, int offset, boolean userRequested) {
|
||||
if (closed || input.kind() != ControlInput.Kind.BOOK || input.gesture() != ControlInput.Gesture.USE
|
||||
|| !input.stillCurrent()) return;
|
||||
var owner = input.session().player();
|
||||
@@ -244,32 +250,45 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
passiveViews.remove(owner);
|
||||
rareViews.remove(owner);
|
||||
reskillViews.remove(owner);
|
||||
var current=sessions.currentRecord(input.session()).orElse(null);
|
||||
var audit=userRequested&&bookEvents!=null&¤t!=null?bookEvents.request(input,current,offset):null;
|
||||
var superseded=pendingBookEvents.remove(owner);if(superseded!=null)bookEvents.refused(superseded,"superseded");
|
||||
if(audit!=null)pendingBookEvents.put(owner,audit);
|
||||
Object ticket = new Object();
|
||||
pending.put(owner, ticket);
|
||||
characters.find(input.session().character()).whenComplete((record, failure) -> {
|
||||
if (closed || pending.get(owner) != ticket) return;
|
||||
try {
|
||||
nextTick.accept(() -> {
|
||||
if (closed || !pending.remove(owner, ticket) || !input.stillCurrent()) return;
|
||||
if (closed || !pending.remove(owner, ticket)) return;
|
||||
if (!input.stillCurrent()) { finishRefused(owner,audit,"stale_session"); return; }
|
||||
if (failure != null) {
|
||||
finishFailed(owner,audit,"load_failed");
|
||||
report(input, failure);
|
||||
input.player().sendMessage("Character book could not be loaded; no character state was changed.");
|
||||
return;
|
||||
}
|
||||
if (record.isEmpty() || record.orElseThrow().dead()) {
|
||||
finishRefused(owner,audit,"character_unavailable");
|
||||
input.player().sendMessage("This character is no longer available.");
|
||||
return;
|
||||
}
|
||||
try { input.player().openBook(render(record.orElseThrow(), input, offset)); }
|
||||
catch (RuntimeException error) { partyControls.forget(owner); views.remove(owner); report(input, error); }
|
||||
try {
|
||||
var book=render(record.orElseThrow(), input, offset);input.player().openBook(book);
|
||||
if(audit!=null&&pendingBookEvents.remove(owner,audit)){var pages=((BookMeta)book.getItemMeta()).pages().size();bookEvents.opened(audit,record.orElseThrow(),pages,1);}
|
||||
}
|
||||
catch (RuntimeException error) { finishFailed(owner,audit,"native_open_failed");partyControls.forget(owner); views.remove(owner); report(input, error); }
|
||||
});
|
||||
} catch (RuntimeException rejected) {
|
||||
pending.remove(owner, ticket);
|
||||
pending.remove(owner, ticket);finishFailed(owner,audit,"scheduler_rejected");
|
||||
report(input, rejected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void finishRefused(UUID owner,games.dmg.fantasystick.events.BookEventBridge.Request request,String reason){if(request!=null&&pendingBookEvents.remove(owner,request))bookEvents.refused(request,reason);}
|
||||
private void finishFailed(UUID owner,games.dmg.fantasystick.events.BookEventBridge.Request request,String reason){if(request!=null&&pendingBookEvents.remove(owner,request))bookEvents.failed(request,reason);}
|
||||
|
||||
private ItemStack render(CharacterRecord record, ControlInput input, int offset) {
|
||||
return renderBook(record, input.player(), input, offset, null, null, false);
|
||||
}
|
||||
@@ -295,7 +314,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
meta.setTitle("Character");
|
||||
meta.setAuthor("Fantasy Stick");
|
||||
|
||||
var statView = interactive ? new StatView(UUID.randomUUID().toString(), input, record.revision()) : null;
|
||||
var statView = interactive ? new StatView(UUID.randomUUID().toString(), input, record.revision(), record) : null;
|
||||
var allOverview = overviewPages(record, subject, statView, savedState);
|
||||
int start = Math.min(offset, (allOverview.size() - 1) / VOLUME_SIZE * VOLUME_SIZE);
|
||||
int end = Math.min(start + VOLUME_SIZE, allOverview.size());
|
||||
@@ -743,15 +762,20 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
|| !(event.getCommonConnection() instanceof io.papermc.paper.connection.PlayerGameConnection connection)) return;
|
||||
var player = connection.getPlayer();
|
||||
var view = views.get(player.getUniqueId());
|
||||
if (view == null || view.input().player() != player || event.getTag() == null) return;
|
||||
if (view == null) { refusePage(player,"missing_or_used_view"); return; }
|
||||
if (view.input().player() != player || event.getTag() == null) { refusePage(player,"foreign_or_missing_payload"); return; }
|
||||
String raw = event.getTag().string();
|
||||
if (raw.length() > 64) return;
|
||||
if (raw.length() > 64) { refusePage(player,"invalid_payload"); return; }
|
||||
var match = PAGE_PAYLOAD.matcher(raw);
|
||||
if (!match.matches() || !view.nonce().equals(match.group(1))) return;
|
||||
if (!match.matches() || !view.nonce().equals(match.group(1))) { refusePage(player,"stale_or_forged_payload"); return; }
|
||||
int offset = match.group(2).equals("0") ? view.previous() : view.next();
|
||||
if (offset < 0 || !views.remove(player.getUniqueId(), view) || !view.input().stillCurrent()) return;
|
||||
if (offset < 0 || !views.remove(player.getUniqueId(), view) || !view.input().stillCurrent()) { refusePage(player,"stale_or_unavailable_page"); return; }
|
||||
// Reread authoritative data, rather than retaining a stale giant character snapshot.
|
||||
load(view.input(), offset);
|
||||
load(view.input(), offset, true);
|
||||
}
|
||||
|
||||
private void refusePage(org.bukkit.entity.Player player,String reason){
|
||||
if(bookEvents==null)return;var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);if(active==null)return;var current=sessions.currentRecord(active).orElse(null);if(current!=null)bookEvents.refused(player,active,current,"character_book_navigate",reason);
|
||||
}
|
||||
|
||||
private void report(ControlInput input, Throwable failure) {
|
||||
@@ -767,49 +791,53 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
public void statAction(io.papermc.paper.event.player.PlayerCustomClickEvent event) {
|
||||
if (closed || !STATS.equals(event.getIdentifier())
|
||||
|| !(event.getCommonConnection() instanceof io.papermc.paper.connection.PlayerGameConnection connection)) return;
|
||||
var player = connection.getPlayer();
|
||||
var player = connection.getPlayer();if(event.getTag()==null)return;String raw=event.getTag().string();if(raw.length()>80)return;var match=STAT_PAYLOAD.matcher(raw);if(!match.matches())return;var stat=Stat.valueOf(match.group(2));
|
||||
var view = statViews.get(player.getUniqueId());
|
||||
if (view == null || view.input().player() != player || event.getTag() == null) return;
|
||||
String raw = event.getTag().string();
|
||||
if (raw.length() > 80) return;
|
||||
var match = STAT_PAYLOAD.matcher(raw);
|
||||
if (!match.matches() || !view.nonce().equals(match.group(1))
|
||||
|| !statViews.remove(player.getUniqueId(), view) || !view.input().stillCurrent()) return;
|
||||
var stat = Stat.valueOf(match.group(2));
|
||||
if (view == null || view.input().player() != player || !view.nonce().equals(match.group(1))
|
||||
|| !statViews.remove(player.getUniqueId(), view) || !view.input().stillCurrent()) { refuseStat(player,stat,"stale_forged_or_repeated_action"); return; }
|
||||
var currentHealth = java.math.BigDecimal.valueOf(player.getHealth());
|
||||
var nativeLimit = NativeHealthLimit.read();
|
||||
characters.purchaseStat(view.input().session().character(), view.revision(), stat, currentHealth, nativeLimit)
|
||||
.whenComplete((purchase, failure) -> {
|
||||
try {
|
||||
nextTick.accept(() -> finishPurchase(view, stat, purchase, failure));
|
||||
} catch (RuntimeException rejected) { report(view.input(), rejected); }
|
||||
});
|
||||
var nativeMaximum=java.math.BigDecimal.valueOf(java.util.Objects.requireNonNull(player.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH)).getValue());
|
||||
var audit=progressionEvents==null?null:progressionEvents.begin(view.input(),view.before(),stat,currentHealth,nativeMaximum,nativeLimit);
|
||||
try {
|
||||
characters.purchaseStat(view.input().session().character(), view.revision(), stat, currentHealth, nativeLimit)
|
||||
.whenComplete((purchase, failure) -> {
|
||||
try { nextTick.accept(() -> finishPurchase(view, stat, audit, purchase, failure)); }
|
||||
catch (RuntimeException rejected) { if(audit!=null){if(purchase!=null&&purchase.status()==CharacterStorage.StatPurchaseStatus.APPLIED)progressionEvents.increased(audit,purchase.record().orElseThrow(),"scheduler_rejected_before_native_publication",purchase.record().orElseThrow().health());else progressionEvents.failed(audit,"scheduler_rejected");}report(view.input(), rejected); }
|
||||
});
|
||||
} catch (RuntimeException failure) { if(audit!=null)progressionEvents.failed(audit,"purchase_rejected");report(view.input(),failure); }
|
||||
}
|
||||
|
||||
private void finishPurchase(StatView view, Stat stat, CharacterStorage.StatPurchase purchase, Throwable failure) {
|
||||
private void refuseStat(org.bukkit.entity.Player player,Stat stat,String reason){if(progressionEvents==null)return;var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);if(active==null)return;var current=sessions.currentRecord(active).orElse(null);if(current!=null)progressionEvents.refused(player,active,current,stat,NativeHealthLimit.read(),reason);}
|
||||
|
||||
private void finishPurchase(StatView view, Stat stat, games.dmg.fantasystick.events.ProgressionEventBridge.Action audit, CharacterStorage.StatPurchase purchase, Throwable failure) {
|
||||
var input = view.input();
|
||||
if (closed || !input.stillCurrent()) return;
|
||||
if (failure != null) {
|
||||
report(input, failure);
|
||||
input.player().sendMessage("Stat purchase could not be saved; no character state was changed.");
|
||||
if(audit!=null)progressionEvents.failed(audit,"save_failed");report(input, failure);
|
||||
if(!closed&&input.player().isOnline())input.player().sendMessage("Stat purchase could not be saved; no character state was changed.");
|
||||
return;
|
||||
}
|
||||
if (purchase.status() == CharacterStorage.StatPurchaseStatus.APPLIED) {
|
||||
var saved = purchase.record().orElseThrow();
|
||||
if(closed||!input.stillCurrent()){if(audit!=null)progressionEvents.increased(audit,saved,closed?"plugin_shutdown_before_native_publication":"session_changed_before_native_publication",saved.health());return;}
|
||||
try {
|
||||
if (!sessions.acknowledgeProgress(input.session(), view.revision(), saved)) {
|
||||
if(audit!=null)progressionEvents.increased(audit,saved,"session_changed_before_native_publication",saved.health());
|
||||
input.player().sendMessage("Stat purchase was saved; reopen the character after the session transition.");
|
||||
return;
|
||||
}
|
||||
} catch (RuntimeException nativeFailure) {
|
||||
report(input, nativeFailure);
|
||||
if(audit!=null)progressionEvents.increased(audit,saved,"native_publication_failed",saved.health());report(input, nativeFailure);
|
||||
input.player().sendMessage("Stat purchase was saved, but native health refresh failed; exit for recovery.");
|
||||
return;
|
||||
}
|
||||
if(audit!=null)progressionEvents.increased(audit,saved,"acknowledged",java.math.BigDecimal.valueOf(input.player().getHealth()));
|
||||
input.player().sendMessage(stat + " increased to " + saved.stats().get(stat) + ".");
|
||||
load(input, 0);
|
||||
load(input, 0, false);
|
||||
return;
|
||||
}
|
||||
if(audit!=null)progressionEvents.refused(audit,purchase.status().name().toLowerCase(java.util.Locale.ROOT),purchase.record().orElse(null));
|
||||
if(closed||!input.stillCurrent())return;
|
||||
String message = switch (purchase.status()) {
|
||||
case STALE -> "That character page is stale; reopen it before spending points.";
|
||||
case NO_POINTS -> "This character has no unspent points.";
|
||||
@@ -817,7 +845,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
case APPLIED -> throw new IllegalStateException("Handled applied purchase");
|
||||
};
|
||||
input.player().sendMessage(message);
|
||||
load(input, 0);
|
||||
load(input, 0, false);
|
||||
}
|
||||
|
||||
@org.bukkit.event.EventHandler
|
||||
@@ -846,7 +874,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
return;
|
||||
}
|
||||
player.sendMessage(abilityMessage(unlock, ability, mutation.status()));
|
||||
load(view.input(), 0);
|
||||
load(view.input(), 0, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -874,7 +902,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
return;
|
||||
}
|
||||
player.sendMessage(passiveMessage(unlock, passive, mutation.status()));
|
||||
load(view.input(), 0);
|
||||
load(view.input(), 0, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -912,12 +940,12 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
}
|
||||
|
||||
@org.bukkit.event.EventHandler
|
||||
public void rareAction(io.papermc.paper.event.player.PlayerCustomClickEvent event){if(closed||rarePotions==null||rareReskills==null||rareMagicCores==null||!RARE_USE.equals(event.getIdentifier())||!(event.getCommonConnection() instanceof io.papermc.paper.connection.PlayerGameConnection connection)||event.getTag()==null)return;var player=connection.getPlayer();var view=rareViews.get(player.getUniqueId());if(view==null||view.input().player()!=player)return;String raw=event.getTag().string();if(raw.length()>120)return;var match=RARE_PAYLOAD.matcher(raw);var product=match.matches()?games.dmg.fantasystick.shop.RareProduct.find(match.group(2)).orElse(null):null;if(product==null||!view.nonce().equals(match.group(1))||!view.quantities().getOrDefault(product.id(),java.math.BigInteger.ZERO).equals(rareInventory.quantity(view.input().session().character(),product))||!rareViews.remove(player.getUniqueId(),view)||!view.input().stillCurrent())return;if(product==games.dmg.fantasystick.shop.RareProduct.RESKILL){var preview=rareReskills.preview(player);if(preview.isEmpty()){player.sendMessage("Reskill preview refused: the character, session or quantity changed.");return;}if(preview.orElseThrow().refund().signum()==0){player.sendMessage("Reskill use refused: this character has no investments to reset.");load(view.input(),0);return;}var confirmation=new ReskillView(UUID.randomUUID().toString(),view.input(),preview.orElseThrow());reskillViews.put(player.getUniqueId(),confirmation);player.openBook(reskillConfirmation(confirmation));return;}if(product==games.dmg.fantasystick.shop.RareProduct.MAGIC_CORE){rareMagicCores.use(player,outcome->{if(closed)return;player.sendMessage(switch(outcome.status()){case APPLIED->"Used Magic Core and gained 1 ordinary unspent point.";case NOT_AVAILABLE->"Magic Core use refused: that item is no longer available.";case STALE->"Magic Core use refused: the character, session, points or quantity changed.";case FAILED->"Magic Core could not be acknowledged; retained proof prevents replay.";});if(view.input().stillCurrent())load(view.input(),0);});return;}if(!product.heals())return;rarePotions.use(player,product,outcome->{if(closed||!view.input().stillCurrent())return;player.sendMessage(switch(outcome.status()){case APPLIED->"Used "+product.displayName()+" and restored "+outcome.healed().toPlainString()+" health.";case FULL_HEALTH->"Potion use refused: health is already full.";case NO_EFFECT->"Potion use refused: no positive healing was accepted.";case NOT_AVAILABLE->"Potion use refused: that item is no longer available.";case STALE->"Potion use refused: the character, session or quantity changed.";case FAILED->"Potion use could not be acknowledged; retained proof prevents replay.";});load(view.input(),0);});}
|
||||
public void rareAction(io.papermc.paper.event.player.PlayerCustomClickEvent event){if(closed||rarePotions==null||rareReskills==null||rareMagicCores==null||!RARE_USE.equals(event.getIdentifier())||!(event.getCommonConnection() instanceof io.papermc.paper.connection.PlayerGameConnection connection)||event.getTag()==null)return;var player=connection.getPlayer();var view=rareViews.get(player.getUniqueId());if(view==null||view.input().player()!=player)return;String raw=event.getTag().string();if(raw.length()>120)return;var match=RARE_PAYLOAD.matcher(raw);var product=match.matches()?games.dmg.fantasystick.shop.RareProduct.find(match.group(2)).orElse(null):null;if(product==null||!view.nonce().equals(match.group(1))||!view.quantities().getOrDefault(product.id(),java.math.BigInteger.ZERO).equals(rareInventory.quantity(view.input().session().character(),product))||!rareViews.remove(player.getUniqueId(),view)||!view.input().stillCurrent())return;if(product==games.dmg.fantasystick.shop.RareProduct.RESKILL){var preview=rareReskills.preview(player);if(preview.isEmpty()){player.sendMessage("Reskill preview refused: the character, session or quantity changed.");return;}if(preview.orElseThrow().refund().signum()==0){player.sendMessage("Reskill use refused: this character has no investments to reset.");load(view.input(),0,false);return;}var confirmation=new ReskillView(UUID.randomUUID().toString(),view.input(),preview.orElseThrow());reskillViews.put(player.getUniqueId(),confirmation);player.openBook(reskillConfirmation(confirmation));return;}if(product==games.dmg.fantasystick.shop.RareProduct.MAGIC_CORE){rareMagicCores.use(player,outcome->{if(closed)return;player.sendMessage(switch(outcome.status()){case APPLIED->"Used Magic Core and gained 1 ordinary unspent point.";case NOT_AVAILABLE->"Magic Core use refused: that item is no longer available.";case STALE->"Magic Core use refused: the character, session, points or quantity changed.";case FAILED->"Magic Core could not be acknowledged; retained proof prevents replay.";});if(view.input().stillCurrent())load(view.input(),0,false);});return;}if(!product.heals())return;rarePotions.use(player,product,outcome->{if(closed||!view.input().stillCurrent())return;player.sendMessage(switch(outcome.status()){case APPLIED->"Used "+product.displayName()+" and restored "+outcome.healed().toPlainString()+" health.";case FULL_HEALTH->"Potion use refused: health is already full.";case NO_EFFECT->"Potion use refused: no positive healing was accepted.";case NOT_AVAILABLE->"Potion use refused: that item is no longer available.";case STALE->"Potion use refused: the character, session or quantity changed.";case FAILED->"Potion use could not be acknowledged; retained proof prevents replay.";});load(view.input(),0,false);});}
|
||||
|
||||
private static ItemStack reskillConfirmation(ReskillView view){var item=new ItemStack(Material.WRITTEN_BOOK);var meta=(BookMeta)item.getItemMeta();meta.setTitle("Confirm Reskill");meta.setAuthor("Fantasy Stick");var page=Component.text("Confirm Reskill\n\nRefund: "+view.preview().refund()+" points\nStats → 0\nAbilities/passives removed\nHealth only clamps to the new maximum\nCooldowns retained\n\n").append(Component.text("[Confirm Reskill]",net.kyori.adventure.text.format.NamedTextColor.DARK_RED).clickEvent(net.kyori.adventure.text.event.ClickEvent.custom(RARE_RESKILL_CONFIRM,net.kyori.adventure.nbt.api.BinaryTagHolder.binaryTagHolder("\""+view.nonce()+"\""))));meta.pages(java.util.List.of(page));item.setItemMeta(meta);return item;}
|
||||
|
||||
@org.bukkit.event.EventHandler
|
||||
public void reskillConfirm(io.papermc.paper.event.player.PlayerCustomClickEvent event){if(closed||rareReskills==null||!RARE_RESKILL_CONFIRM.equals(event.getIdentifier())||!(event.getCommonConnection() instanceof io.papermc.paper.connection.PlayerGameConnection connection)||event.getTag()==null)return;var player=connection.getPlayer();var view=reskillViews.get(player.getUniqueId());String raw=event.getTag().string();if(view==null||view.input().player()!=player||raw.length()>80)return;var match=RARE_CONFIRM_PAYLOAD.matcher(raw);if(!match.matches()||!view.nonce().equals(match.group(1))||!reskillViews.remove(player.getUniqueId(),view)||!view.input().stillCurrent())return;rareReskills.confirm(player,view.preview(),outcome->{if(closed)return;player.sendMessage(switch(outcome.status()){case APPLIED->"Used Reskill; all purchased stats, abilities and passives were refunded.";case NOTHING_TO_RESET->"Reskill use refused: this character has no investments to reset.";case NOT_AVAILABLE->"Reskill use refused: that item is no longer available.";case STALE->"Reskill use refused: the character, session, build or quantity changed.";case FAILED->"Reskill could not be acknowledged; retained proof prevents replay.";});if(view.input().stillCurrent())load(view.input(),0);});}
|
||||
public void reskillConfirm(io.papermc.paper.event.player.PlayerCustomClickEvent event){if(closed||rareReskills==null||!RARE_RESKILL_CONFIRM.equals(event.getIdentifier())||!(event.getCommonConnection() instanceof io.papermc.paper.connection.PlayerGameConnection connection)||event.getTag()==null)return;var player=connection.getPlayer();var view=reskillViews.get(player.getUniqueId());String raw=event.getTag().string();if(view==null||view.input().player()!=player||raw.length()>80)return;var match=RARE_CONFIRM_PAYLOAD.matcher(raw);if(!match.matches()||!view.nonce().equals(match.group(1))||!reskillViews.remove(player.getUniqueId(),view)||!view.input().stillCurrent())return;rareReskills.confirm(player,view.preview(),outcome->{if(closed)return;player.sendMessage(switch(outcome.status()){case APPLIED->"Used Reskill; all purchased stats, abilities and passives were refunded.";case NOTHING_TO_RESET->"Reskill use refused: this character has no investments to reset.";case NOT_AVAILABLE->"Reskill use refused: that item is no longer available.";case STALE->"Reskill use refused: the character, session, build or quantity changed.";case FAILED->"Reskill could not be acknowledged; retained proof prevents replay.";});if(view.input().stillCurrent())load(view.input(),0,false);});}
|
||||
|
||||
@org.bukkit.event.EventHandler
|
||||
public void partyAction(io.papermc.paper.event.player.PlayerCustomClickEvent event) { partyControls.click(event); }
|
||||
@@ -926,6 +954,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
public void quit(org.bukkit.event.player.PlayerQuitEvent event) {
|
||||
var owner = event.getPlayer().getUniqueId();
|
||||
pending.remove(owner);
|
||||
var bookRequest=pendingBookEvents.remove(owner);if(bookRequest!=null)bookEvents.refused(bookRequest,"player_quit");
|
||||
adminPending.remove(owner);
|
||||
adminViews.remove(owner);
|
||||
savedViews.remove(owner);
|
||||
@@ -939,6 +968,6 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
closed = true; pending.clear(); adminPending.clear(); adminViews.clear(); savedViews.clear(); views.clear(); statViews.clear(); abilityViews.clear(); passiveViews.clear(); rareViews.clear(); reskillViews.clear(); partyControls.close();
|
||||
closed = true; pending.clear();pendingBookEvents.values().forEach(request->bookEvents.refused(request,"plugin_shutdown"));pendingBookEvents.clear(); adminPending.clear(); adminViews.clear(); savedViews.clear(); views.clear(); statViews.clear(); abilityViews.clear(); passiveViews.clear(); rareViews.clear(); reskillViews.clear(); partyControls.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import games.dmg.fantasystick.persistence.CharacterRecord;
|
||||
import games.dmg.fantasystick.session.ControlInput;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import org.bukkit.Location;
|
||||
|
||||
/** Stable feature-owned character-book outcomes; event delivery never authorizes or rolls back an open. */
|
||||
public final class BookEventBridge {
|
||||
public record Request(UUID actionId, EventInput.Context context, String actionKind, String viewKind,
|
||||
int offset, BigInteger requestedRevision) {
|
||||
public Request {
|
||||
Objects.requireNonNull(actionId); Objects.requireNonNull(context); Objects.requireNonNull(actionKind);
|
||||
Objects.requireNonNull(viewKind); Objects.requireNonNull(requestedRevision);
|
||||
if (offset < -1) throw new IllegalArgumentException("Invalid book offset");
|
||||
}
|
||||
}
|
||||
|
||||
private final Consumer<EventInput> sink;
|
||||
private final Clock clock;
|
||||
|
||||
public BookEventBridge(Consumer<EventInput> sink, Clock clock) {
|
||||
this.sink = Objects.requireNonNull(sink);
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
}
|
||||
|
||||
public Request request(ControlInput input, CharacterRecord current, int offset) {
|
||||
Objects.requireNonNull(input); Objects.requireNonNull(current);
|
||||
var request = new Request(UUID.randomUUID(), context(input, current.name()),
|
||||
offset == 0 ? "character_book_open" : "character_book_navigate", "active", offset,
|
||||
current.revision());
|
||||
emit("action.observed", request, "observed", null, null, null, null);
|
||||
return request;
|
||||
}
|
||||
|
||||
public void refused(Request request, String reason) {
|
||||
emit("action.resolved", request, "refused", reason, null, null, null);
|
||||
}
|
||||
|
||||
public void refused(org.bukkit.entity.Player player, games.dmg.fantasystick.session.EquipmentSessions.Active active,
|
||||
CharacterRecord current, String actionKind, String reason) {
|
||||
Objects.requireNonNull(player); Objects.requireNonNull(active); Objects.requireNonNull(current);
|
||||
var request=new Request(UUID.randomUUID(),context(player,active,current.name()),actionKind,"active",-1,current.revision());
|
||||
emit("action.observed",request,"observed",null,null,null,null);
|
||||
refused(request,reason);
|
||||
}
|
||||
|
||||
public void failed(Request request, String reason) {
|
||||
emit("action.resolved", request, "failed", reason, null, null, null);
|
||||
}
|
||||
|
||||
public void opened(Request request, CharacterRecord loaded, int presentedPages, int presentedVolumes) {
|
||||
Objects.requireNonNull(loaded);
|
||||
emit("action.resolved", request, "accepted", "acknowledged", loaded.revision(), presentedPages,
|
||||
presentedVolumes);
|
||||
emit(request.offset() == 0 ? "character_book.opened" : "character_book.navigated", request,
|
||||
"accepted", "acknowledged", loaded.revision(), presentedPages, presentedVolumes);
|
||||
}
|
||||
|
||||
private void emit(String type, Request request, String outcome, String reason, BigInteger loadedRevision,
|
||||
Integer pages, Integer volumes) {
|
||||
var data = new LinkedHashMap<String, Object>();
|
||||
data.put("action_kind", request.actionKind()); data.put("view_kind", request.viewKind());
|
||||
data.put("section", "character_inspection"); data.put("control_kind", "book"); data.put("control_slot", "0");
|
||||
data.put("offset", request.offset()<0?"unknown":Integer.toString(request.offset()));
|
||||
data.put("requested_revision", request.requestedRevision().toString()); data.put("outcome", outcome);
|
||||
if (reason != null) data.put(type.startsWith("character_book.") ? "native_open" : "reason", reason);
|
||||
if (loadedRevision != null) data.put("loaded_revision", loadedRevision.toString());
|
||||
if (pages != null) data.put("presented_pages", pages);
|
||||
if (volumes != null) data.put("presented_volumes", volumes);
|
||||
try {
|
||||
sink.accept(new EventInput(id(request.actionId(), type), request.actionId(), clock.instant(), type,
|
||||
request.context(), Map.copyOf(data)));
|
||||
} catch (RuntimeException ignored) {
|
||||
// Observability cannot change book authorization, rendering or native opening.
|
||||
}
|
||||
}
|
||||
|
||||
private EventInput.Context context(ControlInput input, String characterName) {
|
||||
return context(input.player(),input.session(),characterName);
|
||||
}
|
||||
|
||||
private EventInput.Context context(org.bukkit.entity.Player player,
|
||||
games.dmg.fantasystick.session.EquipmentSessions.Active active,
|
||||
String characterName) {
|
||||
return new EventInput.Context(player.getUniqueId(), player.getName(), active.character(),
|
||||
characterName, active.session(), location(player.getLocation()));
|
||||
}
|
||||
|
||||
private EventInput.Location location(Location location) {
|
||||
return new EventInput.Location(location.getWorld().getUID(), location.getWorld().getName(), location.getX(),
|
||||
location.getY(), location.getZ(), clock.instant(), "exact");
|
||||
}
|
||||
|
||||
private static UUID id(UUID action, String type) {
|
||||
return UUID.nameUUIDFromBytes(("fantasystick:book:" + action + ":" + type)
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package games.dmg.fantasystick.events;
|
||||
|
||||
import games.dmg.fantasystick.persistence.CharacterRecord;
|
||||
import games.dmg.fantasystick.progression.Stat;
|
||||
import games.dmg.fantasystick.session.ControlInput;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import org.bukkit.Location;
|
||||
|
||||
/** Feature-owned permanent-stat outcomes, isolated from purchase and native-health authority. */
|
||||
public final class ProgressionEventBridge {
|
||||
public record Action(UUID actionId, EventInput.Context context, Stat stat, BigInteger expectedRevision,
|
||||
BigInteger statBefore, BigInteger pointsBefore, BigDecimal healthBefore,
|
||||
BigDecimal maximumBefore, BigDecimal nativeLimit) {
|
||||
public Action {
|
||||
Objects.requireNonNull(actionId); Objects.requireNonNull(context); Objects.requireNonNull(stat);
|
||||
Objects.requireNonNull(expectedRevision); Objects.requireNonNull(statBefore); Objects.requireNonNull(pointsBefore);
|
||||
Objects.requireNonNull(healthBefore); Objects.requireNonNull(maximumBefore); Objects.requireNonNull(nativeLimit);
|
||||
}
|
||||
}
|
||||
|
||||
private final Consumer<EventInput> sink;
|
||||
private final Clock clock;
|
||||
|
||||
public ProgressionEventBridge(Consumer<EventInput> sink, Clock clock) {
|
||||
this.sink=Objects.requireNonNull(sink);this.clock=Objects.requireNonNull(clock);
|
||||
}
|
||||
|
||||
public Action begin(ControlInput input, CharacterRecord before, Stat stat, BigDecimal health,
|
||||
BigDecimal maximum, BigDecimal nativeLimit) {
|
||||
Objects.requireNonNull(input);Objects.requireNonNull(before);Objects.requireNonNull(stat);
|
||||
var action=new Action(UUID.randomUUID(),context(input,before.name()),stat,before.revision(),
|
||||
before.stats().get(stat),before.unspentPoints(),health,maximum.min(nativeLimit),nativeLimit);
|
||||
emit("action.observed",action,"observed",null,null,null,null);
|
||||
return action;
|
||||
}
|
||||
|
||||
public void refused(Action action,String reason,CharacterRecord current){
|
||||
emit("action.resolved",action,"refused",reason,current,null,null);
|
||||
}
|
||||
public void failed(Action action,String reason){emit("action.resolved",action,"failed",reason,null,null,null);}
|
||||
public void increased(Action action,CharacterRecord saved,String nativePublication,BigDecimal nativeHealthAfter){
|
||||
emit("action.resolved",action,"accepted","acknowledged",saved,nativePublication,nativeHealthAfter);
|
||||
emit("character.stat_increased",action,"accepted","acknowledged",saved,nativePublication,nativeHealthAfter);
|
||||
}
|
||||
|
||||
public void refused(org.bukkit.entity.Player player,games.dmg.fantasystick.session.EquipmentSessions.Active active,
|
||||
CharacterRecord current,Stat stat,BigDecimal nativeLimit,String reason){
|
||||
var input=new ControlInput(player,active,0,"",ControlInput.Kind.BOOK,ControlInput.Gesture.USE,null,0,0,()->true);
|
||||
var action=begin(input,current,stat,BigDecimal.valueOf(player.getHealth()),current.maximumHealth().min(nativeLimit),nativeLimit);
|
||||
emit("action.resolved",action,"refused",reason,current,null,null);
|
||||
}
|
||||
|
||||
private void emit(String type,Action action,String outcome,String reason,CharacterRecord after,
|
||||
String nativePublication,BigDecimal nativeHealthAfter){
|
||||
var data=new LinkedHashMap<String,Object>();data.put("action_kind","character_stat_increase");
|
||||
data.put("stat_id",action.stat().name());data.put("expected_revision",action.expectedRevision().toString());
|
||||
data.put("before_revision",action.expectedRevision().toString());data.put("stat_before",action.statBefore().toString());
|
||||
data.put("points_before",action.pointsBefore().toString());data.put("health_before",action.healthBefore().toPlainString());
|
||||
data.put("maximum_health_before",action.maximumBefore().toPlainString());data.put("native_health_ceiling",action.nativeLimit().toPlainString());
|
||||
data.put("outcome",outcome);if(reason!=null)data.put(type.equals("character.stat_increased")?"native_boundary":"reason",reason);
|
||||
if(after!=null){data.put("after_revision",after.revision().toString());data.put("stat_after",after.stats().get(action.stat()).toString());data.put("points_after",after.unspentPoints().toString());data.put("health_after",(nativeHealthAfter==null?after.health():nativeHealthAfter).toPlainString());data.put("maximum_health_after",after.maximumHealth().min(action.nativeLimit()).toPlainString());}
|
||||
if(nativePublication!=null)data.put("native_publication",nativePublication);
|
||||
try{sink.accept(new EventInput(id(action.actionId(),type),action.actionId(),clock.instant(),type,action.context(),Map.copyOf(data)));}catch(RuntimeException ignored){}
|
||||
}
|
||||
|
||||
private EventInput.Context context(ControlInput input,String characterName){var player=input.player();return new EventInput.Context(player.getUniqueId(),player.getName(),input.session().character(),characterName,input.session().session(),location(player.getLocation()));}
|
||||
private EventInput.Location location(Location location){return new EventInput.Location(location.getWorld().getUID(),location.getWorld().getName(),location.getX(),location.getY(),location.getZ(),clock.instant(),"exact");}
|
||||
private static UUID id(UUID action,String type){return UUID.nameUUIDFromBytes(("fantasystick:progression:"+action+":"+type).getBytes(StandardCharsets.UTF_8));}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class AdminExperienceNativeTest {
|
||||
var viewer = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
viewer.core.inventory.setItemInOffHand(null); viewer.core.location.setWorld(target.world);
|
||||
f.players.put(viewer.core.id, viewer.player);
|
||||
f.particlesCommand.execute(target.player, "fantasyparticles", new String[]{"all", "off"}); f.tick();
|
||||
PartyBookNativeTest.disableAllParticles(f,target);
|
||||
target.particleCalls.clear(); viewer.particleCalls.clear();
|
||||
var sender = new EquipmentAdminCommandNativeTest.Sender();
|
||||
var before = storage.find(target.core.record.id()).get().orElseThrow();
|
||||
|
||||
@@ -51,7 +51,7 @@ class ChainLightningNativeTest {
|
||||
var viewer = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
viewer.core.inventory.setItemInOffHand(null); viewer.core.location.setWorld(caster.world);
|
||||
fixture.players.put(viewer.core.id, viewer.player);
|
||||
fixture.particlesCommand.execute(caster.player, "fantasyparticles", new String[]{"all", "off"}); PartyBookNativeTest.drain(fixture);
|
||||
PartyBookNativeTest.disableAllParticles(fixture,caster);
|
||||
caster.particleCalls.clear(); viewer.particleCalls.clear();
|
||||
var damage = new LinkedHashMap<UUID, Double>(); nativeDamage(fixture, first, damage); nativeDamage(fixture, second, damage);
|
||||
use(fixture, caster); drain(fixture, 2);
|
||||
|
||||
@@ -21,6 +21,16 @@ class CharacterBookNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void bookEventSinkFailureCannotChangeTheAlreadyAuthorizedNativeBookPath() {
|
||||
var player=org.mockito.Mockito.mock(org.bukkit.entity.Player.class);var world=org.mockito.Mockito.mock(org.bukkit.World.class);var record=org.mockito.Mockito.mock(games.dmg.fantasystick.persistence.CharacterRecord.class);var playerId=UUID.randomUUID();var active=new games.dmg.fantasystick.session.EquipmentSessions.Active(playerId,UUID.randomUUID(),UUID.randomUUID());org.mockito.Mockito.when(player.getUniqueId()).thenReturn(playerId);org.mockito.Mockito.when(player.getName()).thenReturn("Player");org.mockito.Mockito.when(world.getUID()).thenReturn(UUID.randomUUID());org.mockito.Mockito.when(world.getName()).thenReturn("world");org.mockito.Mockito.when(player.getLocation()).thenReturn(new org.bukkit.Location(world,1,2,3));org.mockito.Mockito.when(record.name()).thenReturn("Hero");org.mockito.Mockito.when(record.revision()).thenReturn(java.math.BigInteger.ONE);org.mockito.Mockito.when(record.stats()).thenReturn(java.util.Map.of(games.dmg.fantasystick.progression.Stat.CON,java.math.BigInteger.ZERO));org.mockito.Mockito.when(record.unspentPoints()).thenReturn(java.math.BigInteger.ONE);org.mockito.Mockito.when(record.maximumHealth()).thenReturn(java.math.BigDecimal.TEN);org.mockito.Mockito.when(record.health()).thenReturn(java.math.BigDecimal.ONE);var input=new games.dmg.fantasystick.session.ControlInput(player,active,0,"",games.dmg.fantasystick.session.ControlInput.Kind.BOOK,games.dmg.fantasystick.session.ControlInput.Gesture.USE,null,0,0,()->true);var bridge=new games.dmg.fantasystick.events.BookEventBridge(event->{throw new IllegalStateException("disk");},java.time.Clock.systemUTC());var progression=new games.dmg.fantasystick.events.ProgressionEventBridge(event->{throw new IllegalStateException("disk");},java.time.Clock.systemUTC());assertDoesNotThrow(()->{var request=bridge.request(input,record,0);bridge.opened(request,record,3,1);bridge.refused(request,"retry");var action=progression.begin(input,record,games.dmg.fantasystick.progression.Stat.CON,java.math.BigDecimal.ONE,java.math.BigDecimal.TEN,java.math.BigDecimal.valueOf(1024));progression.increased(action,record,"acknowledged",java.math.BigDecimal.ONE);});
|
||||
}
|
||||
|
||||
@Test void registeredOpenReachesOneAcknowledgedFeatureEventAfterAuthoritativeReadAndNativeOpen() throws Exception {
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var a=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(a.core.id,a.player);f.activate(a);click(f,a);drainReads(f);runQueued(f);assertEquals(1,a.books.size());f.plugin.setEnabled(false);f.awaitWorkers();}
|
||||
var events=new java.util.ArrayList<com.google.gson.JsonObject>();try(var files=Files.walk(data.resolve("events"))){for(var file:files.filter(path->path.getFileName().toString().endsWith(".jsonl")).toList())for(var line:Files.readAllLines(file))if(!line.isBlank())events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());}
|
||||
var opened=events.stream().filter(event->event.get("type").getAsString().endsWith("character_book.opened")).toList();assertEquals(1,opened.size());var data=opened.getFirst().getAsJsonObject("data");assertEquals("active",data.get("view_kind").getAsString());assertEquals("0",data.get("offset").getAsString());assertTrue(data.get("loaded_revision").getAsBigInteger().signum()>=0);assertTrue(data.get("presented_pages").getAsInt()>0);assertEquals(1,data.get("presented_volumes").getAsInt());assertEquals("acknowledged",data.get("native_open").getAsString());var action=data.get("action_id").getAsString();assertEquals(1,events.stream().filter(event->event.get("type").getAsString().endsWith("action.observed")&&event.getAsJsonObject("data").get("action_id").getAsString().equals(action)).count());assertEquals(1,events.stream().filter(event->event.get("type").getAsString().endsWith("action.resolved")&&event.getAsJsonObject("data").get("action_id").getAsString().equals(action)).count());
|
||||
}
|
||||
|
||||
@Test void queuedInspectionCannotOpenForAMovedSheetChangedSlotQuitOrDisabledLifetime() throws Exception {
|
||||
for (int route = 0; route < 4; route++) try (var f = new FantasyStickLifecycleNativeTest.Fixture(data.resolve("route-" + route))) {
|
||||
f.plugin.setEnabled(true);
|
||||
@@ -52,6 +62,7 @@ class CharacterBookNativeTest {
|
||||
assertEquals(1, a.books.size());
|
||||
assertTrue(text(a.books.getFirst()).contains("Latest book name"));
|
||||
}
|
||||
var events=new java.util.ArrayList<com.google.gson.JsonObject>();try(var files=Files.walk(data.resolve("events"))){for(var file:files.filter(path->path.getFileName().toString().endsWith(".jsonl")).toList())for(var line:Files.readAllLines(file))if(!line.isBlank())events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());}assertEquals(1,events.stream().filter(event->event.get("type").getAsString().endsWith("character_book.opened")).count());assertEquals(1,events.stream().filter(event->event.get("type").getAsString().endsWith("action.resolved")&&event.getAsJsonObject("data").has("action_kind")&&event.getAsJsonObject("data").get("action_kind").getAsString().equals("character_book_open")&&event.getAsJsonObject("data").has("reason")&&event.getAsJsonObject("data").get("reason").getAsString().equals("superseded")).count());
|
||||
}
|
||||
|
||||
@Test void cappedProgressAndSavedAbilitiesRenderWithoutCappingStatsOrRewritingProgress() throws Exception {
|
||||
@@ -101,6 +112,17 @@ class CharacterBookNativeTest {
|
||||
assertEquals(saved, characters(f).find(a.core.record.id()).get(5, TimeUnit.SECONDS).orElseThrow(),
|
||||
"A rendered purchase action is single-use");
|
||||
}
|
||||
var events=new java.util.ArrayList<com.google.gson.JsonObject>();try(var files=Files.walk(data.resolve("events"))){for(var file:files.filter(path->path.getFileName().toString().endsWith(".jsonl")).toList())for(var line:Files.readAllLines(file))if(!line.isBlank())events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());}assertEquals(1,events.stream().filter(event->event.get("type").getAsString().endsWith("character_book.opened")).count(),"The automatic post-purchase refresh must not duplicate a user-requested book-open outcome");var increased=events.stream().filter(event->event.get("type").getAsString().endsWith("character.stat_increased")).toList();assertEquals(1,increased.size());var value=increased.getFirst().getAsJsonObject("data");assertEquals("CON",value.get("stat_id").getAsString());assertEquals("0",value.get("stat_before").getAsString());assertEquals("1",value.get("stat_after").getAsString());assertEquals("40",value.get("points_before").getAsString());assertEquals("39",value.get("points_after").getAsString());assertEquals("5.0",value.get("health_before").getAsString());assertEquals("6.0",value.get("health_after").getAsString());assertEquals("10.0",value.get("maximum_health_before").getAsString());assertEquals("11",value.get("maximum_health_after").getAsString());assertEquals("acknowledged",value.get("native_publication").getAsString());var action=value.get("action_id").getAsString();assertEquals(1,events.stream().filter(event->event.get("type").getAsString().endsWith("action.observed")&&event.getAsJsonObject("data").has("action_kind")&&event.getAsJsonObject("data").get("action_kind").getAsString().equals("character_stat_increase")&&event.getAsJsonObject("data").get("action_id").getAsString().equals(action)).count());assertEquals(1,events.stream().filter(event->event.get("type").getAsString().endsWith("action.resolved")&&event.getAsJsonObject("data").has("action_kind")&&event.getAsJsonObject("data").get("action_kind").getAsString().equals("character_stat_increase")&&event.getAsJsonObject("data").get("action_id").getAsString().equals(action)).count());
|
||||
}
|
||||
|
||||
@Test void everyPermanentStatPublishesExactlyOneAcknowledgedMutation() throws Exception {
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var a=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(a.core.id,a.player);f.activate(a);PartyBookNativeTest.open(f,a);for(var stat:games.dmg.fantasystick.progression.Stat.values()){PartyBookNativeTest.click(f,a,PartyBookNativeTest.button(a,"[+1 "+stat+"]"));PartyBookNativeTest.drain(f);PartyBookNativeTest.drain(f);}f.plugin.setEnabled(false);f.awaitWorkers();}
|
||||
var events=new java.util.ArrayList<com.google.gson.JsonObject>();try(var files=Files.walk(data.resolve("events"))){for(var file:files.filter(path->path.getFileName().toString().endsWith(".jsonl")).toList())for(var line:Files.readAllLines(file))if(!line.isBlank())events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());}var increased=events.stream().filter(event->event.get("type").getAsString().endsWith("character.stat_increased")).toList();assertEquals(6,increased.size());assertEquals(java.util.Arrays.stream(games.dmg.fantasystick.progression.Stat.values()).map(Enum::name).collect(java.util.stream.Collectors.toSet()),increased.stream().map(event->event.getAsJsonObject("data").get("stat_id").getAsString()).collect(java.util.stream.Collectors.toSet()));assertEquals(6,increased.stream().map(event->event.get("id").getAsString()).distinct().count());assertTrue(increased.stream().allMatch(event->event.getAsJsonObject("data").get("native_publication").getAsString().equals("acknowledged")));
|
||||
}
|
||||
|
||||
@Test void conPurchaseAtTheNativeCeilingRecordsStatAndPointChangeWithoutInventingHealing() throws Exception {
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var a=new FantasyStickLifecycleNativeTest.Actor(data);var old=a.core.record;var stats=new java.util.EnumMap<games.dmg.fantasystick.progression.Stat,java.math.BigInteger>(games.dmg.fantasystick.progression.Stat.class);stats.putAll(old.stats());stats.put(games.dmg.fantasystick.progression.Stat.CON,java.math.BigInteger.valueOf(1014));var prepared=new games.dmg.fantasystick.persistence.CharacterRecord(old.id(),old.name(),old.revision().add(java.math.BigInteger.ONE),stats,old.level(),old.experience(),java.math.BigInteger.ONE,java.math.BigDecimal.TEN,old.learned(),old.loadout(),old.passives(),old.cooldowns(),old.restrictedKills(),false,old.deathId(),old.diedAt(),old.coins(),old.coinsInitialized(),old.rewardReceipts(),old.pvpCoinEscrow(),old.gear(),old.sheetGeneration());new games.dmg.fantasystick.persistence.YamlCharacterRepository(data).replace(prepared,old.revision());f.players.put(a.core.id,a.player);f.activate(a);PartyBookNativeTest.open(f,a);PartyBookNativeTest.click(f,a,PartyBookNativeTest.button(a,"[+1 CON]"));PartyBookNativeTest.drain(f);PartyBookNativeTest.drain(f);f.plugin.setEnabled(false);f.awaitWorkers();}
|
||||
var events=new java.util.ArrayList<com.google.gson.JsonObject>();try(var files=Files.walk(data.resolve("events"))){for(var file:files.filter(path->path.getFileName().toString().endsWith(".jsonl")).toList())for(var line:Files.readAllLines(file))if(!line.isBlank())events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());}var value=events.stream().filter(event->event.get("type").getAsString().endsWith("character.stat_increased")).map(event->event.getAsJsonObject("data")).findFirst().orElseThrow();assertEquals("1014",value.get("stat_before").getAsString());assertEquals("1015",value.get("stat_after").getAsString());assertEquals(0,new java.math.BigDecimal(value.get("maximum_health_before").getAsString()).compareTo(new java.math.BigDecimal(value.get("maximum_health_after").getAsString())));assertEquals(0,new java.math.BigDecimal(value.get("health_before").getAsString()).compareTo(new java.math.BigDecimal(value.get("health_after").getAsString())));
|
||||
}
|
||||
|
||||
@Test void conPurchaseKeepsItsOwnMaximumAndOneTimeGainDespiteExternalModifiers() throws Exception {
|
||||
@@ -173,6 +195,7 @@ class CharacterBookNativeTest {
|
||||
assertTrue(PartyBookNativeTest.text(b).contains("STR: 1"));
|
||||
assertEquals(saved.stats(), characters(f).find(saved.id()).get(5, TimeUnit.SECONDS).orElseThrow().stats());
|
||||
}
|
||||
var events=new java.util.ArrayList<com.google.gson.JsonObject>();try(var files=Files.walk(data.resolve("events"))){for(var file:files.filter(path->path.getFileName().toString().endsWith(".jsonl")).toList())for(var line:Files.readAllLines(file))if(!line.isBlank())events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());}var statEvents=events.stream().filter(event->event.getAsJsonObject("data").has("action_kind")&&event.getAsJsonObject("data").get("action_kind").getAsString().equals("character_stat_increase")).toList();assertEquals(1,statEvents.stream().filter(event->event.get("type").getAsString().endsWith("character.stat_increased")&&event.getAsJsonObject("data").get("stat_id").getAsString().equals("STR")).count());assertTrue(statEvents.stream().filter(event->event.get("type").getAsString().endsWith("action.resolved")).map(event->event.getAsJsonObject("data")).anyMatch(value->value.get("outcome").getAsString().equals("failed")&&value.get("reason").getAsString().equals("save_failed")));assertTrue(statEvents.stream().filter(event->event.get("type").getAsString().endsWith("action.resolved")).map(event->event.getAsJsonObject("data")).anyMatch(value->value.get("outcome").getAsString().equals("refused")&&value.get("reason").getAsString().equals("stale_forged_or_repeated_action")));
|
||||
}
|
||||
|
||||
@Test void nativePagesKeepLargeNumbersUnicodeNamesAndUnknownAbilitiesReadableAndComplete() throws Exception {
|
||||
@@ -210,6 +233,7 @@ class CharacterBookNativeTest {
|
||||
}
|
||||
|
||||
@Test void largeRecordsRemainCompleteThroughBoundedSingleUseNavigation() throws Exception {
|
||||
int expectedNavigations;
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
@@ -247,13 +271,14 @@ class CharacterBookNativeTest {
|
||||
drainReads(f); runQueued(f);
|
||||
assertEquals(volumes + 1, a.books.size(), "One native action cannot open two volumes");
|
||||
}
|
||||
assertTrue(volumes > 0); assertTrue(all.toString().contains(digits), "Every digit remains reachable in order");
|
||||
assertTrue(volumes > 0);expectedNavigations=volumes; assertTrue(all.toString().contains(digits), "Every digit remains reachable in order");
|
||||
var previous = PartyBookNativeTest.button(a, "[Previous pages]");
|
||||
a.core.inventory.getInventory().setSelectedSlot(1);
|
||||
PartyBookNativeTest.click(f, a, previous); drainReads(f); runQueued(f);
|
||||
assertEquals(volumes + 1, a.books.size(), "Stale held-slot navigation has no authority");
|
||||
assertArrayEquals(bytes, Files.readAllBytes(data.resolve("characters").resolve(saved.id() + ".yml")));
|
||||
}
|
||||
var events=new java.util.ArrayList<com.google.gson.JsonObject>();try(var files=Files.walk(data.resolve("events"))){for(var file:files.filter(path->path.getFileName().toString().endsWith(".jsonl")).toList())for(var line:Files.readAllLines(file))if(!line.isBlank())events.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());}assertEquals(expectedNavigations,events.stream().filter(event->event.get("type").getAsString().endsWith("character_book.navigated")).count());assertEquals(expectedNavigations,events.stream().filter(event->event.get("type").getAsString().endsWith("character_book.navigated")&&event.getAsJsonObject("data").get("native_open").getAsString().equals("acknowledged")&&event.getAsJsonObject("data").get("offset").getAsInt()>0).count());
|
||||
}
|
||||
|
||||
@Test void acknowledgedRenameTransferAndReloadInspectTheCurrentRecordWithoutCarryingTheFormerHoldersView() throws Exception {
|
||||
|
||||
@@ -110,7 +110,7 @@ class FantasyHealthNativeTest {
|
||||
viewer.core.inventory.setItemInOffHand(null); viewer.core.location.setWorld(defeated.world);
|
||||
f.players.put(defeated.core.id, defeated.player); f.players.put(viewer.core.id, viewer.player);
|
||||
f.activate(defeated);
|
||||
f.particlesCommand.execute(defeated.player, "fantasyparticles", new String[]{"all", "off"}); f.tick();
|
||||
PartyBookNativeTest.disableAllParticles(f,defeated);
|
||||
defeated.particleCalls.clear(); viewer.particleCalls.clear(); defeated.messages.clear();
|
||||
var source = new org.bukkit.craftbukkit.damage.CraftDamageSource(new net.minecraft.world.damagesource.DamageSource(
|
||||
org.bukkit.craftbukkit.damage.CraftDamageType.bukkitToMinecraftHolder(org.bukkit.damage.DamageType.OUT_OF_WORLD)));
|
||||
|
||||
@@ -610,7 +610,7 @@ class FantasyStickLifecycleNativeTest {
|
||||
f.plugin.setEnabled(true);
|
||||
var actor = new Actor(data); f.players.put(actor.core.id, actor.player);
|
||||
actor.core.inventory.setItem(4, new org.bukkit.inventory.ItemStack(org.bukkit.Material.SHULKER_BOX));
|
||||
f.particlesCommand.execute(actor.player, "fantasyparticles", new String[]{"all", "off"}); f.tick();
|
||||
PartyBookNativeTest.disableAllParticles(f,actor);
|
||||
actor.messages.clear(); actor.particles.clear(); actor.particleCalls.clear();
|
||||
var before = actor.core.snapshot(); f.changed(actor); f.tick(); f.tick();
|
||||
assertArrayEquals(before, actor.core.snapshot()); assertNull(actor.core.marker());
|
||||
|
||||
@@ -52,7 +52,7 @@ class FireballNativeTest {
|
||||
var viewer = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
viewer.core.inventory.setItemInOffHand(null); viewer.core.location.setWorld(a.world);
|
||||
f.players.put(viewer.core.id, viewer.player);
|
||||
f.particlesCommand.execute(a.player, "fantasyparticles", new String[]{"all", "off"}); PartyBookNativeTest.drain(f);
|
||||
PartyBookNativeTest.disableAllParticles(f,a);
|
||||
a.particleCalls.clear(); viewer.particleCalls.clear();
|
||||
f.dispatcher.accept(cast); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
assertTrue(a.particleCalls.isEmpty(), "The opted-out caster receives no Fireball particles");
|
||||
|
||||
@@ -80,7 +80,7 @@ class FrostNovaNativeTest {
|
||||
var viewer = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
viewer.core.inventory.setItemInOffHand(null); viewer.core.location.setWorld(caster.world);
|
||||
fixture.players.put(viewer.core.id, viewer.player);
|
||||
fixture.particlesCommand.execute(caster.player, "fantasyparticles", new String[]{"all", "off"}); PartyBookNativeTest.drain(fixture);
|
||||
PartyBookNativeTest.disableAllParticles(fixture,caster);
|
||||
caster.particleCalls.clear(); viewer.particleCalls.clear();
|
||||
use(fixture, caster); drain(fixture, 2);
|
||||
assertTrue(caster.particleCalls.isEmpty(), "The opted-out caster receives no Frost Nova particles");
|
||||
|
||||
@@ -55,7 +55,7 @@ class FrostShardNativeTest {
|
||||
var viewer = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
viewer.core.inventory.setItemInOffHand(null); viewer.core.location.setWorld(a.world);
|
||||
f.players.put(viewer.core.id, viewer.player);
|
||||
f.particlesCommand.execute(a.player, "fantasyparticles", new String[]{"all", "off"}); PartyBookNativeTest.drain(f);
|
||||
PartyBookNativeTest.disableAllParticles(f,a);
|
||||
a.particleCalls.clear(); viewer.particleCalls.clear();
|
||||
f.dispatcher.accept(cast); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
assertTrue(a.particleCalls.isEmpty(), "The opted-out caster receives no Frost Shard particles");
|
||||
|
||||
@@ -30,7 +30,7 @@ class MeteorNativeTest {
|
||||
|
||||
@Test void characterBookPurchaseEquippedGroundWarningAndNativeImpact()throws Exception{
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var caster=new FantasyStickLifecycleNativeTest.Actor(data);prepare(data,caster,false);f.players.put(caster.core.id,caster.player);f.activate(caster);var placement=placement(caster,24,BlockFace.UP);var enemy=mock(LivingEntity.class);locate(enemy,caster.world,new UUID(0,2),2,65,0);doReturn(List.of(enemy)).when(caster.world).getNearbyEntities(any(Location.class),anyDouble(),anyDouble(),anyDouble());var damaged=new ArrayList<Double>();doAnswer(call->{double amount=call.getArgument(0);damaged.add(amount);var source=(DamageSource)call.getArgument(1);f.dispatcher.accept(new EntityDamageByEntityEvent(caster.player,enemy,EntityDamageEvent.DamageCause.MAGIC,source,amount));return null;}).when(enemy).damage(anyDouble(),any(DamageSource.class));
|
||||
caster.core.inventory.getInventory().setSelectedSlot(0);PartyBookNativeTest.open(f,caster);String text=PartyBookNativeTest.text(caster).replaceAll("\\s+","");assertTrue(text.contains("Meteor"));assertTrue(text.contains("INT26"));assertTrue(text.contains("WIS20"));assertTrue(text.contains("CON12"));assertTrue(text.contains("2-secondwarning"));PartyBookNativeTest.click(f,caster,button(caster,"[Learn]","meteor"));drain(f,2);var viewer=new FantasyStickLifecycleNativeTest.Actor(data);viewer.core.inventory.setItemInOffHand(null);viewer.core.location.setWorld(caster.world);f.players.put(viewer.core.id,viewer.player);f.particlesCommand.execute(caster.player,"fantasyparticles",new String[]{"all","off"});PartyBookNativeTest.drain(f);caster.particleCalls.clear();viewer.particleCalls.clear();use(f,caster);drain(f,2);assertTrue(damaged.isEmpty());assertEquals(1L,f.delayedTicks.getLast());for(int i=0;i<39;i++)f.delayed.removeFirst().run();assertTrue(damaged.isEmpty(),"The 40-tick warning must remain escapable");f.delayed.removeFirst().run();assertEquals(List.of(22d),damaged);assertTrue(caster.particleCalls.isEmpty(),"The opted-out caster receives no Meteor warning or impact particles");assertTrue(viewer.particleCalls.stream().filter(call->call[0]==Particle.DUST&&call[2].equals(24)).count()>=40);assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==Particle.EXPLOSION_EMITTER&&call[2].equals(2)));assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==Particle.FLAME&&call[2].equals(48)));verify(caster.world,atLeastOnce()).playSound(any(Location.class),eq(Sound.ENTITY_GENERIC_EXPLODE),eq(1f),eq(.7f));verify(placement,never()).setType(any(Material.class));assertNotNull(storage(f).find(caster.core.record.id()).get().orElseThrow().cooldowns().get("meteor"));assertEquals(BigInteger.ZERO,storage(f).find(caster.core.record.id()).get().orElseThrow().unspentPoints());
|
||||
caster.core.inventory.getInventory().setSelectedSlot(0);PartyBookNativeTest.open(f,caster);String text=PartyBookNativeTest.text(caster).replaceAll("\\s+","");assertTrue(text.contains("Meteor"));assertTrue(text.contains("INT26"));assertTrue(text.contains("WIS20"));assertTrue(text.contains("CON12"));assertTrue(text.contains("2-secondwarning"));PartyBookNativeTest.click(f,caster,button(caster,"[Learn]","meteor"));drain(f,2);var viewer=new FantasyStickLifecycleNativeTest.Actor(data);viewer.core.inventory.setItemInOffHand(null);viewer.core.location.setWorld(caster.world);f.players.put(viewer.core.id,viewer.player);PartyBookNativeTest.disableAllParticles(f,caster);caster.particleCalls.clear();viewer.particleCalls.clear();use(f,caster);drain(f,2);assertTrue(damaged.isEmpty());assertEquals(1L,f.delayedTicks.getLast());for(int i=0;i<39;i++)f.delayed.removeFirst().run();assertTrue(damaged.isEmpty(),"The 40-tick warning must remain escapable");f.delayed.removeFirst().run();assertEquals(List.of(22d),damaged);assertTrue(caster.particleCalls.isEmpty(),"The opted-out caster receives no Meteor warning or impact particles");assertTrue(viewer.particleCalls.stream().filter(call->call[0]==Particle.DUST&&call[2].equals(24)).count()>=40);assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==Particle.EXPLOSION_EMITTER&&call[2].equals(2)));assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==Particle.FLAME&&call[2].equals(48)));verify(caster.world,atLeastOnce()).playSound(any(Location.class),eq(Sound.ENTITY_GENERIC_EXPLODE),eq(1f),eq(.7f));verify(placement,never()).setType(any(Material.class));assertNotNull(storage(f).find(caster.core.record.id()).get().orElseThrow().cooldowns().get("meteor"));assertEquals(BigInteger.ZERO,storage(f).find(caster.core.record.id()).get().orElseThrow().unspentPoints());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,17 @@ class PartyBookNativeTest {
|
||||
Runnable task;
|
||||
while ((task = f.tasks.poll()) != null) task.run();
|
||||
}
|
||||
static void disableAllParticles(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
FantasyStickLifecycleNativeTest.Actor actor) throws Exception {
|
||||
int before=actor.messages.size();
|
||||
f.particlesCommand.execute(actor.player,"fantasyparticles",new String[]{"all","off"});
|
||||
for(int attempt=0;attempt<120;attempt++){
|
||||
Runnable task=f.tasks.poll(25,java.util.concurrent.TimeUnit.MILLISECONDS);if(task!=null)task.run();
|
||||
if(actor.messages.subList(Math.min(before,actor.messages.size()),actor.messages.size()).stream()
|
||||
.anyMatch(message->message.contains("All Fantasy Stick particles")&&message.contains("off")))return;
|
||||
}
|
||||
fail("Particle preference acknowledgement was not published");
|
||||
}
|
||||
static net.minecraft.network.chat.ClickEvent.Custom button(FantasyStickLifecycleNativeTest.Actor a, String label) {
|
||||
var book = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(a.books.getLast())
|
||||
.get(net.minecraft.core.component.DataComponents.WRITTEN_BOOK_CONTENT);
|
||||
|
||||
@@ -41,7 +41,7 @@ class SanctuaryNativeTest {
|
||||
String text=PartyBookNativeTest.text(caster).replaceAll("\\s+","");assertTrue(text.contains("Sanctuary"));assertTrue(text.contains("WIS24"));assertTrue(text.contains("CHR20"));assertTrue(text.contains("CON10"));
|
||||
PartyBookNativeTest.click(fixture,caster,button(caster,"[Learn]","sanctuary"));drain(fixture,2);
|
||||
var viewer=new FantasyStickLifecycleNativeTest.Actor(data);viewer.core.inventory.setItemInOffHand(null);viewer.core.location.setWorld(caster.world);fixture.players.put(viewer.core.id,viewer.player);
|
||||
fixture.particlesCommand.execute(caster.player,"fantasyparticles",new String[]{"all","off"});PartyBookNativeTest.drain(fixture);caster.particleCalls.clear();viewer.particleCalls.clear();
|
||||
PartyBookNativeTest.disableAllParticles(fixture,caster);caster.particleCalls.clear();viewer.particleCalls.clear();
|
||||
use(fixture,caster);drain(fixture,2);assertEquals(2L,fixture.delayedTicks.getLast());
|
||||
for(int i=0;i<10;i++)fixture.delayed.removeFirst().run();drain(fixture,4);assertEquals(6,ally.core.health,0);
|
||||
assertTrue(caster.particleCalls.isEmpty(),"The opted-out caster receives no Sanctuary particles");assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==org.bukkit.Particle.END_ROD&&call[2].equals(12)),"An enabled nearby viewer retains Sanctuary visuals");
|
||||
|
||||
@@ -35,7 +35,7 @@ class WarBannerNativeTest {
|
||||
@Test void bookAcquisitionSolidTopPlacementBenefitsPartyAndRewardsUsedDamage()throws Exception{
|
||||
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var caster=new FantasyStickLifecycleNativeTest.Actor(data);var ally=new FantasyStickLifecycleNativeTest.Actor(data);prepare(data,caster,false,false);f.players.put(caster.core.id,caster.player);f.players.put(ally.core.id,ally.player);f.activate(caster);f.activate(ally);party(f,caster,ally);placement(caster);nearby(caster,List.of(ally.player));var resistance=attribute(ally.player);resistance.modifiers().add(new AttributeModifier(new NamespacedKey("test","unrelated_knockback"),.2,AttributeModifier.Operation.ADD_NUMBER));
|
||||
caster.core.inventory.getInventory().setSelectedSlot(0);PartyBookNativeTest.open(f,caster);String text=PartyBookNativeTest.text(caster).replaceAll("\\s+","");assertTrue(text.contains("WarBanner"));assertTrue(text.contains("STR22"));assertTrue(text.contains("CHR22"));assertTrue(text.contains("CON12"));assertTrue(text.contains("25%"));assertTrue(text.contains("50%"));
|
||||
PartyBookNativeTest.click(f,caster,button(caster,"[Learn]","war_banner"));drain(f,2);var viewer=new FantasyStickLifecycleNativeTest.Actor(data);viewer.core.inventory.setItemInOffHand(null);viewer.core.location.setWorld(caster.world);f.players.put(viewer.core.id,viewer.player);f.particlesCommand.execute(caster.player,"fantasyparticles",new String[]{"all","off"});PartyBookNativeTest.drain(f);caster.particleCalls.clear();viewer.particleCalls.clear();use(f,caster);drain(f,2);assertTrue(caster.particleCalls.isEmpty(),"The opted-out caster receives no War Banner particles");assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==Particle.DUST&&call[2].equals(16)),"An enabled nearby viewer retains War Banner visuals");assertEquals(200L,f.delayedTicks.getLast());assertEquals(.7,resistance.value(),1e-9);assertNotNull(storage(f).find(caster.core.record.id()).get().orElseThrow().cooldowns().get("war_banner"));
|
||||
PartyBookNativeTest.click(f,caster,button(caster,"[Learn]","war_banner"));drain(f,2);var viewer=new FantasyStickLifecycleNativeTest.Actor(data);viewer.core.inventory.setItemInOffHand(null);viewer.core.location.setWorld(caster.world);f.players.put(viewer.core.id,viewer.player);PartyBookNativeTest.disableAllParticles(f,caster);caster.particleCalls.clear();viewer.particleCalls.clear();use(f,caster);drain(f,2);assertTrue(caster.particleCalls.isEmpty(),"The opted-out caster receives no War Banner particles");assertTrue(viewer.particleCalls.stream().anyMatch(call->call[0]==Particle.DUST&&call[2].equals(16)),"An enabled nearby viewer retains War Banner visuals");assertEquals(200L,f.delayedTicks.getLast());assertEquals(.7,resistance.value(),1e-9);assertNotNull(storage(f).find(caster.core.record.id()).get().orElseThrow().cooldowns().get("war_banner"));
|
||||
var enemy=mock(LivingEntity.class);when(enemy.getUniqueId()).thenReturn(UUID.randomUUID());when(enemy.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);when(enemy.getLocation()).thenReturn(new Location(caster.world,4,65,0));when(caster.world.getUID()).thenReturn(UUID.randomUUID());var source=DamageSource.builder(org.bukkit.damage.DamageType.PLAYER_ATTACK).withCausingEntity(ally.player).withDirectEntity(ally.player).build();var hit=new EntityDamageByEntityEvent(ally.player,enemy,EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,10);f.dispatcher.accept(hit);assertEquals(12.5,hit.getDamage(),1e-9);f.dispatcher.accept(new EntityDeathEvent(enemy,source,new ArrayList<>(),0));drain(f,4);assertEquals(BigInteger.valueOf(5),storage(f).find(caster.core.record.id()).get().orElseThrow().experience());assertEquals(BigInteger.valueOf(5),storage(f).find(ally.core.record.id()).get().orElseThrow().experience());
|
||||
f.delayed.getLast().run();assertEquals(.2,resistance.value(),1e-9,"Owned cleanup preserves unrelated knockback modifiers");var expired=new EntityDamageByEntityEvent(ally.player,enemy,EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,10);f.dispatcher.accept(expired);assertEquals(10,expired.getDamage(),1e-9);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user