/pixelpic commands: help for everyone (how camera, film and photos work), admin info (versions, pack and dump state, coverage, queue, photo stats, storage), photo [player] with phase timings, give <player> camera|film [amount], reload (config, render limits live, pack and models), tab completion by permission, clear german errors for wrong arguments, command registered lowercase (/pixelPic never matched for players) with alias pixelpics
This commit is contained in:
1 parent
492bcb3a39
commit
57a3a06cdf
4 files changed
+439
-51
No files matched your search
@@ -24,6 +24,8 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.configuration.Configuration;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
@@ -69,7 +71,10 @@ public final class Main extends JavaPlugin {
|
||||
this, settings.threads(), settings.maxConcurrent(), settings.queueSize(), settings.timeoutSeconds());
|
||||
getLogger().info("Render pool: " + settings + ".");
|
||||
|
||||
Objects.requireNonNull(getCommand("pixelPic")).setExecutor(new PixelPicsCommand());
|
||||
PluginCommand command = Objects.requireNonNull(getCommand("pixelpic"));
|
||||
PixelPicsCommand executor = new PixelPicsCommand();
|
||||
command.setExecutor(executor);
|
||||
command.setTabCompleter(executor);
|
||||
for (Listener listener : List.of(
|
||||
new OnMapInitialize(),
|
||||
new CameraListener(),
|
||||
@@ -132,6 +137,23 @@ public final class Main extends JavaPlugin {
|
||||
return new RenderSettings(threads, maxConcurrent, queueSize, timeoutSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code /pixelpic reload}: re-reads config.yml (debug logging; render limits applied to the running pool,
|
||||
* queued photos are kept) and sets up the renderer again from the resource pack folder and the models,
|
||||
* downloading or updating the pack as configured. False when a setup run is still going on.
|
||||
*/
|
||||
public boolean reload(CommandSender requester) {
|
||||
if (setup.running()) return false;
|
||||
reloadConfig();
|
||||
addNewConfigOptions();
|
||||
DebugLog.configure(getLogger(), getConfig().getBoolean("debug", false));
|
||||
RenderSettings settings = renderSettings();
|
||||
renderManager.configure(
|
||||
settings.threads(), settings.maxConcurrent(), settings.queueSize(), settings.timeoutSeconds());
|
||||
getLogger().info("Reloaded config.yml: render pool " + settings + ", debug " + DebugLog.enabled() + ".");
|
||||
return setup.start(requester);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
SurvivalRecipes.unregister();
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
package eu.mhsl.minecraft.pixelpics.commands;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.Main;
|
||||
import eu.mhsl.minecraft.pixelpics.render.RenderManager;
|
||||
import eu.mhsl.minecraft.pixelpics.render.RenderStats;
|
||||
import eu.mhsl.minecraft.pixelpics.setup.SetupManager;
|
||||
import eu.mhsl.minecraft.pixelpics.survival.CameraItems;
|
||||
import eu.mhsl.minecraft.pixelpics.survival.PhotoService;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.DebugLog;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||
import eu.mhsl.minecraft.pixelpics.utils.MapUsageScan;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class PixelPicsCommand implements CommandExecutor {
|
||||
/**
|
||||
* {@code /pixelpic}: {@code help} (everyone with {@code pixelpic.use}: how camera, film and photos work) and the admin
|
||||
* subcommands ({@code pixelpic.admin}): {@code info}, {@code photo}, {@code give}, {@code reload}, {@code cleanup}.
|
||||
* Messages are German like the rest of the player-facing texts.
|
||||
*/
|
||||
public class PixelPicsCommand implements TabExecutor {
|
||||
|
||||
private static final String ADMIN = "pixelpic.admin";
|
||||
private static final List<String> ADMIN_SUBCOMMANDS = List.of("info", "photo", "give", "reload", "cleanup");
|
||||
private static final int DEFAULT_CLEANUP_DAYS = 30;
|
||||
private static final int MAX_GIVE = 64;
|
||||
|
||||
private static boolean cleanupRunning = false;
|
||||
|
||||
@@ -31,53 +51,374 @@ public class PixelPicsCommand implements CommandExecutor {
|
||||
@NotNull Command command,
|
||||
@NotNull String label,
|
||||
@NotNull String @NotNull [] args) {
|
||||
if (args.length >= 1 && args[0].equalsIgnoreCase("cleanup")) {
|
||||
return cleanup(sender, args);
|
||||
}
|
||||
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(
|
||||
Component.text("Dieser Command kann nur von einem Spieler ausgeführt werden!", NamedTextColor.RED));
|
||||
String sub = args.length == 0 ? "help" : args[0].toLowerCase(Locale.ROOT);
|
||||
if (sub.equals("help") || sub.equals("?")) {
|
||||
help(sender, label);
|
||||
return true;
|
||||
}
|
||||
if (args.length > 0) return false;
|
||||
|
||||
// Debug render without camera or film.
|
||||
if (!player.hasPermission("pixelpic.admin")) {
|
||||
player.sendActionBar(
|
||||
Component.text("Dafür fehlt dir die Berechtigung — nutze eine Kamera.", NamedTextColor.RED));
|
||||
if (!ADMIN_SUBCOMMANDS.contains(sub)) {
|
||||
error(sender, "Unbekannter Unterbefehl „" + args[0] + "“. Übersicht: /" + label + " help");
|
||||
return true;
|
||||
}
|
||||
|
||||
PhotoService.takePhoto(player);
|
||||
if (!sender.hasPermission(ADMIN)) {
|
||||
error(sender, "Dafür fehlt dir die Berechtigung (" + ADMIN + ").");
|
||||
return true;
|
||||
}
|
||||
switch (sub) {
|
||||
case "info" -> info(sender);
|
||||
case "photo" -> photo(sender, label, args);
|
||||
case "give" -> give(sender, label, args);
|
||||
case "reload" -> reload(sender);
|
||||
default -> cleanup(sender, label, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code /pixelPic cleanup [days]} dry-run; {@code /pixelPic cleanup confirm [days]} deletes photos that no saved
|
||||
* or loaded item references (see {@link MapUsageScan}) and that nobody has seen for {@code days} days.
|
||||
*/
|
||||
private boolean cleanup(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("pixelpic.admin")) {
|
||||
sender.sendMessage(
|
||||
Component.text("Dafür fehlt dir die Berechtigung (pixelpic.admin).", NamedTextColor.RED));
|
||||
return true;
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
@NotNull CommandSender sender,
|
||||
@NotNull Command command,
|
||||
@NotNull String label,
|
||||
@NotNull String @NotNull [] args) {
|
||||
boolean admin = sender.hasPermission(ADMIN);
|
||||
List<String> options = new ArrayList<>();
|
||||
if (args.length == 1) {
|
||||
options.add("help");
|
||||
if (admin) options.addAll(ADMIN_SUBCOMMANDS);
|
||||
} else if (admin) {
|
||||
String sub = args[0].toLowerCase(Locale.ROOT);
|
||||
int i = args.length - 1;
|
||||
switch (sub) {
|
||||
case "photo" -> {
|
||||
if (i == 1) options.addAll(playerNames());
|
||||
}
|
||||
case "give" -> {
|
||||
if (i == 1) {
|
||||
options.addAll(playerNames());
|
||||
options.add("@a");
|
||||
} else if (i == 2) {
|
||||
options.addAll(List.of("camera", "film"));
|
||||
} else if (i == 3) {
|
||||
options.addAll(List.of("1", "8", "16", "64"));
|
||||
}
|
||||
}
|
||||
case "cleanup" -> {
|
||||
if (i <= 2) {
|
||||
if (Stream.of(args).noneMatch("confirm"::equalsIgnoreCase)) options.add("confirm");
|
||||
options.add(String.valueOf(DEFAULT_CLEANUP_DAYS));
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
if (cleanupRunning) {
|
||||
sender.sendMessage(Component.text("Es läuft bereits ein Aufräum-Scan.", NamedTextColor.YELLOW));
|
||||
return true;
|
||||
String prefix = args[args.length - 1].toLowerCase(Locale.ROOT);
|
||||
return options.stream()
|
||||
.filter(o -> o.toLowerCase(Locale.ROOT).startsWith(prefix))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<String> playerNames() {
|
||||
return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList();
|
||||
}
|
||||
|
||||
private static void help(CommandSender sender, String label) {
|
||||
Main plugin = Main.getInstance();
|
||||
sender.sendMessage(Component.text(
|
||||
"PixelPics " + plugin.getPluginMeta().getVersion() + " — Sofortbildkamera", NamedTextColor.GOLD));
|
||||
for (String line : List.of(
|
||||
"Kamera craften: Eisen, Diamant, Glasscheiben, Enderauge und Redstone (Rezeptbuch: „Kamera“).",
|
||||
"Filmrolle craften: 4 Papier und 1 Tintenbeutel (Rezeptbuch: „Filmrolle“).",
|
||||
"Film laden: Kamera und Filmrolle zusammen ins Crafting-Feld (bis zu " + CameraItems.MAX_FILM
|
||||
+ " Filme).",
|
||||
"Foto: Rechtsklick mit der Kamera (1 Film). Das Bild entwickelt sich nach ein paar Sekunden.",
|
||||
"Kopie: Foto und Filmrolle zusammen ins Crafting-Feld.")) {
|
||||
sender.sendMessage(Component.text("• " + line, NamedTextColor.GRAY));
|
||||
}
|
||||
if (!sender.hasPermission(ADMIN)) return;
|
||||
sender.sendMessage(Component.text("Admin-Befehle:", NamedTextColor.GOLD));
|
||||
adminLine(sender, label, "info", "", "Status, Versionen, Resource Pack, Warteschlange, Statistik");
|
||||
adminLine(sender, label, "photo", " [Spieler]", "Testfoto ohne Kamera und Film, mit Zeitmessung");
|
||||
adminLine(sender, label, "give", " <Spieler> <camera|film> [Anzahl]", "Kamera oder Filmrollen geben");
|
||||
adminLine(sender, label, "reload", "", "config.yml, Resource Pack und Modelle neu laden");
|
||||
adminLine(
|
||||
sender,
|
||||
label,
|
||||
"cleanup",
|
||||
" [confirm] [Tage]",
|
||||
"ungenutzte Fotos löschen (ohne confirm nur Vorschau, Standard " + DEFAULT_CLEANUP_DAYS + " Tage)");
|
||||
}
|
||||
|
||||
private static void adminLine(CommandSender sender, String label, String sub, String params, String text) {
|
||||
String command = "/" + label + " " + sub;
|
||||
sender.sendMessage(Component.text(command + params, NamedTextColor.YELLOW)
|
||||
.clickEvent(ClickEvent.suggestCommand(command + " "))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("Klicken zum Einfügen")))
|
||||
.append(Component.text(" – " + text, NamedTextColor.GRAY)));
|
||||
}
|
||||
|
||||
// ---- info ----
|
||||
|
||||
private static void info(CommandSender sender) {
|
||||
Main plugin = Main.getInstance();
|
||||
SetupManager setup = plugin.getSetup();
|
||||
SetupManager.Report report = setup.report();
|
||||
String server = Bukkit.getMinecraftVersion();
|
||||
sender.sendMessage(Component.text(
|
||||
"PixelPics " + plugin.getPluginMeta().getVersion() + " auf " + Bukkit.getName() + " "
|
||||
+ Bukkit.getVersion(),
|
||||
NamedTextColor.GOLD));
|
||||
|
||||
String task = setup.task();
|
||||
if (plugin.getScreenRenderer() == null) {
|
||||
String status = task != null
|
||||
? "wird eingerichtet: " + task
|
||||
: setup.running() ? "wird eingerichtet" : "nicht bereit, Fotos sind deaktiviert";
|
||||
line(sender, "Status", status, NamedTextColor.RED);
|
||||
} else {
|
||||
line(sender, "Status", "bereit" + (task != null ? " (im Hintergrund: " + task + ")" : ""), null);
|
||||
}
|
||||
if (setup.problem() != null) line(sender, "Problem", setup.problem(), NamedTextColor.RED);
|
||||
|
||||
if (report != null) {
|
||||
List<String> packs = new ArrayList<>();
|
||||
for (SetupManager.PackInfo p : report.packs()) {
|
||||
String v = p.version() == null ? "Version unbekannt" : p.version();
|
||||
packs.add(p.name() + " (" + v + (p.loaded() ? "" : ", nicht geladen") + ")");
|
||||
}
|
||||
line(
|
||||
sender,
|
||||
"Resource Pack",
|
||||
packs.isEmpty() ? "Ordner-Pack" : String.join(", ", packs),
|
||||
report.packs().stream()
|
||||
.anyMatch(p -> p.loaded()
|
||||
&& p.version() != null
|
||||
&& !p.version().equals(server))
|
||||
? NamedTextColor.RED
|
||||
: null);
|
||||
coverageLine(sender, "Blockzustände", report.blocks());
|
||||
coverageLine(sender, "Item-Modelle", report.items());
|
||||
if (!report.fontOk()) line(sender, "Schrift", "fehlt im Pack — Schilder bleiben leer", NamedTextColor.RED);
|
||||
String dump = report.dumpVersion() == null
|
||||
? "kein Dump, nur CEM-Vorlagen"
|
||||
: "Dump " + report.dumpVersion() + " (" + report.dumpSource() + "), " + report.entityModels()
|
||||
+ " Modelle"
|
||||
+ (report.datapackVariants() > 0
|
||||
? ", " + report.datapackVariants() + " Datapack-Varianten"
|
||||
: "");
|
||||
boolean dumpOk = server.equals(report.dumpVersion());
|
||||
line(
|
||||
sender,
|
||||
"Entity-Modelle",
|
||||
dump + (dumpOk ? "" : " — passt nicht zu " + server + ", siehe Server-Log"),
|
||||
dumpOk ? null : NamedTextColor.RED);
|
||||
}
|
||||
|
||||
RenderManager manager = plugin.getRenderManager();
|
||||
RenderManager.Load load = manager.load();
|
||||
line(
|
||||
sender,
|
||||
"Rendering",
|
||||
load.running() + " laufen, " + load.queued() + " warten (max. " + load.maxConcurrent()
|
||||
+ " parallel + " + load.queueSize() + " in der Warteschlange, " + load.threads()
|
||||
+ " Kerne, Timeout " + load.timeoutSeconds() + " s)",
|
||||
null);
|
||||
RenderStats stats = manager.stats();
|
||||
String photos = stats.developedCount() + " entwickelt, " + stats.failedCount() + " fehlgeschlagen, "
|
||||
+ stats.rejectedCount() + " abgelehnt";
|
||||
if (stats.developedCount() > 0) {
|
||||
RenderStats.Timings avg = stats.average();
|
||||
photos += "; Ø " + seconds(avg.total()) + " (Warten " + avg.queued() + " ms, Snapshot " + avg.snapshot()
|
||||
+ " ms, Raytracing " + seconds(avg.trace()) + ", Dithern " + avg.dither() + " ms), max. "
|
||||
+ seconds(stats.maxTotal()) + ", Snapshot max. " + stats.maxSnapshot() + " ms";
|
||||
}
|
||||
line(sender, "Fotos seit Start", photos, null);
|
||||
line(sender, "Debug-Logging", DebugLog.enabled() ? "an" : "aus (config.yml: debug)", null);
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
MapManager.Storage storage = MapManager.storage();
|
||||
String text = storage.photos() + (storage.photos() == 1 ? " Foto, " : " Fotos, ")
|
||||
+ String.format(Locale.GERMANY, "%.1f MB", storage.bytes() / 1048576.0) + " in "
|
||||
+ storage.folder();
|
||||
try {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> line(sender, "Gespeichert", text, null));
|
||||
} catch (IllegalPluginAccessException ignored) {
|
||||
// disabled meanwhile
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void coverageLine(CommandSender sender, String name, SetupManager.Coverage c) {
|
||||
String text = (c.total() - c.missing()) + " / " + c.total() + " vorhanden";
|
||||
if (c.missing() > 0) {
|
||||
text += ", fehlen z. B.: " + String.join(", ", c.sample()) + " — Pack älter als der Server?";
|
||||
}
|
||||
line(sender, " " + name, text, c.missing() > 0 ? NamedTextColor.RED : null);
|
||||
}
|
||||
|
||||
private static void line(CommandSender sender, String key, String value, NamedTextColor valueColor) {
|
||||
sender.sendMessage(Component.text(key + ": ", NamedTextColor.GRAY)
|
||||
.append(Component.text(value, valueColor != null ? valueColor : NamedTextColor.WHITE)));
|
||||
}
|
||||
|
||||
private static String seconds(long ms) {
|
||||
return String.format(Locale.GERMANY, "%.2f s", ms / 1000.0);
|
||||
}
|
||||
|
||||
// ---- photo ----
|
||||
|
||||
private static void photo(CommandSender sender, String label, String[] args) {
|
||||
Player target;
|
||||
if (args.length >= 2) {
|
||||
List<Player> found = players(sender, args[1]);
|
||||
if (found == null) return;
|
||||
if (found.size() != 1) {
|
||||
error(sender, "Bitte genau einen Spieler angeben.");
|
||||
return;
|
||||
}
|
||||
target = found.getFirst();
|
||||
} else if (sender instanceof Player player) {
|
||||
target = player;
|
||||
} else {
|
||||
error(sender, "Von der Konsole aus: /" + label + " photo <Spieler> (fotografiert aus dessen Sicht).");
|
||||
return;
|
||||
}
|
||||
String who = target.equals(sender) ? "" : " aus der Sicht von " + target.getName();
|
||||
Component refusal = PhotoService.takeDebugPhoto(target, timings -> {
|
||||
if (timings == null) {
|
||||
error(sender, "Testfoto" + who + " fehlgeschlagen (Details im Server-Log).");
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(Component.text(
|
||||
"Testfoto" + who + " entwickelt in " + seconds(timings.total()) + ": Warten " + timings.queued()
|
||||
+ " ms, Snapshot " + timings.snapshot() + " ms (Main-Thread), Raytracing "
|
||||
+ seconds(timings.trace()) + ", Dithern " + timings.dither() + " ms.",
|
||||
NamedTextColor.GREEN));
|
||||
});
|
||||
if (refusal != null) {
|
||||
sender.sendMessage(refusal);
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(Component.text(
|
||||
"Testfoto" + who + " ausgelöst (ohne Film), die Karte landet im Inventar von " + target.getName() + ".",
|
||||
NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
// ---- give ----
|
||||
|
||||
private static void give(CommandSender sender, String label, String[] args) {
|
||||
if (args.length < 3) {
|
||||
error(sender, "Verwendung: /" + label + " give <Spieler> <camera|film> [Anzahl]");
|
||||
return;
|
||||
}
|
||||
List<Player> targets = players(sender, args[1]);
|
||||
if (targets == null) return;
|
||||
String kind = args[2].toLowerCase(Locale.ROOT);
|
||||
boolean camera = kind.equals("camera") || kind.equals("kamera");
|
||||
if (!camera && !kind.equals("film")) {
|
||||
error(sender, "Unbekanntes Item „" + args[2] + "“ — möglich sind camera und film.");
|
||||
return;
|
||||
}
|
||||
int amount = 1;
|
||||
if (args.length >= 4) {
|
||||
try {
|
||||
amount = Integer.parseInt(args[3]);
|
||||
} catch (NumberFormatException e) {
|
||||
amount = -1;
|
||||
}
|
||||
if (amount < 1 || amount > MAX_GIVE) {
|
||||
error(sender, "Die Anzahl muss eine Zahl von 1 bis " + MAX_GIVE + " sein.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (Player target : targets) {
|
||||
if (camera) {
|
||||
for (int i = 0; i < amount; i++) CameraItems.give(target, CameraItems.createCamera(0));
|
||||
} else {
|
||||
ItemStack film = CameraItems.createFilm();
|
||||
film.setAmount(amount);
|
||||
CameraItems.give(target, film);
|
||||
}
|
||||
}
|
||||
String item = camera
|
||||
? (amount == 1 ? "Kamera (leer)" : "Kameras (leer)")
|
||||
: (amount == 1 ? "Filmrolle" : "Filmrollen");
|
||||
String who = targets.size() == 1 ? targets.getFirst().getName() : targets.size() + " Spieler";
|
||||
sender.sendMessage(Component.text(amount + " " + item + " an " + who + " gegeben.", NamedTextColor.GREEN));
|
||||
}
|
||||
|
||||
/** Online players for a name or selector, or null after telling the sender why there are none. */
|
||||
private static List<Player> players(CommandSender sender, String arg) {
|
||||
if (arg.startsWith("@")) {
|
||||
List<Player> found = new ArrayList<>();
|
||||
try {
|
||||
for (Entity e : Bukkit.selectEntities(sender, arg)) {
|
||||
if (e instanceof Player p) found.add(p);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
error(sender, "Ungültiger Selektor „" + arg + "“.");
|
||||
return null;
|
||||
}
|
||||
if (found.isEmpty()) {
|
||||
error(sender, "Der Selektor „" + arg + "“ trifft keinen Spieler.");
|
||||
return null;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
Player player = Bukkit.getPlayerExact(arg);
|
||||
if (player == null) {
|
||||
error(sender, "Spieler „" + arg + "“ ist nicht online.");
|
||||
return null;
|
||||
}
|
||||
return List.of(player);
|
||||
}
|
||||
|
||||
// ---- reload ----
|
||||
|
||||
private static void reload(CommandSender sender) {
|
||||
Main plugin = Main.getInstance();
|
||||
if (!plugin.reload(sender)) {
|
||||
String task = plugin.getSetup().task();
|
||||
error(
|
||||
sender,
|
||||
"PixelPics wird gerade schon eingerichtet" + (task != null ? " (" + task + ")" : "")
|
||||
+ " — bitte kurz warten.");
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(Component.text(
|
||||
"config.yml neu geladen; Resource Pack und Modelle werden im Hintergrund neu eingelesen …",
|
||||
NamedTextColor.GRAY));
|
||||
}
|
||||
|
||||
// ---- cleanup ----
|
||||
|
||||
/**
|
||||
* {@code /pixelpic cleanup [days]} dry-run; {@code /pixelpic cleanup confirm [days]} deletes photos that no saved
|
||||
* or loaded item references (see {@link MapUsageScan}) and that nobody has seen for {@code days} days.
|
||||
*/
|
||||
private static void cleanup(CommandSender sender, String label, String[] args) {
|
||||
boolean confirm = false;
|
||||
int days = DEFAULT_CLEANUP_DAYS;
|
||||
for (int i = 1; i < args.length; i++) {
|
||||
if (args[i].equalsIgnoreCase("confirm")) confirm = true;
|
||||
else {
|
||||
try {
|
||||
days = Math.max(0, Integer.parseInt(args[i]));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
if (args[i].equalsIgnoreCase("confirm")) {
|
||||
confirm = true;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
days = Integer.parseInt(args[i]);
|
||||
} catch (NumberFormatException e) {
|
||||
days = -1;
|
||||
}
|
||||
if (days < 0) {
|
||||
error(
|
||||
sender,
|
||||
"„" + args[i] + "“ ist keine gültige Anzahl Tage. Verwendung: /" + label
|
||||
+ " cleanup [confirm] [Tage]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cleanupRunning) {
|
||||
sender.sendMessage(Component.text("Es läuft bereits ein Aufräum-Scan.", NamedTextColor.YELLOW));
|
||||
return;
|
||||
}
|
||||
|
||||
// Loaded state now, saved world and player data off the main thread, loaded state again before deciding.
|
||||
@@ -102,24 +443,22 @@ public class PixelPicsCommand implements CommandExecutor {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
cleanupRunning = false;
|
||||
if (finalOnDisk == null) {
|
||||
sender.sendMessage(Component.text(
|
||||
"Konnte nicht alle Weltdaten lesen, es wurde nichts gelöscht: " + finalError,
|
||||
NamedTextColor.RED));
|
||||
error(sender, "Konnte nicht alle Weltdaten lesen, es wurde nichts gelöscht: " + finalError);
|
||||
return;
|
||||
}
|
||||
Set<Integer> inUse = new HashSet<>(liveBefore);
|
||||
inUse.addAll(finalOnDisk);
|
||||
inUse.addAll(MapUsageScan.live());
|
||||
finishCleanup(sender, inUse, finalConfirm, finalDays);
|
||||
finishCleanup(sender, label, inUse, finalConfirm, finalDays);
|
||||
});
|
||||
} catch (IllegalPluginAccessException e) {
|
||||
cleanupRunning = false; // disabled meanwhile
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void finishCleanup(CommandSender sender, Set<Integer> inUse, boolean confirm, int days) {
|
||||
private static void finishCleanup(
|
||||
CommandSender sender, String label, Set<Integer> inUse, boolean confirm, int days) {
|
||||
long cutoff = System.currentTimeMillis() - days * 86_400_000L;
|
||||
List<MapManager.StoredMap> candidates = MapManager.listStored().stream()
|
||||
.filter(s -> !inUse.contains(s.id()) && s.lastModified() < cutoff)
|
||||
@@ -127,7 +466,7 @@ public class PixelPicsCommand implements CommandExecutor {
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
sender.sendMessage(Component.text(
|
||||
"Keine aufräumbaren Aufnahmen gefunden (nirgends mehr vorhanden und seit " + days
|
||||
"Keine aufräumbaren Fotos gefunden (nirgends mehr vorhanden und seit " + days
|
||||
+ " Tagen nicht gesehen).",
|
||||
NamedTextColor.YELLOW));
|
||||
return;
|
||||
@@ -135,14 +474,14 @@ public class PixelPicsCommand implements CommandExecutor {
|
||||
|
||||
if (!confirm) {
|
||||
sender.sendMessage(Component.text(
|
||||
candidates.size() + " Aufnahme(n) könnten gelöscht werden: in keiner geladenen oder gespeicherten"
|
||||
candidates.size() + " Foto(s) könnten gelöscht werden: in keiner geladenen oder gespeicherten"
|
||||
+ " Welt, keinem Spielerinventar/Enderchest (auch offline) und keinem Container/Shulker/"
|
||||
+ "Bündel gefunden und seit " + days + " Tagen nicht geladen oder angesehen.",
|
||||
NamedTextColor.YELLOW));
|
||||
sender.sendMessage(Component.text(
|
||||
"Achtung: Nicht erkannt werden Karten in gerade nicht geladenen Welten und in Speichern anderer"
|
||||
+ " Plugins (z. B. Rucksäcke, Auktionshäuser). Zum Löschen: /pixelPic cleanup confirm "
|
||||
+ days,
|
||||
+ " Plugins (z. B. Rucksäcke, Auktionshäuser). Zum Löschen: /" + label
|
||||
+ " cleanup confirm " + days,
|
||||
NamedTextColor.GRAY));
|
||||
return;
|
||||
}
|
||||
@@ -151,6 +490,10 @@ public class PixelPicsCommand implements CommandExecutor {
|
||||
for (MapManager.StoredMap s : candidates) {
|
||||
if (MapManager.delete(s.id())) deleted++;
|
||||
}
|
||||
sender.sendMessage(Component.text(deleted + " Aufnahme(n) gelöscht.", NamedTextColor.GREEN));
|
||||
sender.sendMessage(Component.text(deleted + " Foto(s) gelöscht.", NamedTextColor.GREEN));
|
||||
}
|
||||
|
||||
private static void error(CommandSender sender, String message) {
|
||||
sender.sendMessage(Component.text(message, NamedTextColor.RED));
|
||||
}
|
||||
}
|
||||
@@ -292,6 +292,25 @@ public class MapManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Number of stored photos and the bytes of their files (thread-safe: only reads the folder). */
|
||||
public record Storage(int photos, long bytes, String folder) {}
|
||||
|
||||
public static Storage storage() {
|
||||
File folder = imageFolder();
|
||||
File[] files = folder.listFiles();
|
||||
Set<Integer> ids = new HashSet<>();
|
||||
long bytes = 0;
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
Matcher m = FILE_ID.matcher(f.getName());
|
||||
if (!m.matches()) continue;
|
||||
ids.add(Integer.parseInt(m.group(1)));
|
||||
bytes += f.length();
|
||||
}
|
||||
}
|
||||
return new Storage(ids.size(), bytes, folder.getPath());
|
||||
}
|
||||
|
||||
private static long lastModified(File f) {
|
||||
return f.isFile() ? f.lastModified() : 0L;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,17 @@ version: '${version}'
|
||||
main: eu.mhsl.minecraft.pixelpics.Main
|
||||
api-version: '1.21'
|
||||
commands:
|
||||
pixelPic:
|
||||
pixelpic:
|
||||
description: "PixelPics: how the camera works, admin tools"
|
||||
aliases: [pixelpics]
|
||||
permission: "pixelpic.use"
|
||||
usage: "/pixelPic [cleanup [confirm] [days]]"
|
||||
usage: "/pixelpic help"
|
||||
permissions:
|
||||
pixelpic.use:
|
||||
description: "Allows running /pixelPic (the render branch itself requires pixelpic.admin)"
|
||||
description: "Allows /pixelpic help (how camera, film and photos work)"
|
||||
default: true
|
||||
pixelpic.admin:
|
||||
description: "Allows the /pixelPic debug render (no camera/film) and cleanup management"
|
||||
description: "Allows the admin subcommands of /pixelpic: info, photo, give, reload, cleanup"
|
||||
default: op
|
||||
children:
|
||||
pixelpic.use: true
|
||||
Reference in new issue
Block a user