fix(party): submit names through native registrar anvil (#19)
Release / release (push) Successful in 1m32s

Closes OpenProject #479.
This commit was merged in pull request #19.
This commit is contained in:
dmg committed 2026-10-17 15:07:25 +00:00
commit 608a5fd2aa
5 files changed
+325 -10

No files matched your search

@@ -33,7 +33,7 @@ public final class PartyManagement implements AutoCloseable {
private void mutateParty(Player player,UUID id,java.util.function.BiFunction<Context,CharacterParty,java.util.concurrent.CompletableFuture<CharacterPartyDirectory.Result>> action,Consumer<Outcome> result){var context=context(player,id);if(context.isEmpty()){result.accept(new Outcome(Status.INVALID_SESSION,Optional.empty()));return;}var party=parties.partyOf(context.orElseThrow().record().id()).orElse(null);if(party==null){result.accept(new Outcome(Status.FAILED,Optional.empty()));return;}try{action.apply(context.orElseThrow(),party).whenComplete((value,failure)->complete(value,failure,result));}catch(RuntimeException failure){fail(failure,result);}}
private void complete(CharacterPartyDirectory.Result value,Throwable failure,Consumer<Outcome> result){if(failure!=null){fail(failure,result);return;}publish(()->result.accept(new Outcome(value.status()==CharacterPartyDirectory.Status.APPLIED?Status.APPLIED:map(value.status()),value.party())));}
public int registrationPrice(){return prices.registrationPrice();}public int leaderChangePrice(){return prices.leaderChangePrice();}
public Optional<CharacterRecord> active(Player player,UUID registrar){return context(player,registrar).map(Context::record);}public Optional<UUID> activeSession(Player player,UUID registrar){return context(player,registrar).map(value->value.active().session());}public boolean eligibleInvitationTarget(UUID character){return parties.partyOf(character).isEmpty()&&sessions.activeCharacter(character).filter(sessions::current).flatMap(sessions::currentRecord).filter(value->!value.dead()).isPresent();}public Optional<CharacterRecord> activeCharacter(Player player){var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);return active==null?Optional.empty():sessions.currentRecord(active).filter(value->!value.dead());}
public Optional<CharacterRecord> active(Player player,UUID registrar){return context(player,registrar).map(Context::record);}public Optional<UUID> activeSession(Player player,UUID registrar){return context(player,registrar).map(value->value.active().session());}public Status accessStatus(Player player,UUID registrarId,UUID character,UUID session){var registrar=registrars.find(registrarId).orElse(null);if(!parties.ready()||!recovered||registrar==null)return Status.INVALID_SESSION;var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);var record=active==null?null:sessions.currentRecord(active).filter(value->!value.dead()).orElse(null);if(record==null||!record.id().equals(character)||!active.session().equals(session))return Status.INVALID_SESSION;var location=player.getLocation();if(location.getWorld()==null||!location.getWorld().getUID().equals(registrar.world()))return Status.TOO_FAR;double dx=location.getX()-registrar.x(),dy=location.getY()-registrar.y(),dz=location.getZ()-registrar.z();return dx*dx+dy*dy+dz*dz>36?Status.TOO_FAR:Status.APPLIED;}public boolean eligibleInvitationTarget(UUID character){return parties.partyOf(character).isEmpty()&&sessions.activeCharacter(character).filter(sessions::current).flatMap(sessions::currentRecord).filter(value->!value.dead()).isPresent();}public Optional<CharacterRecord> activeCharacter(Player player){var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);return active==null?Optional.empty():sessions.currentRecord(active).filter(value->!value.dead());}
private Optional<Context> context(Player player,UUID registrarId){if(!parties.ready()||!recovered)return Optional.empty();var registrar=registrars.find(registrarId).orElse(null);var active=sessions.active(player.getUniqueId()).filter(sessions::current).orElse(null);var record=active==null?null:sessions.currentRecord(active).filter(value->!value.dead()).orElse(null);if(registrar==null||record==null)return Optional.empty();var location=player.getLocation();if(location.getWorld()==null||!location.getWorld().getUID().equals(registrar.world()))return Optional.empty();double dx=location.getX()-registrar.x(),dy=location.getY()-registrar.y(),dz=location.getZ()-registrar.z();if(dx*dx+dy*dy+dz*dz>36)return Optional.empty();return Optional.of(new Context(active,record,registrar));}
private CharacterParty partyForInvitation(UUID invitation){return parties.all().stream().filter(party->party.invitations().stream().anyMatch(value->value.id().equals(invitation))).findFirst().orElse(null);}
private games.dmg.fantasystick.events.PartyEventBridge.Action audit(Player player,Context context,String kind,UUID target,UUID invitation,CharacterParty before,BigInteger charge,UUID operation){return audit(player,context,kind,target,invitation,before,charge,operation,"requested");}
@@ -9,6 +9,8 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.PrepareAnvilEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
@@ -19,17 +21,24 @@ import org.bukkit.plugin.Plugin;
public final class PartyRegistrarMenus implements Listener,AutoCloseable {
private enum Screen{ROOT,NAME,INVITATIONS,OUTGOING,INVITE,MEMBERS}
private record Holder(UUID registrar,UUID character,UUID session,Screen screen,int page) implements InventoryHolder{public Inventory getInventory(){return null;}}
private final Server server;private final PartyManagement management;private final NamespacedKey actionKey,targetKey;private boolean closed;
private record Submission(UUID player,UUID registrar,UUID character,UUID session){}
private final Server server;private final PartyManagement management;private final NamespacedKey actionKey,targetKey;private final Set<Submission> submissions=new HashSet<>();private final Map<Inventory,Holder> nameScreens=new IdentityHashMap<>();private boolean closed;
public PartyRegistrarMenus(Plugin plugin,PartyManagement management){server=plugin.getServer();this.management=Objects.requireNonNull(management);actionKey=new NamespacedKey(plugin,"party_registrar_action");targetKey=new NamespacedKey(plugin,"party_registrar_target");}
public void open(Player player,UUID registrar){open(player,registrar,Screen.ROOT,0);}
private void open(Player player,UUID registrar,Screen screen,int page){if(closed)return;var record=management.active(player,registrar);if(record.isEmpty()){player.sendMessage("You must be actively playing a living fantasy character beside this Party Registrar.");return;}if(screen==Screen.NAME){openName(player,registrar,record.orElseThrow().id(),management.activeSession(player,registrar).orElseThrow());return;}var inventory=server.createInventory(new Holder(registrar,record.orElseThrow().id(),management.activeSession(player,registrar).orElseThrow(),screen,page),screen==Screen.ROOT?27:54,net.kyori.adventure.text.Component.text("Party Registrar"));var party=management.party(player,registrar);if(screen==Screen.ROOT){if(party.isEmpty()){inventory.setItem(11,item(Material.WRITABLE_BOOK,"Register a party — "+management.registrationPrice()+" coins","register",null));inventory.setItem(15,item(Material.MAP,"Invitations","invitations",null));}else{var group=party.orElseThrow();inventory.setItem(4,item(Material.PAPER,"Party: "+group.displayName(),"info",null));if(group.leader().equals(record.orElseThrow().id())){inventory.setItem(10,item(Material.EMERALD,"Invite active characters","invite",null));inventory.setItem(13,item(Material.PLAYER_HEAD,"Manage members","members",null));inventory.setItem(16,item(Material.BARRIER,"Disband party","disband",null));inventory.setItem(22,item(Material.MAP,"Pending invitations","outgoing",null));}else inventory.setItem(13,item(Material.BARRIER,"Leave party","leave",null));}}else if(screen==Screen.INVITATIONS){var values=management.invitations(player,registrar);fill(inventory,page,values,value->item(Material.MAP,"Invitation (left accept, right decline)","invitation",value.id()));}else if(screen==Screen.OUTGOING){var values=management.outgoingInvitations(player,registrar);fill(inventory,page,values,value->item(Material.MAP,"Cancel invitation for "+value.character(),"cancel-invitation",value.id()));}else if(screen==Screen.INVITE){var values=server.getOnlinePlayers().stream().filter(Player::isOnline).map(target->management.activeCharacter(target).orElse(null)).filter(Objects::nonNull).filter(target->management.eligibleInvitationTarget(target.id())).map(value->value.id()).distinct().sorted().toList();fill(inventory,page,values,value->item(Material.PLAYER_HEAD,"Invite "+value,"invite-target",value));}else{var values=party.map(value->value.members().stream().filter(member->!member.equals(value.leader())).sorted().toList()).orElse(List.of());fill(inventory,page,values,value->item(Material.PLAYER_HEAD,"Member "+value,"member",value,List.of(net.kyori.adventure.text.Component.text("Left click: Kick (free)"),net.kyori.adventure.text.Component.text("Right click: Transfer leadership — "+management.leaderChangePrice()+" coins"))));}player.openInventory(inventory);}
private <T> void fill(Inventory inventory,int page,List<T> values,java.util.function.Function<T,ItemStack> item){int start=Math.max(0,page)*45;for(int index=start;index<Math.min(values.size(),start+45);index++)inventory.setItem(index-start,item.apply(values.get(index)));if(page>0)inventory.setItem(45,item(Material.ARROW,"Previous","previous",null));if(start+45<values.size())inventory.setItem(53,item(Material.ARROW,"Next","next",null));}
private void openName(Player player,UUID registrar,UUID character,UUID session){var inventory=server.createInventory(new Holder(registrar,character,session,Screen.NAME,0),org.bukkit.event.inventory.InventoryType.ANVIL,net.kyori.adventure.text.Component.text("Choose party name"));inventory.setItem(0,item(Material.PAPER,"Party name","name",null));player.openInventory(inventory);}
private void openName(Player player,UUID registrar,UUID character,UUID session){var opened=org.bukkit.inventory.MenuType.ANVIL.builder().title(net.kyori.adventure.text.Component.text("Choose party name")).location(player.getLocation()).checkReachable(false).build(player);var inventory=opened.getTopInventory();nameScreens.put(inventory,new Holder(registrar,character,session,Screen.NAME,0));inventory.setItem(0,item(Material.PAPER,"Party name","name",null));free(opened);try{player.openInventory(opened);if(player.getOpenInventory()!=opened)nameScreens.remove(inventory);}catch(RuntimeException failure){nameScreens.remove(inventory);throw failure;}}
private ItemStack item(Material material,String name,String action,UUID target){return item(material,name,action,target,List.of());}
private ItemStack item(Material material,String name,String action,UUID target,List<net.kyori.adventure.text.Component> lore){var item=new ItemStack(material);var meta=item.getItemMeta();meta.customName(net.kyori.adventure.text.Component.text(name));if(!lore.isEmpty())meta.lore(lore);meta.getPersistentDataContainer().set(actionKey,PersistentDataType.STRING,action);if(target!=null)meta.getPersistentDataContainer().set(targetKey,PersistentDataType.STRING,target.toString());item.setItemMeta(meta);return item;}
@EventHandler(priority=EventPriority.HIGHEST,ignoreCancelled=true)public void click(InventoryClickEvent event){if(!(event.getInventory().getHolder() instanceof Holder holder)||!(event.getWhoClicked() instanceof Player player))return;event.setCancelled(true);if(closed)return;if(!management.active(player,holder.registrar()).map(value->value.id().equals(holder.character())).orElse(false)||!management.activeSession(player,holder.registrar()).map(holder.session()::equals).orElse(false)){player.sendMessage("Party action refused: the character or session changed.");return;}if(holder.screen()==Screen.NAME&&event.getRawSlot()==2){String name=event.getView() instanceof org.bukkit.inventory.view.AnvilView anvil?anvil.getRenameText():null;if(name!=null)management.register(player,holder.registrar(),name,outcome->respond(player,holder.registrar(),outcome,"Registered party."));return;}var clicked=event.getCurrentItem();if(clicked==null)return;var data=clicked.getItemMeta().getPersistentDataContainer();String action=data.get(actionKey,PersistentDataType.STRING);UUID target=uuid(data.get(targetKey,PersistentDataType.STRING));if("register".equals(action)){open(player,holder.registrar(),Screen.NAME,0);return;}if("invitations".equals(action)){open(player,holder.registrar(),Screen.INVITATIONS,0);return;}if("invite".equals(action)){open(player,holder.registrar(),Screen.INVITE,0);return;}if("outgoing".equals(action)){open(player,holder.registrar(),Screen.OUTGOING,0);return;}if("members".equals(action)){open(player,holder.registrar(),Screen.MEMBERS,0);return;}if("previous".equals(action)){open(player,holder.registrar(),holder.screen(),holder.page()-1);return;}if("next".equals(action)){open(player,holder.registrar(),holder.screen(),holder.page()+1);return;}if("leave".equals(action)){management.leave(player,holder.registrar(),outcome->respond(player,holder.registrar(),outcome,"Left party."));return;}if("disband".equals(action)){management.disband(player,holder.registrar(),outcome->respond(player,holder.registrar(),outcome,"Party ended."));return;}if(target==null)return;if("invite-target".equals(action)){management.invite(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Invitation saved."));return;}if("cancel-invitation".equals(action)){management.cancel(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Invitation cancelled."));return;}if("invitation".equals(action)){if(event.isRightClick())management.decline(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Invitation declined."));else management.accept(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Joined party."));return;}if("member".equals(action)){if(event.isRightClick())management.transfer(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Leadership transferred."));else management.kick(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Member removed."));}}
@EventHandler(priority=EventPriority.HIGHEST,ignoreCancelled=true)public void drag(org.bukkit.event.inventory.InventoryDragEvent event){if(event.getInventory().getHolder() instanceof Holder)event.setCancelled(true);}
@EventHandler(priority=EventPriority.HIGHEST)public void prepare(PrepareAnvilEvent event){var holder=holder(event.getInventory());if(closed||holder==null||holder.screen()!=Screen.NAME)return;var source=event.getInventory().getItem(0);if(!action(source,"name")){event.setResult(null);return;}var result=source.clone();var meta=result.getItemMeta();String name=event.getView().getRenameText();meta.customName(net.kyori.adventure.text.Component.text(name==null||name.isBlank()?"Submit party name":name));result.setItemMeta(meta);event.setResult(result);free(event.getView());}
@EventHandler(priority=EventPriority.HIGHEST,ignoreCancelled=true)public void click(InventoryClickEvent event){if(!(event.getWhoClicked() instanceof Player player))return;var holder=holder(event.getInventory());if(holder==null)return;event.setCancelled(true);if(closed)return;var access=management.accessStatus(player,holder.registrar(),holder.character(),holder.session());if(access!=PartyManagement.Status.APPLIED){player.sendMessage(access==PartyManagement.Status.TOO_FAR?"Party action refused: too far from the Party Registrar.":"Party action refused: the character or session changed.");return;}if(holder.screen()==Screen.NAME&&event.getRawSlot()==2){if(!(event.getView() instanceof org.bukkit.inventory.view.AnvilView anvil)||!action(event.getCurrentItem(),"name")){player.sendMessage("Party action refused: the name result is unavailable.");return;}var submission=new Submission(player.getUniqueId(),holder.registrar(),holder.character(),holder.session());if(!submissions.add(submission)){player.sendMessage("Party action refused: submission already in progress.");return;}String name=anvil.getRenameText();try{management.register(player,holder.registrar(),name==null?"":name,outcome->{submissions.remove(submission);respond(player,holder.registrar(),outcome,"Registered party.");});}catch(RuntimeException failure){submissions.remove(submission);throw failure;}return;}var clicked=event.getCurrentItem();if(clicked==null)return;var data=clicked.getItemMeta().getPersistentDataContainer();String action=data.get(actionKey,PersistentDataType.STRING);UUID target=uuid(data.get(targetKey,PersistentDataType.STRING));if("register".equals(action)){open(player,holder.registrar(),Screen.NAME,0);return;}if("invitations".equals(action)){open(player,holder.registrar(),Screen.INVITATIONS,0);return;}if("invite".equals(action)){open(player,holder.registrar(),Screen.INVITE,0);return;}if("outgoing".equals(action)){open(player,holder.registrar(),Screen.OUTGOING,0);return;}if("members".equals(action)){open(player,holder.registrar(),Screen.MEMBERS,0);return;}if("previous".equals(action)){open(player,holder.registrar(),holder.screen(),holder.page()-1);return;}if("next".equals(action)){open(player,holder.registrar(),holder.screen(),holder.page()+1);return;}if("leave".equals(action)){management.leave(player,holder.registrar(),outcome->respond(player,holder.registrar(),outcome,"Left party."));return;}if("disband".equals(action)){management.disband(player,holder.registrar(),outcome->respond(player,holder.registrar(),outcome,"Party ended."));return;}if(target==null)return;if("invite-target".equals(action)){management.invite(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Invitation saved."));return;}if("cancel-invitation".equals(action)){management.cancel(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Invitation cancelled."));return;}if("invitation".equals(action)){if(event.isRightClick())management.decline(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Invitation declined."));else management.accept(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Joined party."));return;}if("member".equals(action)){if(event.isRightClick())management.transfer(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Leadership transferred."));else management.kick(player,holder.registrar(),target,outcome->respond(player,holder.registrar(),outcome,"Member removed."));}}
@EventHandler(priority=EventPriority.HIGHEST,ignoreCancelled=true)public void drag(org.bukkit.event.inventory.InventoryDragEvent event){if(holder(event.getInventory())!=null)event.setCancelled(true);}
@EventHandler public void close(InventoryCloseEvent event){clearName(event.getInventory());}
private void respond(Player player,UUID registrar,PartyManagement.Outcome outcome,String success){if(!player.isOnline())return;if(outcome.status()==PartyManagement.Status.APPLIED){player.sendMessage(success);open(player,registrar);}else player.sendMessage("Party action refused: "+outcome.status().name().toLowerCase(Locale.ROOT).replace('_',' ')+".");}
private Holder holder(Inventory inventory){return inventory.getHolder() instanceof Holder holder?holder:nameScreens.get(inventory);}
private void clearName(Inventory inventory){if(nameScreens.remove(inventory)==null)return;for(int slot:new int[]{0,2})if(action(inventory.getItem(slot),"name"))inventory.setItem(slot,null);}
private boolean action(ItemStack item,String expected){return item!=null&&item.hasItemMeta()&&expected.equals(item.getItemMeta().getPersistentDataContainer().get(actionKey,PersistentDataType.STRING));}
private static void free(org.bukkit.inventory.view.AnvilView anvil){anvil.setRepairItemCountCost(0);anvil.setRepairCost(0);anvil.setMaximumRepairCost(Integer.MAX_VALUE);anvil.setBypassCost(true);}
private static UUID uuid(String value){try{return value==null?null:UUID.fromString(value);}catch(IllegalArgumentException invalid){return null;}}
@Override public void close(){closed=true;}
@Override public void close(){closed=true;submissions.clear();for(var inventory:List.copyOf(nameScreens.keySet()))clearName(inventory);}
}
@@ -1468,6 +1468,8 @@ class FantasyStickLifecycleNativeTest {
case "callEvent" -> { dispatcher().accept((org.bukkit.event.Event) args[0]); yield null; }
default -> throw new AssertionError("Unexpected manager call " + name);
});
when(nativeServer.getPluginManager()).thenReturn(manager);
platform.when(org.bukkit.Bukkit::getPluginManager).thenReturn(manager);
var scheduler = port(BukkitScheduler.class, (name, args) -> switch (name) {
case "runTaskTimer" -> {
if ("presentation-timer".equals(failRegistrationAt)) throw new IllegalStateException("Timer rejected");
@@ -1703,6 +1705,8 @@ class FantasyStickLifecycleNativeTest {
final List<String> actionBars = new ArrayList<>();
final List<org.bukkit.inventory.ItemStack> books = new ArrayList<>();
final List<org.bukkit.inventory.Inventory> inventories = new ArrayList<>();
final List<org.bukkit.inventory.InventoryView> inventoryViews = new ArrayList<>();
org.bukkit.inventory.InventoryView openView;
int inventoryCloses;
final List<Integer> particles = new ArrayList<>();
final List<Object[]> particleCalls = new ArrayList<>();
@@ -1747,6 +1751,7 @@ class FantasyStickLifecycleNativeTest {
when(craftBoard.getHandle()).thenReturn(board);
player = own(mock(org.bukkit.craftbukkit.entity.CraftPlayer.class, this));
core.location.setWorld(world);
when(nativePlayer.getBukkitEntity()).thenReturn(player);
when(nativePlayer.getId()).thenReturn(42);
when(nativePlayer.getUUID()).thenReturn(core.id);
when(nativePlayer.getTabListDisplayName()).thenReturn(net.minecraft.network.chat.Component.literal("Ordinary tab"));
@@ -1854,8 +1859,13 @@ class FantasyStickLifecycleNativeTest {
yield null;
}
case "openBook" -> { books.add(((org.bukkit.inventory.ItemStack) args[0]).clone()); yield null; }
case "openInventory" -> { inventories.add((org.bukkit.inventory.Inventory) args[0]); yield core.view; }
case "closeInventory" -> { inventoryCloses++; yield null; }
case "getOpenInventory" -> openView == null ? core.view : openView;
case "openInventory" -> {
if (args[0] instanceof org.bukkit.inventory.Inventory inventory) { inventories.add(inventory); openView=null; yield core.view; }
var opened = (org.bukkit.inventory.InventoryView) args[0]; inventoryViews.add(opened);
inventories.add(opened.getTopInventory()); openView=opened; yield null;
}
case "closeInventory" -> { inventoryCloses++; openView=null; yield null; }
case "spawnParticle" -> { particleCalls.add(args.clone()); particles.add((Integer) args[2]); yield null; }
case "playSound" -> { if (args.length > 1 && args[1] instanceof org.bukkit.Sound sound) sounds.add(sound); yield null; }
case "showTitle" -> { titles.add((net.kyori.adventure.title.Title)args[0]); yield null; }
@@ -0,0 +1,296 @@
package games.dmg.fantasystick;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import games.dmg.fantasystick.items.CharacterSheets;
import games.dmg.fantasystick.party.CharacterPartyDirectory;
import games.dmg.fantasystick.party.PartyManagement;
import games.dmg.fantasystick.party.PartyRegistrar;
import games.dmg.fantasystick.party.PartyRegistrarController;
import games.dmg.fantasystick.party.PartyRegistrarDirectory;
import games.dmg.fantasystick.persistence.CharacterRecord;
import games.dmg.fantasystick.persistence.CharacterStorage;
import games.dmg.fantasystick.persistence.YamlCharacterRepository;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import net.minecraft.world.inventory.AnvilMenu;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Villager;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.MenuType;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.view.AnvilView;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class PartyRegistrarNameSubmissionNativeTest {
@TempDir Path data;
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
@Test void registeredNativeResultIsFreeAndCreatesOnePartyAtZeroVanillaExperience() throws Exception {
try (var path = new RegistrarPath(data.resolve("success"), BigInteger.valueOf(20))) {
var view = path.openName();
int paperBefore = paperCount(path.actor.core.inventory);
path.actor.core.inventory.setItem(7, new ItemStack(Material.DIAMOND, 3));
var unrelatedBefore = path.actor.core.inventory.getItem(7).clone();
var inputBefore = view.getTopInventory().getItem(0).clone();
var result = path.prepare(view, " Cafe\u0301 Heroes ");
assertEquals(0, view.getRepairCost());
assertEquals(0, view.getRepairItemCountCost());
assertTrue(view.canBypassCost());
assertNotNull(result, "The registered native prepare path must make the result available");
assertEquals(Material.PAPER, result.getType());
assertNotNull(view.getTopInventory().getLocation());
var nativeMenu = assertInstanceOf(AnvilMenu.class,
((org.bukkit.craftbukkit.inventory.CraftInventoryView<?, ?>) view).getHandle());
assertFalse(nativeMenu.checkReachable, "The virtual text-entry menu must not require a physical anvil block");
var priorCancellation = path.submit(view, true);
assertTrue(priorCancellation.isCancelled());
path.assertUnchanged(BigInteger.valueOf(20));
var first = path.submit(view);
var repeated = path.submit(view);
assertTrue(first.isCancelled(), "The plugin owns result submission instead of native taking");
assertTrue(repeated.isCancelled());
assertTrue(path.actor.messages.stream().anyMatch(message -> message.contains("already in progress")),
path.actor.messages::toString);
path.await(() -> path.parties.partyOf(path.actor.core.record.id()).isPresent()
&& path.actor.messages.contains("Registered party."));
var party = path.parties.partyOf(path.actor.core.record.id()).orElseThrow();
assertEquals("Café Heroes", party.displayName());
assertEquals(path.actor.core.record.id(), party.leader());
assertEquals(1, path.parties.all().size());
assertEquals(BigInteger.TEN, path.characters.find(path.actor.core.record.id()).get().orElseThrow().coins());
assertEquals(0, path.actor.player.getLevel());
verify(path.actor.player, never()).setLevel(anyInt());
assertEquals(paperBefore, paperCount(path.actor.core.inventory));
assertEquals(unrelatedBefore, path.actor.core.inventory.getItem(7));
assertTrue(path.actor.core.cursor == null || path.actor.core.cursor.getType().isAir());
assertEquals(inputBefore, view.getTopInventory().getItem(0));
assertEquals(result, view.getTopInventory().getItem(2));
assertEquals(Material.PAPER, view.getTopInventory().getItem(2).getType(),
"The cancelled plugin control remains in the view until the view closes");
assertTrue(path.actor.inventories.getLast().getItem(4).getItemMeta().getDisplayName().contains("Café Heroes"));
path.fixture.dispatcher.accept(new org.bukkit.event.inventory.InventoryCloseEvent(view));
assertNull(view.getTopInventory().getItem(0)); assertNull(view.getTopInventory().getItem(2));
nativeMenu.removed(path.actor.nativePlayer);
assertEquals(0, path.actor.nativePlayer.getInventory().getContents().stream()
.filter(item -> item.is(net.minecraft.world.item.Items.PAPER)).mapToInt(net.minecraft.world.item.ItemStack::getCount).sum());
}
}
@Test void invalidDuplicateInsufficientDistantAndStaleSubmissionsRefuseWithoutCharging() throws Exception {
try (var path = new RegistrarPath(data.resolve("invalid"), BigInteger.valueOf(20))) {
var view = path.openName(); view.getTopInventory().setItem(2, null); path.submit(view);
assertTrue(path.actor.messages.stream().anyMatch(message -> message.contains("result is unavailable")), path.actor.messages::toString);
path.prepare(view, " "); path.submit(view);
path.prepare(view, "x".repeat(33)); path.submit(view);
assertEquals(2, path.actor.messages.stream().filter(message -> message.contains("invalid name")).count(), path.actor.messages::toString);
path.assertUnchanged(BigInteger.valueOf(20));
}
try (var path = new RegistrarPath(data.resolve("duplicate"), BigInteger.valueOf(20))) {
path.parties.create(UUID.randomUUID(), UUID.randomUUID(), "Café Heroes").get();
var view = path.openName(); path.prepare(view, " cafe\u0301 heroes "); path.submit(view);
assertTrue(path.actor.messages.stream().anyMatch(message -> message.contains("name taken")), path.actor.messages::toString);
assertTrue(path.parties.partyOf(path.actor.core.record.id()).isEmpty());
assertEquals(BigInteger.valueOf(20), path.characters.find(path.actor.core.record.id()).get().orElseThrow().coins());
}
try (var path = new RegistrarPath(data.resolve("insufficient"), BigInteger.valueOf(3))) {
var view = path.openName(); path.prepare(view, "Poor Heroes"); path.submit(view);
path.await(() -> path.actor.messages.stream().anyMatch(message -> message.contains("insufficient")));
path.assertUnchanged(BigInteger.valueOf(3));
}
try (var path = new RegistrarPath(data.resolve("distant"), BigInteger.valueOf(20))) {
var view = path.openName(); path.prepare(view, "Far Heroes");
path.actor.core.location.setX(path.actor.core.location.getX() + 100);
path.submit(view);
assertTrue(path.actor.messages.stream().anyMatch(message -> message.contains("too far from the Party Registrar")),
path.actor.messages::toString);
path.assertUnchanged(BigInteger.valueOf(20));
}
try (var path = new RegistrarPath(data.resolve("stale"), BigInteger.valueOf(20))) {
var stale = path.openName(); path.prepare(stale, "Stale Heroes");
path.actor.core.inventory.setItemInOffHand(null); path.fixture.changed(path.actor);
PartyBookNativeTest.drain(path.fixture); path.fixture.awaitCharacterAndReceipt(path.actor);
var current = new YamlCharacterRepository(path.directory).load(path.actor.core.record.id()).orElseThrow();
path.actor.core.inventory.setItemInOffHand(new CharacterSheets().create(current)); path.fixture.activate(path.actor);
path.submit(stale);
assertTrue(path.actor.messages.stream().anyMatch(message -> message.contains("character or session changed")),
path.actor.messages::toString);
path.assertUnchanged(BigInteger.valueOf(20));
}
}
@Test void failedPartySaveRefundsTheCoinAndARegisteredRetrySucceedsOnce() throws Exception {
try (var path = new RegistrarPath(data.resolve("save-failure"), BigInteger.valueOf(20))) {
var partyFile = path.directory.resolve("parties.yml");
assertTrue(Files.notExists(partyFile)); Files.createDirectory(partyFile);
var view = path.openName(); path.prepare(view, "Recovered Heroes"); path.submit(view);
path.await(() -> path.actor.messages.stream().anyMatch(message -> message.contains("failed")));
path.assertUnchanged(BigInteger.valueOf(20));
assertTrue(Files.isDirectory(partyFile)); Files.delete(partyFile);
path.reenter(); view = path.openName();
path.prepare(view, "Recovered Heroes"); path.submit(view);
path.await(() -> path.parties.partyOf(path.actor.core.record.id()).isPresent()
&& path.actor.messages.contains("Registered party."));
assertEquals(1, path.parties.all().size());
assertEquals(BigInteger.TEN, path.characters.find(path.actor.core.record.id()).get().orElseThrow().coins());
}
try (var path = new RegistrarPath(data.resolve("coin-failure"), BigInteger.valueOf(20))) {
var characterFile = path.directory.resolve("characters").resolve(path.actor.core.record.id() + ".yml");
var backup = path.directory.resolve("character-backup.yml");
Files.move(characterFile, backup); Files.createDirectory(characterFile);
var view = path.openName(); path.prepare(view, "Coin Recovery"); path.submit(view);
path.await(() -> path.actor.messages.stream().anyMatch(message -> message.contains("error")));
assertTrue(path.parties.partyOf(path.actor.core.record.id()).isEmpty());
assertTrue(Files.isDirectory(characterFile)); Files.delete(characterFile); Files.move(backup, characterFile);
path.assertUnchanged(BigInteger.valueOf(20));
path.prepare(view, "Coin Recovery Retry"); path.submit(view);
path.await(() -> path.parties.partyOf(path.actor.core.record.id()).isPresent()
&& path.actor.messages.contains("Registered party."));
assertEquals(1, path.parties.all().size());
assertEquals(BigInteger.TEN, path.characters.find(path.actor.core.record.id()).get().orElseThrow().coins());
}
}
private static int paperCount(PlayerInventory inventory) {
int count = 0;
for (var item : inventory.getContents()) if (item != null && item.getType() == Material.PAPER) count += item.getAmount();
return count;
}
private static final class RegistrarPath implements AutoCloseable {
final Path directory;
final FantasyStickLifecycleNativeTest.Fixture fixture;
final FantasyStickLifecycleNativeTest.Actor actor;
final PartyRegistrar registrar;
final CharacterPartyDirectory parties;
final CharacterStorage characters;
final Villager villager;
RegistrarPath(Path directory, BigInteger coins) throws Exception {
this.directory = directory;
fixture = new FantasyStickLifecycleNativeTest.Fixture(directory); fixture.plugin.setEnabled(true);
actor = new FantasyStickLifecycleNativeTest.Actor(directory); fixture.players.put(actor.core.id, actor.player);
when(actor.nativePlayer.getInventory()).thenReturn(new net.minecraft.world.entity.player.Inventory(actor.nativePlayer,
new net.minecraft.world.entity.EntityEquipment()));
doReturn(0).when(actor.player).getLevel(); doReturn(true).when(actor.player).isOnline();
var repository = new YamlCharacterRepository(directory); var old = repository.load(actor.core.record.id()).orElseThrow();
repository.replace(withCoins(old, coins), old.revision()); fixture.activate(actor);
var management = (PartyManagement) FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "partyManagement");
management.ready().toCompletableFuture().get(5, TimeUnit.SECONDS);
var registrations = (PartyRegistrarDirectory) FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "partyRegistrars");
var location = actor.core.location; registrar = registrations.place("Central", actor.world.getUID(), location.getX(),
location.getY(), location.getZ(), location.getYaw(), location.getPitch()).get(5, TimeUnit.SECONDS);
parties = (CharacterPartyDirectory) FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "characterParties");
characters = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "characters");
villager = mock(Villager.class); when(villager.getLocation()).thenReturn(location);
when(villager.getUniqueId()).thenReturn(UUID.randomUUID());
var values = new HashMap<String, String>(); values.put("managed_npc_type", "party_registrar");
values.put("party_registrar_id", registrar.id().toString());
var pdc = mock(PersistentDataContainer.class); when(villager.getPersistentDataContainer()).thenReturn(pdc);
when(pdc.get(any(NamespacedKey.class), eq(PersistentDataType.STRING)))
.thenAnswer(call -> values.get(((NamespacedKey) call.getArgument(0)).getKey()));
interact();
}
void interact() {
var interaction = new PlayerInteractEntityEvent(actor.player, villager, EquipmentSlot.HAND);
fixture.dispatcher.accept(interaction); assertTrue(interaction.isCancelled());
}
void reenter() throws Exception {
actor.core.inventory.setItemInOffHand(null); fixture.changed(actor);
PartyBookNativeTest.drain(fixture); fixture.awaitCharacterAndReceipt(actor);
var current = new YamlCharacterRepository(directory).load(actor.core.record.id()).orElseThrow();
actor.core.inventory.setItemInOffHand(new CharacterSheets().create(current)); fixture.activate(actor); interact();
}
AnvilView openName() {
var root = actor.inventories.getLast();
var rootClick = new InventoryClickEvent(view(actor, root), InventoryType.SlotType.CONTAINER, 11,
ClickType.LEFT, InventoryAction.PICKUP_ALL);
fixture.dispatcher.accept(rootClick); assertTrue(rootClick.isCancelled());
var opened = actor.inventoryViews.getLast();
assertInstanceOf(org.bukkit.craftbukkit.inventory.view.CraftAnvilView.class, opened);
assertEquals(org.bukkit.inventory.MenuType.ANVIL, opened.getMenuType());
return (AnvilView) opened;
}
ItemStack prepare(AnvilView view, String name) {
var nativeMenu = assertInstanceOf(AnvilMenu.class,
((org.bukkit.craftbukkit.inventory.CraftInventoryView<?, ?>) view).getHandle());
assertTrue(nativeMenu.setItemName(name) || name.equals(view.getRenameText()), "Native rename input was rejected");
return view.getTopInventory().getItem(2);
}
InventoryClickEvent submit(AnvilView view) { return submit(view, false); }
InventoryClickEvent submit(AnvilView view, boolean cancelled) {
var event = new InventoryClickEvent(view, InventoryType.SlotType.RESULT, 2,
ClickType.LEFT, InventoryAction.PICKUP_ALL);
event.setCancelled(cancelled); fixture.dispatcher.accept(event); return event;
}
void assertUnchanged(BigInteger coins) throws Exception {
characters.find(UUID.randomUUID()).get(5, TimeUnit.SECONDS); parties.defeat(UUID.randomUUID()).get(5, TimeUnit.SECONDS);
assertTrue(parties.partyOf(actor.core.record.id()).isEmpty());
assertEquals(coins, characters.find(actor.core.record.id()).get(5, TimeUnit.SECONDS).orElseThrow().coins());
}
void await(BooleanSupplier condition) throws Exception {
for (int attempt = 0; attempt < 160 && !condition.getAsBoolean(); attempt++) {
var task = fixture.tasks.poll(25, TimeUnit.MILLISECONDS); if (task != null) task.run();
if (!condition.getAsBoolean()) Thread.sleep(5);
}
assertTrue(condition.getAsBoolean(), () -> "condition not reached; messages=" + actor.messages + "; parties=" + parties.all());
}
@Override public void close() throws java.io.IOException { fixture.close(); }
}
private static CharacterRecord withCoins(CharacterRecord old, BigInteger coins) {
return new CharacterRecord(old.id(), old.name(), old.revision().add(BigInteger.ONE), old.stats(), old.level(),
old.experience(), old.unspentPoints(), old.health(), old.learned(), old.loadout(), old.passives(), old.cooldowns(),
old.restrictedKills(), old.dead(), old.deathId(), old.diedAt(), coins, true, old.rewardReceipts(), old.pvpCoinEscrow(),
old.gear(), old.sheetGeneration());
}
private static org.bukkit.inventory.InventoryView view(FantasyStickLifecycleNativeTest.Actor actor, Inventory top) {
return new org.bukkit.craftbukkit.inventory.CraftAbstractInventoryView() {
@Override public Inventory getTopInventory() { return top; }
@Override public PlayerInventory getBottomInventory() { return actor.core.inventory; }
@Override public org.bukkit.entity.HumanEntity getPlayer() { return actor.player; }
@Override public InventoryType getType() { return top.getType(); }
@Override public ItemStack getCursor() { return actor.core.cursor; }
@Override public void setCursor(ItemStack item) { actor.core.cursor = item; }
@Override public String getTitle() { return "Party Registrar"; }
@Override public String getOriginalTitle() { return getTitle(); }
@Override public void setTitle(String title) { throw new AssertionError("Unexpected title mutation"); }
@Override public MenuType getMenuType() { return null; }
};
}
}
@@ -45,7 +45,7 @@ class PartyRegistrarNativeTest {
@Test void adminPlacementReconcilesProtectedVillagerAndActiveCharacterCanPayToRegister()throws Exception{
UUID invitedPlayer,invitedCharacter,invitedSession;
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);when(actor.nativePlayer.getInventory()).thenReturn(new net.minecraft.world.entity.player.Inventory(actor.nativePlayer,new net.minecraft.world.entity.EntityEquipment()));
doReturn(true).when(actor.player).hasPermission("fantasystick.admin");UUID worldId=UUID.randomUUID();when(actor.world.getUID()).thenReturn(worldId);
int chunkX=actor.core.location.getBlockX()>>4,chunkZ=actor.core.location.getBlockZ()>>4;var chunk=mock(Chunk.class);when(chunk.getWorld()).thenReturn(actor.world);when(chunk.getX()).thenReturn(chunkX);when(chunk.getZ()).thenReturn(chunkZ);when(chunk.getEntities()).thenReturn(new org.bukkit.entity.Entity[0]);when(actor.world.getChunkAt(chunkX,chunkZ)).thenReturn(chunk);when(actor.world.getChunkAt(any(Location.class))).thenReturn(chunk);when(actor.world.isChunkLoaded(chunkX,chunkZ)).thenReturn(true);var worldEntities=new ArrayList<org.bukkit.entity.Entity>();when(actor.world.getEntities()).thenAnswer(call->List.copyOf(worldEntities));
var villager=mock(Villager.class);when(villager.getUniqueId()).thenReturn(UUID.randomUUID());when(villager.getLocation()).thenReturn(actor.core.location);var values=new HashMap<String,String>();var pdc=mock(PersistentDataContainer.class);when(villager.getPersistentDataContainer()).thenReturn(pdc);doAnswer(call->{values.put(((NamespacedKey)call.getArgument(0)).getKey(),call.getArgument(2));return null;}).when(pdc).set(any(NamespacedKey.class),eq(PersistentDataType.STRING),anyString());when(pdc.get(any(NamespacedKey.class),eq(PersistentDataType.STRING))).thenAnswer(call->values.get(((NamespacedKey)call.getArgument(0)).getKey()));
@@ -54,7 +54,7 @@ class PartyRegistrarNativeTest {
var directory=(games.dmg.fantasystick.party.PartyRegistrarDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"partyRegistrars");var registrar=directory.all().getFirst();assertEquals("Central",registrar.name());
var old=new games.dmg.fantasystick.persistence.YamlCharacterRepository(data).load(actor.core.record.id()).orElseThrow();new games.dmg.fantasystick.persistence.YamlCharacterRepository(data).replace(new games.dmg.fantasystick.persistence.CharacterRecord(old.id(),old.name(),old.revision().add(BigInteger.ONE),old.stats(),old.level(),old.experience(),old.unspentPoints(),old.health(),old.learned(),old.loadout(),old.passives(),old.cooldowns(),old.restrictedKills(),false,null,null,BigInteger.valueOf(20),true,old.rewardReceipts()),old.revision());f.activate(actor);var management=(games.dmg.fantasystick.party.PartyManagement)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"partyManagement");management.ready().toCompletableFuture().get(5,java.util.concurrent.TimeUnit.SECONDS);assertTrue(management.active(actor.player,registrar.id()).isPresent());f.command.getExecutor().onCommand(actor.player,f.command,"fantasystick",new String[]{"config","party-registration-price","7"});for(int attempt=0;attempt<10&&actor.messages.stream().noneMatch(message->message.contains("Saved party-registration-price=7"));attempt++){response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();}f.command.getExecutor().onCommand(actor.player,f.command,"fantasystick",new String[]{"config","party-leader-change-price","6"});for(int attempt=0;attempt<10&&actor.messages.stream().noneMatch(message->message.contains("Saved party-leader-change-price=6"));attempt++){response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();}f.command.getExecutor().onCommand(actor.player,f.command,"fantasystick",new String[]{"config","party-registration-price"});f.command.getExecutor().onCommand(actor.player,f.command,"fantasystick",new String[]{"config","party-leader-change-price"});var controller=(games.dmg.fantasystick.party.PartyRegistrarController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"partyRegistrarController");assertTrue(controller.tagged(villager));
int openMenus=actor.inventories.size(),openBooks=actor.books.size();var mainInteraction=new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND);f.dispatcher.accept(mainInteraction);var offhandDuplicate=new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.OFF_HAND);f.dispatcher.accept(offhandDuplicate);assertTrue(mainInteraction.isCancelled());assertTrue(offhandDuplicate.isCancelled());assertEquals(openMenus+1,actor.inventories.size());assertEquals(openBooks,actor.books.size());assertEquals(Material.WRITABLE_BOOK,actor.inventories.getLast().getItem(11).getType());var vetoedInteraction=new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND);vetoedInteraction.setCancelled(true);f.dispatcher.accept(vetoedInteraction);assertEquals(openMenus+1,actor.inventories.size());assertEquals(openBooks,actor.books.size());
var menus=(games.dmg.fantasystick.party.PartyRegistrarMenus)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"partyRegistrarMenus");var root=actor.inventories.getLast();assertEquals(net.kyori.adventure.text.Component.text("Register a party — 7 coins"),root.getItem(11).getItemMeta().displayName());var openName=mock(org.bukkit.event.inventory.InventoryClickEvent.class);when(openName.getInventory()).thenReturn(root);when(openName.getWhoClicked()).thenReturn(actor.player);when(openName.getCurrentItem()).thenReturn(root.getItem(11));menus.click(openName);var anvil=actor.inventories.getLast();var submit=mock(org.bukkit.event.inventory.InventoryClickEvent.class);when(submit.getInventory()).thenReturn(anvil);when(submit.getWhoClicked()).thenReturn(actor.player);when(submit.getRawSlot()).thenReturn(2);var anvilView=mock(org.bukkit.inventory.view.AnvilView.class);when(anvilView.getRenameText()).thenReturn(" Cafe\u0301 Heroes ");when(submit.getView()).thenReturn(anvilView);menus.click(submit);var characters=(games.dmg.fantasystick.persistence.CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");characters.find(UUID.randomUUID()).join();var parties=(games.dmg.fantasystick.party.CharacterPartyDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characterParties");parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();assertEquals("Café Heroes",parties.partyOf(old.id()).orElseThrow().displayName());assertEquals(BigInteger.valueOf(13),characters.find(old.id()).join().orElseThrow().coins());
var menus=(games.dmg.fantasystick.party.PartyRegistrarMenus)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"partyRegistrarMenus");var root=actor.inventories.getLast();assertEquals(net.kyori.adventure.text.Component.text("Register a party — 7 coins"),root.getItem(11).getItemMeta().displayName());var openName=mock(org.bukkit.event.inventory.InventoryClickEvent.class);when(openName.getInventory()).thenReturn(root);when(openName.getWhoClicked()).thenReturn(actor.player);when(openName.getCurrentItem()).thenReturn(root.getItem(11));menus.click(openName);var anvilView=(org.bukkit.inventory.view.AnvilView)actor.inventoryViews.getLast();var nativeMenu=(net.minecraft.world.inventory.AnvilMenu)((org.bukkit.craftbukkit.inventory.CraftInventoryView<?,?>)anvilView).getHandle();assertTrue(nativeMenu.setItemName(" Cafe\u0301 Heroes "));var submit=new org.bukkit.event.inventory.InventoryClickEvent(anvilView,org.bukkit.event.inventory.InventoryType.SlotType.RESULT,2,org.bukkit.event.inventory.ClickType.LEFT,org.bukkit.event.inventory.InventoryAction.PICKUP_ALL);f.dispatcher.accept(submit);assertTrue(submit.isCancelled());var characters=(games.dmg.fantasystick.persistence.CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");characters.find(UUID.randomUUID()).join();var parties=(games.dmg.fantasystick.party.CharacterPartyDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characterParties");parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();assertEquals("Café Heroes",parties.partyOf(old.id()).orElseThrow().displayName());assertEquals(BigInteger.valueOf(13),characters.find(old.id()).join().orElseThrow().coins());
var member=new FantasyStickLifecycleNativeTest.Actor(data);member.core.location.setWorld(actor.world);f.players.put(member.core.id,member.player);f.activate(member);invitedPlayer=member.core.id;invitedCharacter=member.core.record.id();invitedSession=((games.dmg.fantasystick.session.EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions")).active(member.core.id).orElseThrow().session();var leaderRoot=actor.inventories.getLast();click(menus,actor.player,leaderRoot,10,false);var eligible=actor.inventories.getLast();click(menus,actor.player,eligible,0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();click(menus,actor.player,actor.inventories.getLast(),22,false);click(menus,actor.player,actor.inventories.getLast(),0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();assertTrue(parties.invitationsFor(member.core.record.id()).isEmpty());click(menus,actor.player,actor.inventories.getLast(),10,false);click(menus,actor.player,actor.inventories.getLast(),0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();controller.interact(new PlayerInteractEntityEvent(member.player,villager,EquipmentSlot.HAND));var memberRoot=member.inventories.getLast();click(menus,member.player,memberRoot,15,false);var invitationMenu=member.inventories.getLast();click(menus,member.player,invitationMenu,0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();assertTrue(parties.partyOf(member.core.record.id()).isPresent());controller.interact(new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND));click(menus,actor.player,actor.inventories.getLast(),13,false);assertEquals(List.of(net.kyori.adventure.text.Component.text("Left click: Kick (free)"),net.kyori.adventure.text.Component.text("Right click: Transfer leadership — 6 coins")),actor.inventories.getLast().getItem(0).getItemMeta().lore());click(menus,actor.player,actor.inventories.getLast(),0,true);characters.find(UUID.randomUUID()).join();parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();assertEquals(member.core.record.id(),parties.partyOf(old.id()).orElseThrow().leader());assertEquals(BigInteger.valueOf(7),characters.find(old.id()).join().orElseThrow().coins());
controller.interact(new PlayerInteractEntityEvent(member.player,villager,EquipmentSlot.HAND));click(menus,member.player,member.inventories.getLast(),13,false);click(menus,member.player,member.inventories.getLast(),0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();assertTrue(parties.partyOf(old.id()).isEmpty());controller.interact(new PlayerInteractEntityEvent(member.player,villager,EquipmentSlot.HAND));click(menus,member.player,member.inventories.getLast(),10,false);click(menus,member.player,member.inventories.getLast(),0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();controller.interact(new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND));click(menus,actor.player,actor.inventories.getLast(),15,false);click(menus,actor.player,actor.inventories.getLast(),0,true);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();controller.interact(new PlayerInteractEntityEvent(member.player,villager,EquipmentSlot.HAND));click(menus,member.player,member.inventories.getLast(),10,false);click(menus,member.player,member.inventories.getLast(),0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();controller.interact(new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND));click(menus,actor.player,actor.inventories.getLast(),15,false);click(menus,actor.player,actor.inventories.getLast(),0,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();controller.interact(new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND));click(menus,actor.player,actor.inventories.getLast(),13,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();controller.interact(new PlayerInteractEntityEvent(member.player,villager,EquipmentSlot.HAND));click(menus,member.player,member.inventories.getLast(),16,false);parties.defeat(UUID.randomUUID()).join();response=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(response);response.run();assertTrue(parties.partyOf(member.core.record.id()).isEmpty());
f.command.getExecutor().onCommand(actor.player,f.command,"fantasystick",new String[]{"party-registrar","list"});assertTrue(actor.messages.stream().anyMatch(message->message.contains(registrar.id().toString())&&message.contains("Central")));when(chunk.getEntities()).thenReturn(new org.bukkit.entity.Entity[]{villager});controller.reconcile(chunk);verify(actor.world,times(1)).spawn(any(Location.class),eq(Villager.class),any(java.util.function.Consumer.class));var damage=new org.bukkit.event.entity.EntityDamageEvent(villager,org.bukkit.event.entity.EntityDamageEvent.DamageCause.FIRE,1);controller.damage(damage);assertTrue(damage.isCancelled());var combust=new org.bukkit.event.entity.EntityCombustEvent(villager,5);controller.combust(combust);assertTrue(combust.isCancelled());var interaction=new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND);controller.interact(interaction);assertTrue(interaction.isCancelled());var leash=new org.bukkit.event.entity.PlayerLeashEntityEvent(villager,actor.player,actor.player,EquipmentSlot.HAND);controller.leash(leash);assertTrue(leash.isCancelled());