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
2 changes: 2 additions & 0 deletions src/main/java/net/tfminecraft/recycler/Recycler.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public void onEnable() {
}
getServer().getPluginManager().registerEvents(recyclerManager, this);
escrowManager.loadPending();
commandManager.trackPendingOwners();
getServer().getPluginManager().registerEvents(commandManager, this);
if (escrowManager.identity() instanceof net.tfminecraft.recycler.ownership.CharacterQueueBridge)
net.tfminecraft.recycler.ownership.CharacterQueueBridge.start(this);
getServer().getScheduler().runTaskTimer(this, () -> {
Expand Down
46 changes: 43 additions & 3 deletions src/main/java/net/tfminecraft/recycler/command/CommandManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.TreeSet;

import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
Expand All @@ -13,16 +17,51 @@
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;

import net.tfminecraft.recycler.Messages;
import net.tfminecraft.recycler.Recycler;
import net.tfminecraft.recycler.manager.EscrowManager;

public final class CommandManager implements CommandExecutor, TabCompleter {
public final class CommandManager implements CommandExecutor, TabCompleter, Listener {

private static final List<String> ROOT_SUBCOMMANDS = List.of("reload", "escrow");
private static final List<String> ESCROW_SUBCOMMANDS = List.of("list", "return");

private Map<UUID, String> pendingOwnerNames = new HashMap<>();
private Set<UUID> pendingOwners = Set.of();

public void trackPendingOwners() {
Recycler.plugin.getEscrowManager().store().onPendingOwnersChanged(this::refreshPendingOwnerNames);
}

private void refreshPendingOwnerNames(List<UUID> owners) {
// Keep joined names while their owners remain pending; resolve only uncached profiles.
Map<UUID, String> names = new HashMap<>();
for (UUID owner : owners) {
String cached = pendingOwnerNames.get(owner);
if (cached != null) { names.put(owner, cached); continue; }
try {
String name = Bukkit.getOfflinePlayer(owner).getName();
if (name != null) names.put(owner, name);
} catch (RuntimeException ex) {
Recycler.plugin.getLogger().warning("Recycler pending owner name unavailable: " + owner + " " + ex.getMessage());
}
}
pendingOwners = Set.copyOf(owners);
pendingOwnerNames = names;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (pendingOwners.contains(player.getUniqueId())) {
pendingOwnerNames.put(player.getUniqueId(), player.getName());
}
}

@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("recycler.admin")) {
Expand Down Expand Up @@ -104,11 +143,12 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
return filterPrefix(ESCROW_SUBCOMMANDS, args[1]);
}
if (args.length == 3 && "escrow".equalsIgnoreCase(args[0]) && "return".equalsIgnoreCase(args[1])) {
List<String> names = new ArrayList<>();
TreeSet<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
names.addAll(pendingOwnerNames.values());
for (Player player : Bukkit.getOnlinePlayers()) {
names.add(player.getName());
}
return filterPrefix(names, args[2]);
return filterPrefix(new ArrayList<>(names), args[2]);
}
return Collections.emptyList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,7 @@ public boolean hasPending(UUID account) {
public List<UUID> listMemoryEscrowPlayerIds() { return List.of(); }
public int countMemoryEscrow() { return 0; }
public List<UUID> listPendingPlayerIds() {
List<UUID> accounts = new ArrayList<>();
run(() -> {
for (Path path : store().allFiles()) {
try { JsonObject row = ReturnStore.read(path); if (!ReturnStore.terminal(row)) accounts.add(ReturnStore.owner(row).account()); }
catch (IOException | RuntimeException ex) { warning("Held queue: " + path); }
}
});
return accounts.stream().distinct().toList();
return store().pendingPlayerIds();
}
public int countPendingFiles() { return describeQueues().size(); }
private interface Work { void run() throws IOException; }
Expand Down
40 changes: 35 additions & 5 deletions src/main/java/net/tfminecraft/recycler/ownership/ReturnStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
Expand All @@ -20,6 +22,8 @@ public static final class Conflict extends IOException {
}
private static final List<String> STATES = List.of("depositing", "escrow", "pending", "delivering", "held", "delivered", "consumed", "cancelled");
private final Path root;
private final Map<Path, UUID> pendingOwners = new java.util.LinkedHashMap<>();
private Consumer<List<UUID>> pendingOwnersChanged = accounts -> {};
private final java.util.Map<Path, java.util.Map.Entry<String, java.util.Map<UUID, List<QueueIdentity.Owner>>>> validated = new java.util.HashMap<>();
public ReturnStore(Path root) { this.root = root.toAbsolutePath().normalize(); }
public Path root() { return root; }
Expand All @@ -32,14 +36,35 @@ public static QueueIdentity.Owner owner(JsonObject row) {
}
public static String text(JsonObject row, String key) { return row.get(key).getAsString(); }

/** Recovery seeds this index; successful queue publications keep it current without discovery. */
public List<UUID> pendingPlayerIds() { return pendingOwners.values().stream().distinct().toList(); }
public void onPendingOwnersChanged(Consumer<List<UUID>> listener) {
pendingOwnersChanged = listener;
listener.accept(pendingPlayerIds());
}
private void index(Path path, JsonObject row) {
if (terminal(row)) forget(path);
else {
UUID account = owner(row).account();
if (!account.equals(pendingOwners.put(path, account))) pendingOwnersChanged.accept(pendingPlayerIds());
}
}
private void forget(Path path) {
if (pendingOwners.remove(path) != null) pendingOwnersChanged.accept(pendingPlayerIds());
}
private JsonObject readRow(Path path) throws IOException {
try {
JsonObject row = read(path); validate(path, row); index(path, row); return row;
} catch (Conflict | NoSuchFileException ex) { forget(path); throw ex; }
}

public List<JsonObject> rows(QueueIdentity.Owner owner) throws IOException {
requireDirectory(root.resolve("character-returns"));
Path folder = root.resolve("character-returns").resolve(owner.character().toString());
requireDirectory(folder);
List<JsonObject> rows = new ArrayList<>();
for (Path path : files(folder)) {
JsonObject row = read(path);
validate(path, row);
JsonObject row = readRow(path);
if (!owner.equals(owner(row))) throw new Conflict("Return owner differs from its character folder: " + path);
rows.add(row);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Expand All @@ -60,20 +85,23 @@ public JsonObject create(QueueIdentity.Owner owner, UUID id, JsonObject payload)
row.addProperty("format", 1); row.addProperty("id", id.toString()); setOwner(row, owner);
validate(file(owner, id), row);
QueueFiles.write(file(owner, id), QueueJson.bytes(row), false);
index(file(owner, id), row);
return row;
}
public JsonObject update(JsonObject expected, Consumer<JsonObject> change) throws IOException {
QueueIdentity.Owner owner = owner(expected); requireWritable(owner);
Path file = file(owner, id(expected));
JsonObject current = read(file); validate(file, current);
JsonObject current = readRow(file);
if (!current.equals(expected)) throw new Conflict("Return changed before its effect: " + id(expected));
change.accept(current); validate(file, current);
write(file, current); return current;
write(file, current); index(file, current); return current;
}
public void recover() throws IOException {
pendingOwners.clear();
pendingOwnersChanged.accept(pendingPlayerIds());
for (Path path : allFiles()) {
try {
JsonObject row = read(path); validate(path, row);
JsonObject row = readRow(path);
// No in-memory GUI survived a restart. Ambiguous inventory intents stay untouched.
if ("escrow".equals(text(row, "state"))) update(row, copy -> copy.addProperty("state", "pending"));
} catch (Conflict ex) { /* Retain unloadable documents for staff; other queues remain readable. */ }
Expand Down Expand Up @@ -361,10 +389,12 @@ private void apply(Path file, JsonObject journal, JsonObject plan, BooleanSuppli
if (!guard.getAsBoolean()) throw new IOException("Estate changed before return publication");
QueueFiles.write(target, Base64.getDecoder().decode(text(plan, "after")), false);
}
index(target, readPayload(plan, "after"));
plan.addProperty("phase", "published"); write(file, journal);
if (remove) {
if (!guard.getAsBoolean()) throw new IOException("Estate changed before source removal");
QueueFiles.delete(source);
forget(source);
}
plan.addProperty("phase", "moved"); write(file, journal);
}
Expand Down
Loading
Loading