Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bdb43b2bb |
No files matched your search
@@ -166,6 +166,7 @@ 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 java.util.concurrent.CompletionStage<Void> configurationDrain = java.util.concurrent.CompletableFuture.completedStage(null);
|
||||
|
||||
@Override public void onEnable() {
|
||||
@@ -703,13 +704,14 @@ 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);
|
||||
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;
|
||||
getLogger().log(Level.WARNING,"Fantasy Stick local events are unavailable; gameplay remains enabled",failure);
|
||||
}
|
||||
getServer().getPluginManager().registerEvents(crafting, this);
|
||||
@@ -812,7 +814,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;
|
||||
var closingBooks = books; books = null;
|
||||
var closingPartyEvents = partyEvents; partyEvents = null;
|
||||
var closingPartyCombat = partyCombat; partyCombat = null;
|
||||
|
||||
@@ -36,6 +36,8 @@ 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 final Map<UUID, Object> adminPending = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, AdminView> adminViews = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, SavedView> savedViews = new ConcurrentHashMap<>();
|
||||
@@ -117,7 +119,9 @@ 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);}
|
||||
|
||||
@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 +237,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 +248,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);
|
||||
}
|
||||
@@ -743,15 +760,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) {
|
||||
@@ -807,7 +829,7 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
return;
|
||||
}
|
||||
input.player().sendMessage(stat + " increased to " + saved.stats().get(stat) + ".");
|
||||
load(input, 0);
|
||||
load(input, 0, false);
|
||||
return;
|
||||
}
|
||||
String message = switch (purchase.status()) {
|
||||
@@ -817,7 +839,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 +868,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 +896,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 +934,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 +948,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 +962,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));
|
||||
}
|
||||
}
|
||||
@@ -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);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());assertDoesNotThrow(()->{var request=bridge.request(input,record,0);bridge.opened(request,record,3,1);bridge.refused(request,"retry");});
|
||||
}
|
||||
|
||||
@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,7 @@ 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");
|
||||
}
|
||||
|
||||
@Test void conPurchaseKeepsItsOwnMaximumAndOneTimeGainDespiteExternalModifiers() throws Exception {
|
||||
@@ -210,6 +222,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 +260,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