/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:
MineTec committed 2026-11-05 18:57:11 +01:00
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;
}
+8 -4
View File
@@ -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