From 42f9f7927c99ebe807fab408e52a6ebb32a29186 Mon Sep 17 00:00:00 2001 From: Nowko Date: Wed, 21 Oct 2026 23:00:45 +0200 Subject: [PATCH 1/2] feat: open camp inventory from ruin info --- README.md | 6 +++ .../tfminecraft/archaeo/ArcheologyPlugin.java | 31 ++++++++------ .../archaeo/command/ArchaeoCommand.java | 22 +++++----- .../archaeo/establish/CampListener.java | 13 +++++- .../archaeo/command/ArchaeoCommandTest.java | 40 ++++++++++++++++++- .../archaeo/establish/CampListenerTest.java | 24 +++++++++++ 6 files changed, 108 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 9add89f..31c71c7 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,12 @@ for that serial. Omit the player to list all sites. A player filter includes sites where any recorded character of that account directed or participated, including closed digs. This directory never teleports or edits a site. +`/archaeo ruin info ` opens the camp inventory for staff, including +other players' excavations and closed records. Omit the query while standing in +the ruin or camp chunk. The menu keeps the usual director and staff permissions; +viewing another excavation does not grant director tools. Hidden ruins without +a camp, and commands issued from console, still show the dossier as text. + Staff can inspect and remove individual recovered pieces: - `/archaeo find list <#serial> [page]` lists recovered pieces and their stored/current item IDs. diff --git a/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java b/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java index 6114980..3f0cf3a 100644 --- a/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java +++ b/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java @@ -53,6 +53,7 @@ public class ArcheologyPlugin extends JavaPlugin { private ProspectService prospect; private EstablishItem establishItem; private EstablishService establish; + private CampListener camp; private DigTools digTools; private HandPickService handPick; private FindDustService findDust; @@ -110,19 +111,18 @@ public void onEnable() { new RecoveredFindListener(this, sites, catalogs, recoveredFindItem), this); CampClosure campClosure = new CampClosure(this, sites); - getServer().getPluginManager().registerEvents( - new CampListener( - this, - sites, - catalogs, - establishItem, - establish, - handPick, - outline, - brushItem, - recoveredFindItem, - campClosure), - this); + camp = new CampListener( + this, + sites, + catalogs, + establishItem, + establish, + handPick, + outline, + brushItem, + recoveredFindItem, + campClosure); + getServer().getPluginManager().registerEvents(camp, this); findDust = new FindDustService(this, sites, catalogs.pick()); establish.setFindDust(findDust); getServer().getPluginManager().registerEvents(findDust, this); @@ -255,6 +255,11 @@ public MuseumListener museum() { return museum; } + /** @return the camp menus shared by block interaction, field books, and staff commands */ + public CampListener camp() { + return camp; + } + /** * @return configured field sheet and pencil */ diff --git a/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java b/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java index 2cc3d3a..328a1e3 100644 --- a/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java +++ b/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java @@ -711,7 +711,8 @@ private boolean handleCreate(CommandSender sender, String[] args) { } /** - * Prints a site dossier. With no query, uses the player's chunk; otherwise name or serial ({@code 12} or {@code #12}). + * Opens a camp record for players. Hidden ruins and console retain the text dossier. + * With no query, uses the player's ruin or camp chunk; otherwise resolves a name or serial. * * @param sender command issuer * @param args full argument list including {@code ruin info} @@ -720,21 +721,18 @@ private boolean handleCreate(CommandSender sender, String[] args) { private boolean handleInfo(CommandSender sender, String[] args) { Optional resolved; if (args.length == 2) { - if (!(sender instanceof Player player)) { - sender.sendMessage("Console must pass a site name or serial: /archaeo ruin info "); - return true; - } - Chunk chunk = player.getLocation().getChunk(); - resolved = sites.findByChunk(chunk.getWorld().getName(), chunk.getX(), chunk.getZ()); - if (resolved.isEmpty()) { - sender.sendMessage("No site in this chunk (" + chunk.getX() + "," + chunk.getZ() + ")."); - return true; - } + resolved = siteHere(sender); } else { String query = Arrays.stream(args).skip(2).collect(Collectors.joining(" ")); resolved = resolveQuery(sender, query); } - resolved.ifPresent(site -> sendSiteInfo(sender, site)); + resolved.ifPresent(site -> { + if (sender instanceof Player player && site.mayConsult()) { + plugin.camp().openBoard(player, site); + } else { + sendSiteInfo(sender, site); + } + }); return true; } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java b/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java index 1066a35..e2ef1e5 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java @@ -19,6 +19,7 @@ import org.bukkit.OfflinePlayer; import org.bukkit.DyeColor; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.Event; @@ -856,11 +857,19 @@ private void openStaff(Player player, Site site, boolean director) { } /** + * Opens the same camp record for an interaction, field book, or staff command. + * The excavation's world determines its work-day budget, including remote views. + * * @param player viewer * @param site excavation */ - private void openBoard(Player player, Site site) { - handPick.ensureJornada(site, player.getWorld()); + public void openBoard(Player player, Site site) { + World world = plugin.getServer().getWorld(site.getWorldName()); + if (world == null) { + player.sendMessage("The excavation world is not loaded."); + return; + } + handPick.ensureJornada(site, world); new CampBoard(site.getId(), staffing(player, site), canCloseCamp(player, site), catalogs) .open(player, site); } diff --git a/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java b/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java index 3449045..4bd8ec1 100644 --- a/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java +++ b/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java @@ -60,6 +60,44 @@ public void deniesNonStaffBeforeDispatchAndCompletion() { assertContains(run("give tracker"), "You do not have permission"); assertTrue(staff.getInventory().isEmpty()); assertTrue(complete("give", "").isEmpty()); + assertContains(run("ruin info #56"), "You do not have permission"); + } + + @Test + public void ruinInfoOpensOtherDirectorsActiveExhaustedAndClosedCampRecords() { + Site site = dossier(56, "Other camp"); + PlayerMock owner = player("Director"); + net.tfminecraft.archaeo.CharacterTestSupport.establish(site, owner.getUniqueId(), 5, 5, 84, 4, 84); + org.bukkit.Location before = staff.getLocation().clone(); + for (SiteStatus status : List.of(SiteStatus.ESTABLISHED, SiteStatus.EXHAUSTED, SiteStatus.CLOSED)) { + site.setStatus(status); + plugin.sites().save(site); + + assertTrue(run("ruin info #56").isEmpty()); + + org.bukkit.inventory.Inventory inventory = staff.getOpenInventory().getTopInventory(); + assertTrue(inventory.getHolder() instanceof net.tfminecraft.archaeo.establish.CampBoard); + assertEquals(site.getId(), ((net.tfminecraft.archaeo.establish.CampBoard) inventory.getHolder()).siteId()); + assertFalse(inventory.contains(Material.NAME_TAG)); + assertFalse(inventory.contains(Material.CAMPFIRE)); + assertEquals(owner.getUniqueId(), site.getDirector()); + assertEquals(before, staff.getLocation()); + } + } + + @Test + public void ruinInfoResolvesCampNamesAndTheCampChunkWhenTheRuinIsElsewhere() { + Site site = dossier(56, "Remote ruin"); + site.setChunkX(10); site.setChunkZ(10); + net.tfminecraft.archaeo.CharacterTestSupport.establish(site, staff.getUniqueId(), 1, 0, 20, 4, 4); + plugin.sites().save(site); + staff.teleport(new org.bukkit.Location(staff.getWorld(), 20, 4, 4)); + + for (String input : List.of("ruin info Remote ruin", "ruin info")) { + assertTrue(run(input).isEmpty()); + assertEquals(site.getId(), ((net.tfminecraft.archaeo.establish.CampBoard) + staff.getOpenInventory().getTopInventory().getHolder()).siteId()); + } } @Test @@ -301,7 +339,7 @@ public void consoleCommandsExplainWorldRequirementsAndCanGiveToNamedPlayers() { find.setBuriedConservation(90); find.setConservation(90); find.getCells().addAll(List.of(new BlockCell(1, 38, 1), new BlockCell(2, 38, 1), new BlockCell(3, 38, 1), new BlockCell(4, 38, 1))); find.woundBeforeDig(find.getCells().get(0)); find.woundFromAbove(find.getCells().get(1)); find.markCleaned(find.getCells().get(2)); site.getFinds().add(find); - List info = run("ruin info #1"); + List info = console("ruin info #1"); assertContains(info, "Created by: " + staff.getUniqueId()); assertContains(info, "Camp chunk 1,0 · table 20,4,4"); assertContains(info, "I · Y 36–39 · disturbed"); assertContains(info, "II · absent"); assertContains(info, "Hints: pottery, masonry"); assertContains(info, "Prospect confirmed: 1 · samplers: 1"); diff --git a/src/test/java/net/tfminecraft/archaeo/establish/CampListenerTest.java b/src/test/java/net/tfminecraft/archaeo/establish/CampListenerTest.java index 3c34d87..250e362 100644 --- a/src/test/java/net/tfminecraft/archaeo/establish/CampListenerTest.java +++ b/src/test/java/net/tfminecraft/archaeo/establish/CampListenerTest.java @@ -70,6 +70,30 @@ public class CampListenerTest { } @After public void teardown() { MockBukkit.unmock(); } + @Test public void remotelyOpenedBoardUsesTheExcavationWorldAndKeepsDirectorToolsRestricted() { + World otherWorld = server.addSimpleWorld("Another world"); + worker.teleport(new Location(otherWorld, 0, 70, 0)); + + listener.openBoard(worker, site); + + verify(handPick).ensureJornada(site, director.getWorld()); + verify(handPick, never()).ensureJornada(site, otherWorld); + assertTrue(worker.getOpenInventory().getTopInventory().getHolder() instanceof CampBoard); + assertFalse(worker.getOpenInventory().getTopInventory().contains(Material.NAME_TAG)); + assertFalse(worker.getOpenInventory().getTopInventory().contains(Material.CAMPFIRE)); + assertEquals(director.getUniqueId(), site.getDirector()); + } + + @Test public void unopenedExcavationWorldReportsAnErrorWithoutRefreshingTheWorkday() { + site.setWorldName("Unloaded world"); + + listener.openBoard(worker, site); + + assertEquals("The excavation world is not loaded.", worker.nextMessage()); + assertNull(worker.getOpenInventory().getTopInventory()); + verifyNoInteractions(handPick); + } + @Test public void campInteractionDeniesVanillaUseAndOpensOnlyOneMainHandBoard() { PlayerInteractEvent off = interact(director, camp, EquipmentSlot.OFF_HAND, null); listener.onInteract(off); assertEquals(Event.Result.DENY, off.useInteractedBlock()); From 78b3c452a886773a30fe9ec7f928ada6d451976d Mon Sep 17 00:00:00 2001 From: Nowko Date: Wed, 21 Oct 2026 23:10:22 +0200 Subject: [PATCH 2/2] feat: restore consumed artifacts from their original archive records --- README.md | 1 + .../archaeo/command/ArchaeoCommand.java | 7 +- .../archaeo/command/FindArchiveCommand.java | 86 ++++++++++++++++++- .../archaeo/command/ArchaeoCommandTest.java | 62 +++++++++++++ 4 files changed, 149 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 31c71c7..1781c3f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Staff can inspect and remove individual recovered pieces: - `/archaeo find list <#serial> [page]` lists recovered pieces and their stored/current item IDs. - `/archaeo find info` inspects the piece in the main hand; add `<#serial> ` for an explicit record. - `/archaeo find delete <#serial> confirm` backs up the dossier, deletes that recovered row, and removes matching physical pieces in online inventories and loaded locations. Omit `confirm` to preview. Offline inventories and unloaded chunks are not edited. +- `/archaeo find restore <#serial> confirm` recreates a lost or consumed physical item from its existing recovered row, preserving its UUID, inventory number, stored item, condition, and study history. Omit `confirm` to preview. The recipient must be online with an empty slot; known copies in online inventories, ender chests, and cursors prevent recovery. Staff must confirm that no stored or offline copy remains. This command does not alter money balances. See [artifact migration and TFMC equivalences](ARTIFACT-MIGRATION.md) before replacing old pieces with `give artifact`. Reissuing creates a new find; it does not transfer the original study or recovery history. diff --git a/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java b/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java index 328a1e3..6e2d42b 100644 --- a/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java +++ b/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java @@ -69,7 +69,7 @@ public class ArchaeoCommand implements CommandExecutor, TabCompleter { private static final List GIVE_KINDS = List.of( "tracker", "prospect", "establish", "tool", "brush", "paper", "pencil", "artifact"); private static final List WORKDAY_ACTIONS = List.of("reset"); - private static final List FIND_ACTIONS = List.of("spawn", "list", "info", "delete"); + private static final List FIND_ACTIONS = List.of("spawn", "list", "info", "delete", "restore"); private final ArcheologyPlugin plugin; private final CatalogRegistry catalogs; @@ -586,10 +586,10 @@ private Optional establishedSiteNear(Player player) { } /** - * Inspects or removes archive rows, or plants a test find at the issuer's feet. + * Inspects, removes, or restores archived finds, or plants a test find at the issuer's feet. * * @param sender staff issuer - * @param args {@code find spawn|list|info|delete ...} + * @param args {@code find spawn|list|info|delete|restore ...} * @return {@code true} always (handled) */ private boolean handleFind(CommandSender sender, String[] args) { @@ -1283,6 +1283,7 @@ private void sendUsage(CommandSender sender) { sender.sendMessage(" /archaeo find list <#serial> [page]"); sender.sendMessage(" /archaeo find info [<#serial> ]"); sender.sendMessage(" /archaeo find delete <#serial> confirm"); + sender.sendMessage(" /archaeo find restore <#serial> confirm"); sender.sendMessage(" /archaeo sketch"); sender.sendMessage(" /archaeo reload"); } diff --git a/src/main/java/net/tfminecraft/archaeo/command/FindArchiveCommand.java b/src/main/java/net/tfminecraft/archaeo/command/FindArchiveCommand.java index 81f5b05..c94ff62 100644 --- a/src/main/java/net/tfminecraft/archaeo/command/FindArchiveCommand.java +++ b/src/main/java/net/tfminecraft/archaeo/command/FindArchiveCommand.java @@ -8,6 +8,7 @@ import net.tfminecraft.archaeo.model.Site; import net.tfminecraft.archaeo.site.SitePurge; import net.tfminecraft.archaeo.site.SiteRepository; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -18,7 +19,7 @@ import java.util.UUID; import java.util.stream.Collectors; -/** Staff inspection and removal of individual recovered pieces, including retired catalog items. */ +/** Staff inspection, removal, and restoration of individual recovered pieces. */ final class FindArchiveCommand { private static final int PAGE_SIZE = 8; private final SiteRepository sites; @@ -38,6 +39,7 @@ boolean execute(CommandSender sender, String[] args) { case "list" -> list(sender, args); case "info" -> info(sender, args); case "delete" -> delete(sender, args); + case "restore" -> restore(sender, args); default -> usage(sender); } return true; @@ -132,6 +134,76 @@ private void delete(CommandSender sender, String[] args) { + " Offline inventories, unloaded chunks, and previously printed books are not edited."); } + /** Rebuilds a confirmed missing physical piece from its existing archive row. */ + private void restore(CommandSender sender, String[] args) { + if (args.length < 5 || args.length > 6 || (args.length == 6 && !"confirm".equalsIgnoreCase(args[5]))) { + sender.sendMessage("Usage: /archaeo find restore <#serial> confirm"); + return; + } + Site site = site(sender, args[2]); + if (site == null) return; + BuriedFind find = find(sender, site, args[3]); + if (find == null) return; + if (find.getState() != FindState.RECOVERED) { + sender.sendMessage("Only an already recovered find can be restored."); + return; + } + if (site.isOwnershipHeld()) { + sender.sendMessage("This excavation record is held for ownership review and cannot be restored."); + return; + } + Player target = Bukkit.getPlayerExact(args[4]); + if (target == null) { + sender.sendMessage("Player not online: " + args[4]); + return; + } + ArtifactTemplate template = catalogs.artifact(find.getArtifactId()); + if (template == null || find.getItem() == null) { + sender.sendMessage("The original item or artifact definition is missing; restoration was refused."); + return; + } + if (args.length == 5) { + describe(sender, site, find); + sender.sendMessage("Restore only if the physical item was lost or consumed. Offline inventories and stored copies are not checked."); + sender.sendMessage("Restore this same piece to " + target.getName() + ": /archaeo find restore #" + + site.getSerial() + " " + find.getId() + " " + target.getName() + " confirm"); + return; + } + for (Player online : Bukkit.getOnlinePlayers()) { + if (hasFind(online.getInventory().getContents(), find) + || hasFind(online.getEnderChest().getContents(), find) + || recovered.isThisFind(online.getItemOnCursor(), find.getId())) { + sender.sendMessage("An online player already holds this find; restoration was refused."); + return; + } + } + int slot = -1; + ItemStack[] storage = target.getInventory().getStorageContents(); + for (int index = 0; index < storage.length; index++) { + if (storage[index] == null || storage[index].getType().isAir()) { + slot = index; + break; + } + } + if (slot < 0) { + sender.sendMessage("The recipient needs an empty inventory slot; nothing was restored."); + return; + } + String grade = catalogs.pick().conservation().gradeLabel(find.getConservation()); + ItemStack stack = recovered.create(template, site, find, find.getRecoveredBy(), grade, + find.isFieldDamaged(), catalogs); + target.getInventory().setItem(slot, stack); + sender.sendMessage("Restored " + find.publicNumber(site) + " to " + target.getName() + + " with its original UUID, condition, and history."); + } + + private boolean hasFind(ItemStack[] contents, BuriedFind find) { + for (ItemStack stack : contents) { + if (recovered.isThisFind(stack, find.getId())) return true; + } + return false; + } + private void describe(CommandSender sender, Site site, BuriedFind find) { sender.sendMessage("Find " + find.publicNumber(site) + " · ruin " + site.displayLabel()); sender.sendMessage("UUID: " + find.getId()); @@ -186,7 +258,7 @@ private BuriedFind find(CommandSender sender, Site site, String token) { List complete(String[] args) { String action = args[1].toLowerCase(Locale.ROOT); - if (!List.of("list", "info", "delete").contains(action)) return List.of(); + if (!List.of("list", "info", "delete", "restore").contains(action)) return List.of(); String typed = args[args.length - 1].toLowerCase(Locale.ROOT); if (args.length == 3) { return sites.all().stream().map(site -> "#" + site.getSerial()) @@ -197,11 +269,16 @@ List complete(String[] args) { if (site == null) return List.of(); if (args.length == 4 && !"list".equals(action)) { return site.getFinds().stream() - .filter(find -> !"delete".equals(action) || find.getState() == FindState.RECOVERED) + .filter(find -> !("delete".equals(action) || "restore".equals(action)) || find.getState() == FindState.RECOVERED) .map(find -> find.getFindNumber() > 0 ? String.valueOf(find.getFindNumber()) : find.getId().toString()) .filter(number -> number.toLowerCase(Locale.ROOT).startsWith(typed)).distinct().toList(); } - return args.length == 5 && "delete".equals(action) && "confirm".startsWith(typed) ? List.of("confirm") : List.of(); + if (args.length == 5 && "restore".equals(action)) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName) + .filter(name -> name.toLowerCase(Locale.ROOT).startsWith(typed)).sorted().toList(); + } + return ((args.length == 5 && "delete".equals(action)) || (args.length == 6 && "restore".equals(action))) + && "confirm".startsWith(typed) ? List.of("confirm") : List.of(); } private static int positiveInt(String token) { @@ -218,5 +295,6 @@ static void usage(CommandSender sender) { sender.sendMessage(" /archaeo find list <#serial> [page]"); sender.sendMessage(" /archaeo find info [<#serial> ]"); sender.sendMessage(" /archaeo find delete <#serial> confirm"); + sender.sendMessage(" /archaeo find restore <#serial> confirm"); } } diff --git a/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java b/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java index 4bd8ec1..feb4982 100644 --- a/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java +++ b/src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java @@ -802,6 +802,68 @@ public void consoleCommandsExplainWorldRequirementsAndCanGiveToNamedPlayers() { assertEquals(3, copy.getFinds().getLast().getFindNumber()); } + @Test public void restoresAConsumedPieceWithoutReplacingItsArchivedIdentityOrStudyHistory() { + Site site = dossier(12, "Holy Order Project"); + run("give artifact coin #12"); + BuriedFind find = site.getFinds().getFirst(); + find.setItem("GOLD_INGOT"); + find.setConservation(64); find.setBuriedConservation(64); + find.setGivenName("Faith charm"); + find.setLabCleaned(true); find.setFieldSketch(true); find.setStudied(true); + UUID originalRecoverer = find.getRecoveredBy(); + java.time.Instant originalRecovery = find.getRecoveredAt(); + staff.getInventory().clear(); + PlayerMock recipient = player("Recipient"); + var tags = new net.tfminecraft.archaeo.item.RecoveredFindItem(plugin); + + assertContains(run("find restore #12 1 Recipient"), "Restore this same piece"); + assertTrue(recipient.getInventory().isEmpty()); + assertContains(run("find restore #12 " + find.getId() + " Recipient confirm"), "Restored"); + + var restored = recipient.getInventory().getItem(0); + assertEquals(Material.GOLD_INGOT, restored.getType()); + assertEquals("Faith charm", org.bukkit.ChatColor.stripColor(restored.getItemMeta().getDisplayName())); + assertEquals(find.getId(), tags.findIdOf(restored)); + assertEquals(site.getId(), tags.siteIdOf(restored)); + var pdc = restored.getItemMeta().getPersistentDataContainer(); + assertEquals(Integer.valueOf(64), pdc.get(new org.bukkit.NamespacedKey(plugin, "conservation"), org.bukkit.persistence.PersistentDataType.INTEGER)); + assertEquals(1, site.getFinds().size()); + assertEquals(1, site.getRecoveredCount()); + assertEquals(1, find.getFindNumber()); + assertEquals(originalRecoverer, find.getRecoveredBy()); + assertEquals(originalRecovery, find.getRecoveredAt()); + assertTrue(find.isLabCleaned()); assertTrue(find.hasFieldSketch()); assertTrue(find.isStudied()); + assertContains(run("find restore #12 1 Recipient confirm"), "already holds"); + assertEquals(1, recipient.getInventory().all(Material.GOLD_INGOT).size()); + } + + @Test public void restorationRefusesKnownEnderChestCopiesFullInventoriesAndInvalidRecords() { + Site site = dossier(12, "Museum dig"); + run("give artifact coin #12"); + var old = staff.getInventory().getItem(0).clone(); + staff.getInventory().clear(); + staff.getEnderChest().setItem(0, old); + PlayerMock recipient = player("Recipient"); + assertContains(run("find restore #12 1 Recipient confirm"), "already holds"); + staff.getEnderChest().clear(); + for (int slot = 0; slot < 36; slot++) recipient.getInventory().setItem(slot, new org.bukkit.inventory.ItemStack(Material.STONE, 64)); + assertContains(run("find restore #12 1 Recipient confirm"), "empty inventory slot"); + assertContains(run("find restore #12 1 Missing confirm"), "Player not online"); + assertContains(run("find restore #12 1 Recipient invalid"), "Usage:"); + site.setOwnershipHeld(true); + assertContains(run("find restore #12 1 Recipient confirm"), "held for ownership review"); + site.setOwnershipHeld(false); + site.getFinds().getFirst().setState(FindState.LOST); + assertContains(run("find restore #12 1 Recipient confirm"), "already recovered"); + assertEquals(1, site.getFinds().size()); + assertEquals(List.of("restore"), complete("find", "res")); + assertEquals(List.of("Recipient"), complete("find", "restore", "#12", "1", "Rec")); + assertEquals(List.of("confirm"), complete("find", "restore", "#12", "1", "Recipient", "con")); + staff.setOp(false); + staff.addAttachment(plugin, plugin.catalogs().staffPermission(), false); + assertContains(run("find restore #12 1 Recipient confirm"), "You do not have permission"); + } + @Test public void reportsStoredAndCurrentItemsWithoutChangingLegacyFinds() throws Exception { Site site = dossier(12, "Museum dig"); run("give artifact coin #12");