Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,18 @@ 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 <name|#serial>` 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.
- `/archaeo find info` inspects the piece in the main hand; add `<#serial> <number|uuid>` for an explicit record.
- `/archaeo find delete <#serial> <number|uuid> 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> <number|uuid> <player> 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.

Expand Down
31 changes: 18 additions & 13 deletions src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
*/
Expand Down
29 changes: 14 additions & 15 deletions src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class ArchaeoCommand implements CommandExecutor, TabCompleter {
private static final List<String> GIVE_KINDS = List.of(
"tracker", "prospect", "establish", "tool", "brush", "paper", "pencil", "artifact");
private static final List<String> WORKDAY_ACTIONS = List.of("reset");
private static final List<String> FIND_ACTIONS = List.of("spawn", "list", "info", "delete");
private static final List<String> FIND_ACTIONS = List.of("spawn", "list", "info", "delete", "restore");

private final ArcheologyPlugin plugin;
private final CatalogRegistry catalogs;
Expand Down Expand Up @@ -586,10 +586,10 @@ private Optional<Site> 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) {
Expand Down Expand Up @@ -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}
Expand All @@ -720,21 +721,18 @@ private boolean handleCreate(CommandSender sender, String[] args) {
private boolean handleInfo(CommandSender sender, String[] args) {
Optional<Site> resolved;
if (args.length == 2) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Console must pass a site name or serial: /archaeo ruin info <name|#serial>");
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;
}

Expand Down Expand Up @@ -1285,6 +1283,7 @@ private void sendUsage(CommandSender sender) {
sender.sendMessage(" /archaeo find list <#serial> [page]");
sender.sendMessage(" /archaeo find info [<#serial> <number|uuid>]");
sender.sendMessage(" /archaeo find delete <#serial> <number|uuid> confirm");
sender.sendMessage(" /archaeo find restore <#serial> <number|uuid> <player> confirm");
sender.sendMessage(" /archaeo sketch");
sender.sendMessage(" /archaeo reload");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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> <number|uuid> <player> 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());
Expand Down Expand Up @@ -186,7 +258,7 @@ private BuriedFind find(CommandSender sender, Site site, String token) {

List<String> 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())
Expand All @@ -197,11 +269,16 @@ List<String> 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) {
Expand All @@ -218,5 +295,6 @@ static void usage(CommandSender sender) {
sender.sendMessage(" /archaeo find list <#serial> [page]");
sender.sendMessage(" /archaeo find info [<#serial> <number|uuid>]");
sender.sendMessage(" /archaeo find delete <#serial> <number|uuid> confirm");
sender.sendMessage(" /archaeo find restore <#serial> <number|uuid> <player> confirm");
}
}
13 changes: 11 additions & 2 deletions src/main/java/net/tfminecraft/archaeo/establish/CampListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading