wire up rendering + build config
This commit is contained in:
Generated
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/PixelPic.main.iml" filepath="$PROJECT_DIR$/.idea/modules/PixelPic.main.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/PixelPic.main.iml" filepath="$PROJECT_DIR$/.idea/modules/PixelPic.main.iml" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/PixelPics.PixelPic.main.iml" filepath="$PROJECT_DIR$/.idea/modules/PixelPics.PixelPic.main.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/PixelPics.PixelPic.main.iml" filepath="$PROJECT_DIR$/.idea/modules/PixelPics.PixelPic.main.iml" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/eu.mhsl.minecraft.pixelpic.PixelPic.main.iml" filepath="$PROJECT_DIR$/.idea/modules/eu.mhsl.minecraft.pixelpic.PixelPic.main.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/PixelPics.main.iml" filepath="$PROJECT_DIR$/.idea/modules/PixelPics.main.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
|
||||||
|
|||||||
@@ -1,124 +1,89 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics;
|
package eu.mhsl.minecraft.pixelpics;
|
||||||
|
|
||||||
|
import eu.mhsl.minecraft.pixelpics.assets.AssetReader;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.assets.BlockModelRegistry;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.assets.ResourcePack;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.assets.ResourcePackLoader;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
|
||||||
import eu.mhsl.minecraft.pixelpics.commands.PixelPicsCommand;
|
import eu.mhsl.minecraft.pixelpics.commands.PixelPicsCommand;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.listeners.OnMapInitialize;
|
||||||
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
|
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
|
||||||
import eu.mhsl.minecraft.pixelpics.render.render.Renderer;
|
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.File;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.Optional;
|
||||||
import java.util.jar.JarFile;
|
|
||||||
|
|
||||||
public final class Main extends JavaPlugin {
|
public final class Main extends JavaPlugin {
|
||||||
private static Main instance;
|
|
||||||
private Renderer screenRenderer;
|
|
||||||
|
|
||||||
public final NamespacedKey pictureIdFlag = Objects.requireNonNull(
|
private static Main instance;
|
||||||
NamespacedKey.fromString("imageId".toLowerCase(), this),
|
|
||||||
"Failed to create item-Flag Namespace"
|
private ResourcePack resourcePack;
|
||||||
);
|
private DefaultScreenRenderer screenRenderer;
|
||||||
|
|
||||||
|
public final NamespacedKey pictureIdFlag = new NamespacedKey(this, "imageid");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
instance = this;
|
instance = this;
|
||||||
extractJsonResources();
|
|
||||||
|
|
||||||
Objects.requireNonNull(Bukkit.getPluginCommand("pixelPic"))
|
Bukkit.getPluginManager().registerEvents(new OnMapInitialize(), this);
|
||||||
.setExecutor(new PixelPicsCommand());
|
Objects.requireNonNull(Bukkit.getPluginCommand("pixelPic")).setExecutor(new PixelPicsCommand());
|
||||||
|
|
||||||
Bukkit.getPluginCommand("test").setExecutor((sender, command, label, args) -> {
|
initRenderer();
|
||||||
// Dialog dialog = Dialog.create(
|
|
||||||
// builder -> builder.empty()
|
|
||||||
// .base(
|
|
||||||
// DialogBase.builder(Component.text("Hello World")).build()
|
|
||||||
// )
|
|
||||||
// .type(DialogType.multiAction(
|
|
||||||
// List.of(
|
|
||||||
// ActionButton.builder(Component.text("Option 1")).action(DialogAction.staticAction(ClickEvent.callback(audience -> System.out.println("HIIIII")))).build(),
|
|
||||||
// ActionButton.builder(Component.text("Option 2")).action(DialogAction.customClick(Key.key("test"), null)).build(),
|
|
||||||
// ActionButton.builder(Component.text("Option 3")).action(DialogAction.commandTemplate("say hi")).build()
|
|
||||||
// ),
|
|
||||||
// ActionButton.builder(Component.text("Beenden")).build(),
|
|
||||||
// 3
|
|
||||||
// ))
|
|
||||||
//
|
|
||||||
// );
|
|
||||||
// sender.showDialog(dialog);
|
|
||||||
|
|
||||||
Material.getMaterial("acacia_button");
|
|
||||||
Bukkit.broadcast(Component.text(Material.STONE.getBlockTranslationKey().replace("block.minecraft.", "")));
|
|
||||||
|
|
||||||
if(!(sender instanceof Player))
|
|
||||||
throw new IllegalStateException("Dieser Command kann nur von einem Spieler ausgeführt werden!");
|
|
||||||
|
|
||||||
File blockDir = new File(getDataFolder(), "models/block");
|
|
||||||
for (File file : blockDir.listFiles()) {
|
|
||||||
String blockName = file.getName().substring(0, file.getName().lastIndexOf('.'));
|
|
||||||
Material material = Material.getMaterial(blockName.toUpperCase());
|
|
||||||
System.out.println(material);
|
|
||||||
if(material == null) {
|
|
||||||
System.out.println(blockName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void extractJsonResources() {
|
private void initRenderer() {
|
||||||
String resourcePath = "models/block/";
|
File resourcePackDir = new File(getDataFolder(), "resourcepack");
|
||||||
File outputDir = new File(getDataFolder(), resourcePath);
|
if (!resourcePackDir.exists() && !resourcePackDir.mkdirs()) {
|
||||||
if (outputDir.exists()) return;
|
getLogger().warning("Could not create resource pack directory: " + resourcePackDir);
|
||||||
outputDir.mkdirs();
|
}
|
||||||
|
|
||||||
try {
|
Optional<ResourcePack> pack = ResourcePackLoader.load(resourcePackDir, getLogger());
|
||||||
URL jarUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
|
if (pack.isEmpty()) {
|
||||||
File jarFile = new File(jarUrl.toURI());
|
getLogger().severe("No resource pack found in " + resourcePackDir.getPath()
|
||||||
|
+ " — place a vanilla resource pack (directory with assets/minecraft/... or a .zip) there. "
|
||||||
|
+ "/pixelPic is disabled until a pack is available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (JarFile jar = new JarFile(jarFile)) {
|
this.resourcePack = pack.get();
|
||||||
Enumeration<JarEntry> entries = jar.entries();
|
AssetReader reader = new AssetReader(resourcePack);
|
||||||
|
TextureCache textures = new TextureCache(resourcePack);
|
||||||
|
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
|
||||||
|
BiomeTintProvider tintProvider = new BiomeTintProvider(textures);
|
||||||
|
|
||||||
while (entries.hasMoreElements()) {
|
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader cemLoader =
|
||||||
JarEntry entry = entries.nextElement();
|
new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader();
|
||||||
String entryName = entry.getName();
|
try (java.io.InputStream in = getResource("cem/cem_template_models.json")) {
|
||||||
|
int n = in == null ? 0 : cemLoader.load(in, getLogger());
|
||||||
|
getLogger().info("Loaded " + n + " CEM entity models.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().severe("Failed to load CEM entity models: " + e.getMessage());
|
||||||
|
}
|
||||||
|
eu.mhsl.minecraft.pixelpics.assets.SkinCache skinCache = new eu.mhsl.minecraft.pixelpics.assets.SkinCache();
|
||||||
|
eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker entityBaker =
|
||||||
|
new eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker(cemLoader, textures, skinCache);
|
||||||
|
|
||||||
// Nur JSON-Dateien im gewünschten Ordner
|
this.screenRenderer = new DefaultScreenRenderer(registry, tintProvider, textures, entityBaker, getLogger());
|
||||||
if (entryName.startsWith(resourcePath) && entryName.endsWith(".json")) {
|
// Warm the map palette on the main thread so off-thread dithering never triggers its first init.
|
||||||
InputStream in = getResource(entryName);
|
eu.mhsl.minecraft.pixelpics.utils.MapColorPalette.size();
|
||||||
if (in == null) continue;
|
getLogger().info("PixelPics renderer initialized with resource pack assets.");
|
||||||
|
}
|
||||||
|
|
||||||
File outFile = new File(getDataFolder(), entryName);
|
@Override
|
||||||
outFile.getParentFile().mkdirs(); // Ordnerstruktur sicherstellen
|
public void onDisable() {
|
||||||
|
if (resourcePack != null) {
|
||||||
try (OutputStream out = new FileOutputStream(outFile)) {
|
resourcePack.close();
|
||||||
byte[] buffer = new byte[1024];
|
resourcePack = null;
|
||||||
int len;
|
|
||||||
while ((len = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, len);
|
|
||||||
}
|
|
||||||
System.out.println("Extrahiert: " + entryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException | URISyntaxException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Renderer getScreenRenderer() {
|
/** The renderer, or {@code null} when no resource pack is available (degraded mode). */
|
||||||
if(this.screenRenderer == null) this.screenRenderer = new DefaultScreenRenderer();
|
public DefaultScreenRenderer getScreenRenderer() {
|
||||||
return this.screenRenderer;
|
return this.screenRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package eu.mhsl.minecraft.pixelpics.commands;
|
package eu.mhsl.minecraft.pixelpics.commands;
|
||||||
|
|
||||||
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
|
||||||
import eu.mhsl.minecraft.pixelpics.Main;
|
import eu.mhsl.minecraft.pixelpics.Main;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
|
||||||
import eu.mhsl.minecraft.pixelpics.render.render.Resolution;
|
import eu.mhsl.minecraft.pixelpics.render.render.Resolution;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.utils.MapImageDither;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
@@ -15,50 +21,128 @@ import org.bukkit.map.MapView;
|
|||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.util.List;
|
||||||
import java.io.IOException;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class PixelPicsCommand implements CommandExecutor {
|
public class PixelPicsCommand implements CommandExecutor {
|
||||||
|
|
||||||
|
private static final int DEFAULT_CLEANUP_DAYS = 30;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String @NotNull [] args) {
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
|
||||||
if(!(sender instanceof Player player))
|
@NotNull String @NotNull [] args) {
|
||||||
throw new IllegalStateException("Dieser Command kann nur von einem Spieler ausgeführt werden!");
|
if (args.length >= 1 && args[0].equalsIgnoreCase("cleanup")) {
|
||||||
|
return cleanup(sender, args);
|
||||||
if(args.length > 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// render image
|
|
||||||
Resolution resolution = new Resolution(Resolution.Pixels._128P, Resolution.AspectRatio._1_1);
|
|
||||||
BufferedImage image = Main.getInstance().getScreenRenderer().render(player.getEyeLocation(), resolution);
|
|
||||||
|
|
||||||
// save image
|
|
||||||
UUID imageId = UUID.randomUUID();
|
|
||||||
File imageFolder = new File(Main.getInstance().getDataFolder(), "images");
|
|
||||||
try {
|
|
||||||
if(!imageFolder.exists() && !imageFolder.mkdirs())
|
|
||||||
throw new IOException("Failed to create folders for image output!");
|
|
||||||
|
|
||||||
ImageIO.write(image, "png", new File(imageFolder, String.format("%s.png", imageId)));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to save image to disk!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// image item
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage(Component.text("Dieser Command kann nur von einem Spieler ausgeführt werden!",
|
||||||
|
NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length > 0) return false;
|
||||||
|
|
||||||
|
DefaultScreenRenderer renderer = Main.getInstance().getScreenRenderer();
|
||||||
|
if (renderer == null) {
|
||||||
|
player.sendMessage(Component.text("PixelPics ist nicht einsatzbereit: es wurde kein Resource-Pack geladen.",
|
||||||
|
NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Resolution resolution = new Resolution(Resolution.Pixels._128P, Resolution.AspectRatio._1_1);
|
||||||
|
|
||||||
|
// Capture the world snapshot on the main thread.
|
||||||
|
RenderJob job = renderer.prepare(player.getEyeLocation(), resolution, player.getUniqueId());
|
||||||
|
|
||||||
|
// Hand the map over immediately, showing blank "film"; it develops once the render is ready.
|
||||||
|
ImageMapRenderer mapRenderer = new ImageMapRenderer();
|
||||||
|
MapView mapView = Bukkit.createMap(player.getWorld());
|
||||||
|
int id = mapView.getId();
|
||||||
|
MapManager.attachView(mapView, mapRenderer);
|
||||||
|
|
||||||
ItemStack map = new ItemStack(Material.FILLED_MAP, 1);
|
ItemStack map = new ItemStack(Material.FILLED_MAP, 1);
|
||||||
MapMeta meta = (MapMeta) map.getItemMeta();
|
MapMeta meta = (MapMeta) map.getItemMeta();
|
||||||
meta.getPersistentDataContainer().set(Main.getInstance().pictureIdFlag, PersistentDataType.STRING, imageId.toString());
|
meta.getPersistentDataContainer().set(Main.getInstance().pictureIdFlag,
|
||||||
|
PersistentDataType.STRING, UUID.randomUUID().toString());
|
||||||
// display image
|
|
||||||
MapView mapView = Bukkit.createMap(Bukkit.getWorlds().getFirst());
|
|
||||||
mapView.getRenderers().forEach(mapView::removeRenderer);
|
|
||||||
mapView.addRenderer(new ImageMapRenderer(image));
|
|
||||||
meta.setMapView(mapView);
|
meta.setMapView(mapView);
|
||||||
|
|
||||||
map.setItemMeta(meta);
|
map.setItemMeta(meta);
|
||||||
player.getInventory().addItem(map);
|
player.getInventory().addItem(map);
|
||||||
|
player.sendMessage(Component.text("📸 Aufnahme wird entwickelt …", NamedTextColor.GRAY));
|
||||||
|
|
||||||
|
// Trace + dither off-thread, then start the developing animation on the main thread.
|
||||||
|
Main plugin = Main.getInstance();
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
BufferedImage image;
|
||||||
|
byte[] indices;
|
||||||
|
try {
|
||||||
|
image = renderer.execute(job);
|
||||||
|
indices = MapImageDither.dither(image);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("Render failed: " + e.getMessage());
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () ->
|
||||||
|
player.sendMessage(Component.text("Rendern fehlgeschlagen.", NamedTextColor.RED)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BufferedImage finalImage = image;
|
||||||
|
byte[] finalIndices = indices;
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
MapManager.saveImage(finalImage, id);
|
||||||
|
MapManager.saveIndices(finalIndices, id);
|
||||||
|
mapRenderer.develop(finalIndices);
|
||||||
|
player.sendMessage(Component.text("✅ Aufnahme erstellt!", NamedTextColor.GREEN));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code /pixelPic cleanup [days]} dry-run; {@code /pixelPic cleanup confirm [days]} deletes orphans. */
|
||||||
|
private boolean cleanup(CommandSender sender, String[] args) {
|
||||||
|
if (!sender.hasPermission("pixelpic.admin") && !sender.isOp()) {
|
||||||
|
sender.sendMessage(Component.text("Dafür fehlt dir die Berechtigung (pixelpic.admin).", NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Integer> inUse = MapManager.collectInUseMapIds();
|
||||||
|
long cutoff = System.currentTimeMillis() - days * 86_400_000L;
|
||||||
|
List<MapManager.StoredMap> candidates = MapManager.listStored().stream()
|
||||||
|
.filter(s -> !inUse.contains(s.id()) && s.lastModified() < cutoff)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
sender.sendMessage(Component.text("Keine aufräumbaren Aufnahmen gefunden (älter als " + days
|
||||||
|
+ " Tage und nicht in Benutzung).", NamedTextColor.YELLOW));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
sender.sendMessage(Component.text(candidates.size() + " Aufnahme(n) könnten gelöscht werden "
|
||||||
|
+ "(älter als " + days + " Tage, nicht in geladenen Itemframes/Online-Inventaren).",
|
||||||
|
NamedTextColor.YELLOW));
|
||||||
|
sender.sendMessage(Component.text("Achtung: Maps in lange ungeladenen Bereichen werden hierbei nicht "
|
||||||
|
+ "erkannt. Zum Löschen: /pixelPic cleanup confirm " + days, NamedTextColor.GRAY));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deleted = 0;
|
||||||
|
for (MapManager.StoredMap s : candidates) {
|
||||||
|
if (MapManager.delete(s.id())) deleted++;
|
||||||
|
}
|
||||||
|
sender.sendMessage(Component.text(deleted + " Aufnahme(n) gelöscht.", NamedTextColor.GREEN));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.mhsl.minecraft.pixelpics.listeners;
|
||||||
|
|
||||||
|
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.utils.MapImageDither;
|
||||||
|
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.server.MapInitializeEvent;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
public class OnMapInitialize implements Listener {
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onMapInitialize(MapInitializeEvent event) {
|
||||||
|
int mapId = event.getMap().getId();
|
||||||
|
|
||||||
|
// Fast path: use the cached dithered indices (no re-quantization).
|
||||||
|
byte[] indices = MapManager.readIndices(mapId);
|
||||||
|
if (indices != null) {
|
||||||
|
MapManager.attachView(event.getMap(), new ImageMapRenderer(indices));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback/migration: dither from the stored PNG once, then cache for next time.
|
||||||
|
BufferedImage image = MapManager.readImage(mapId);
|
||||||
|
if (image == null) return;
|
||||||
|
indices = MapImageDither.dither(image);
|
||||||
|
MapManager.saveIndices(indices, mapId);
|
||||||
|
MapManager.attachView(event.getMap(), new ImageMapRenderer(indices));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,39 +8,114 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws precomputed map palette indices onto the canvas. Supports a Polaroid-style "developing"
|
||||||
|
* animation: the picture dissolves in over {@link #DEVELOP_MILLIS} once the render is ready, like an
|
||||||
|
* old instant camera. Persisted maps are drawn instantly without animation.
|
||||||
|
*
|
||||||
|
* <p>{@code render()} is invoked by Bukkit each tick for viewing players, which drives the animation;
|
||||||
|
* progress is based on wall-clock time so it is correct even if the player looks away and back.
|
||||||
|
*/
|
||||||
public class ImageMapRenderer extends MapRenderer {
|
public class ImageMapRenderer extends MapRenderer {
|
||||||
public static final int MAP_SIZE = 128;
|
|
||||||
private final BufferedImage image;
|
|
||||||
private boolean alreadyRendered = false;
|
|
||||||
|
|
||||||
|
public static final int MAP_SIZE = MapImageDither.SIZE;
|
||||||
|
private static final long DEVELOP_MILLIS = 3000;
|
||||||
|
|
||||||
|
private static byte filmIndex = 0;
|
||||||
|
private static boolean filmResolved = false;
|
||||||
|
|
||||||
|
private volatile byte[] indices; // MAP_SIZE*MAP_SIZE, null while still rendering
|
||||||
|
private final boolean animate;
|
||||||
|
private volatile long developStart = 0; // 0 until indices are available
|
||||||
|
private boolean finished = false;
|
||||||
|
private boolean blankDrawn = false;
|
||||||
|
|
||||||
|
/** Persisted/instant map: draw the full picture immediately, no animation. */
|
||||||
|
public ImageMapRenderer(byte[] indices) {
|
||||||
|
this.indices = indices;
|
||||||
|
this.animate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fresh capture from a rendered image: dithers immediately, then develops. */
|
||||||
public ImageMapRenderer(BufferedImage image) {
|
public ImageMapRenderer(BufferedImage image) {
|
||||||
this(image, 0, 0);
|
this(MapImageDither.dither(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageMapRenderer(BufferedImage image, int x, int y) {
|
/** Pending capture: shows blank film until {@link #develop} supplies the picture. */
|
||||||
this.image = this.recalculateInput(image, x, y);
|
public ImageMapRenderer() {
|
||||||
|
this.indices = null;
|
||||||
|
this.animate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage recalculateInput(BufferedImage input, int x, int y) {
|
public byte[] getIndices() {
|
||||||
if (x * MAP_SIZE > input.getWidth() || y * MAP_SIZE > input.getHeight())
|
return indices;
|
||||||
throw new RuntimeException(String.format("Input image mus match a multiple of x and y with %d", MAP_SIZE));
|
}
|
||||||
|
|
||||||
int x1 = (int) (double) (x * MAP_SIZE);
|
/** Supplies the finished picture and starts the developing animation. */
|
||||||
int y1 = (int) (double) (y * MAP_SIZE);
|
public void develop(byte[] indices) {
|
||||||
|
this.indices = indices;
|
||||||
int x2 = (int) (double) Math.min(input.getWidth(), ((x + 1) * MAP_SIZE));
|
this.developStart = System.currentTimeMillis();
|
||||||
int y2 = (int) (double) Math.min(input.getHeight(), ((y + 1) * MAP_SIZE));
|
this.finished = false;
|
||||||
|
|
||||||
if (x2 - x1 <= 0 || y2 - y1 <= 0)
|
|
||||||
throw new RuntimeException("Invalid Image dimensions!");
|
|
||||||
|
|
||||||
return input.getSubimage(x1, y1, x2 - x1, y2 - y1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
|
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
|
||||||
if(this.alreadyRendered) return;
|
if (finished) return;
|
||||||
canvas.drawImage(0, 0, this.image);
|
byte[] data = this.indices;
|
||||||
this.alreadyRendered = true;
|
|
||||||
|
// Still rendering: show a blank "film" once.
|
||||||
|
if (data == null) {
|
||||||
|
if (!blankDrawn) {
|
||||||
|
fill(canvas, film());
|
||||||
|
blankDrawn = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!animate) {
|
||||||
|
blit(canvas, data, 1.0);
|
||||||
|
finished = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double progress = (System.currentTimeMillis() - developStart) / (double) DEVELOP_MILLIS;
|
||||||
|
blit(canvas, data, progress);
|
||||||
|
if (progress >= 1.0) finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Draws revealed pixels at the given progress; unrevealed pixels stay film-colored. */
|
||||||
|
private void blit(MapCanvas canvas, byte[] data, double progress) {
|
||||||
|
byte film = film();
|
||||||
|
for (int y = 0; y < MAP_SIZE; y++) {
|
||||||
|
for (int x = 0; x < MAP_SIZE; x++) {
|
||||||
|
byte value = (progress >= 1.0 || revealThreshold(x, y) <= progress)
|
||||||
|
? data[y * MAP_SIZE + x] : film;
|
||||||
|
canvas.setPixel(x, y, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private void fill(MapCanvas canvas, byte value) {
|
||||||
|
for (int y = 0; y < MAP_SIZE; y++) {
|
||||||
|
for (int x = 0; x < MAP_SIZE; x++) canvas.setPixel(x, y, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-pixel reveal time in [0,1]: mostly noise plus a gentle top-to-bottom sweep for an organic develop. */
|
||||||
|
private double revealThreshold(int x, int y) {
|
||||||
|
int h = (x * 374761393 + y * 668265263);
|
||||||
|
h = (h ^ (h >>> 13)) * 1274126177;
|
||||||
|
double noise = ((h >>> 8) & 0xFFFF) / 65535.0;
|
||||||
|
double sweep = y / (double) (MAP_SIZE - 1);
|
||||||
|
return Math.min(0.999, noise * 0.7 + sweep * 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte film() {
|
||||||
|
if (!filmResolved) {
|
||||||
|
filmIndex = MapColorPalette.mapIndex(MapColorPalette.nearestPos(206, 202, 190));
|
||||||
|
filmResolved = true;
|
||||||
|
}
|
||||||
|
return filmIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package eu.mhsl.minecraft.pixelpics.utils;
|
||||||
|
|
||||||
|
import org.bukkit.map.MapPalette;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of usable Minecraft map colors, with nearest-color matching in CIELAB (perceptual) space.
|
||||||
|
* Built once from {@link MapPalette}. Matching in Lab rather than RGB keeps hues correct (greens stay
|
||||||
|
* green instead of collapsing to gray).
|
||||||
|
*/
|
||||||
|
public final class MapColorPalette {
|
||||||
|
|
||||||
|
private static volatile boolean initialized = false;
|
||||||
|
private static byte[] indices;
|
||||||
|
private static int[] rgb;
|
||||||
|
private static double[][] lab;
|
||||||
|
|
||||||
|
private MapColorPalette() {}
|
||||||
|
|
||||||
|
private static synchronized void ensure() {
|
||||||
|
if (initialized) return;
|
||||||
|
List<Byte> idx = new ArrayList<>();
|
||||||
|
List<Integer> rgbs = new ArrayList<>();
|
||||||
|
List<double[]> labs = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
Color c;
|
||||||
|
try {
|
||||||
|
c = MapPalette.getColor((byte) i);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == null || c.getAlpha() < 255) continue; // skip transparent slots
|
||||||
|
idx.add((byte) i);
|
||||||
|
rgbs.add((c.getRed() << 16) | (c.getGreen() << 8) | c.getBlue());
|
||||||
|
labs.add(rgbToLab(c.getRed(), c.getGreen(), c.getBlue()));
|
||||||
|
}
|
||||||
|
indices = new byte[idx.size()];
|
||||||
|
rgb = new int[idx.size()];
|
||||||
|
lab = new double[idx.size()][];
|
||||||
|
for (int i = 0; i < idx.size(); i++) {
|
||||||
|
indices[i] = idx.get(i);
|
||||||
|
rgb[i] = rgbs.get(i);
|
||||||
|
lab[i] = labs.get(i);
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int size() {
|
||||||
|
ensure();
|
||||||
|
return indices.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Index into the internal arrays of the perceptually nearest palette color. */
|
||||||
|
public static int nearestPos(int r, int g, int b) {
|
||||||
|
ensure();
|
||||||
|
double[] target = rgbToLab(r, g, b);
|
||||||
|
double best = Double.MAX_VALUE;
|
||||||
|
int bestPos = 0;
|
||||||
|
for (int i = 0; i < lab.length; i++) {
|
||||||
|
double[] l = lab[i];
|
||||||
|
double dl = target[0] - l[0], da = target[1] - l[1], db = target[2] - l[2];
|
||||||
|
double d = dl * dl + da * da + db * db;
|
||||||
|
if (d < best) {
|
||||||
|
best = d;
|
||||||
|
bestPos = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte mapIndex(int pos) {
|
||||||
|
return indices[pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int mapRgb(int pos) {
|
||||||
|
return rgb[pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double[] rgbToLab(int r, int g, int b) {
|
||||||
|
double rl = pivotSrgb(r), gl = pivotSrgb(g), bl = pivotSrgb(b);
|
||||||
|
// sRGB -> XYZ (D65)
|
||||||
|
double x = rl * 0.4124 + gl * 0.3576 + bl * 0.1805;
|
||||||
|
double y = rl * 0.2126 + gl * 0.7152 + bl * 0.0722;
|
||||||
|
double z = rl * 0.0193 + gl * 0.1192 + bl * 0.9505;
|
||||||
|
double fx = pivotXyz(x / 0.95047);
|
||||||
|
double fy = pivotXyz(y);
|
||||||
|
double fz = pivotXyz(z / 1.08883);
|
||||||
|
return new double[]{116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double pivotSrgb(int c) {
|
||||||
|
double v = c / 255.0;
|
||||||
|
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double pivotXyz(double t) {
|
||||||
|
return t > 0.008856 ? Math.cbrt(t) : (7.787 * t + 16.0 / 116.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package eu.mhsl.minecraft.pixelpics.utils;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quantizes a rendered image to Minecraft map color indices using dampened Floyd–Steinberg error
|
||||||
|
* diffusion with perceptual (CIELAB) nearest-color matching. The result is a {@code 128*128} byte
|
||||||
|
* array of palette indices, ready to blit onto a {@link org.bukkit.map.MapCanvas}.
|
||||||
|
*
|
||||||
|
* <p>This is the expensive step; computing it once and caching the byte array lets map re-rendering
|
||||||
|
* after a restart be a cheap copy instead of a full re-quantization.
|
||||||
|
*/
|
||||||
|
public final class MapImageDither {
|
||||||
|
|
||||||
|
public static final int SIZE = 128;
|
||||||
|
|
||||||
|
/** Fraction of quantization error diffused. Low values keep gradients smooth without visible noise. */
|
||||||
|
private static final float DITHER_STRENGTH = 0.35f;
|
||||||
|
|
||||||
|
private MapImageDither() {}
|
||||||
|
|
||||||
|
/** Returns a {@code SIZE*SIZE} array of map palette indices (row-major, y*SIZE + x). */
|
||||||
|
public static byte[] dither(BufferedImage image) {
|
||||||
|
int w = Math.min(SIZE, image.getWidth());
|
||||||
|
int h = Math.min(SIZE, image.getHeight());
|
||||||
|
byte[] out = new byte[SIZE * SIZE];
|
||||||
|
|
||||||
|
float[][] buf = new float[h][w * 3];
|
||||||
|
for (int y = 0; y < h; y++) {
|
||||||
|
for (int x = 0; x < w; x++) {
|
||||||
|
int argb = image.getRGB(x, y);
|
||||||
|
buf[y][x * 3] = (argb >> 16) & 0xFF;
|
||||||
|
buf[y][x * 3 + 1] = (argb >> 8) & 0xFF;
|
||||||
|
buf[y][x * 3 + 2] = argb & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = 0; y < h; y++) {
|
||||||
|
for (int x = 0; x < w; x++) {
|
||||||
|
int i = x * 3;
|
||||||
|
int r = clamp(buf[y][i]);
|
||||||
|
int g = clamp(buf[y][i + 1]);
|
||||||
|
int b = clamp(buf[y][i + 2]);
|
||||||
|
|
||||||
|
int pos = MapColorPalette.nearestPos(r, g, b);
|
||||||
|
out[y * SIZE + x] = MapColorPalette.mapIndex(pos);
|
||||||
|
|
||||||
|
int chosen = MapColorPalette.mapRgb(pos);
|
||||||
|
float er = (r - ((chosen >> 16) & 0xFF)) * DITHER_STRENGTH;
|
||||||
|
float eg = (g - ((chosen >> 8) & 0xFF)) * DITHER_STRENGTH;
|
||||||
|
float eb = (b - (chosen & 0xFF)) * DITHER_STRENGTH;
|
||||||
|
|
||||||
|
spread(buf, y, x + 1, w, h, er, eg, eb, 7f / 16f);
|
||||||
|
spread(buf, y + 1, x - 1, w, h, er, eg, eb, 3f / 16f);
|
||||||
|
spread(buf, y + 1, x, w, h, er, eg, eb, 5f / 16f);
|
||||||
|
spread(buf, y + 1, x + 1, w, h, er, eg, eb, 1f / 16f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void spread(float[][] buf, int y, int x, int w, int h, float er, float eg, float eb, float f) {
|
||||||
|
if (x < 0 || x >= w || y < 0 || y >= h) return;
|
||||||
|
int i = x * 3;
|
||||||
|
buf[y][i] += er * f;
|
||||||
|
buf[y][i + 1] += eg * f;
|
||||||
|
buf[y][i + 2] += eb * f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(float v) {
|
||||||
|
return v < 0 ? 0 : (v > 255 ? 255 : Math.round(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package eu.mhsl.minecraft.pixelpics.utils;
|
||||||
|
|
||||||
|
import eu.mhsl.minecraft.pixelpics.Main;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.ItemFrame;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.MapMeta;
|
||||||
|
import org.bukkit.map.MapView;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists rendered images (PNG, source of truth) and their dithered map-color indices (cache) to
|
||||||
|
* disk, (re)attaches the renderer to a {@link MapView}, and supports orphan cleanup. State is
|
||||||
|
* filesystem-backed so screenshots survive a restart.
|
||||||
|
*/
|
||||||
|
public class MapManager {
|
||||||
|
|
||||||
|
private static final Pattern FILE_ID = Pattern.compile("mapId_(\\d+)\\.(png|mcmap)");
|
||||||
|
private static final int INDEX_BYTES = ImageMapRenderer.MAP_SIZE * ImageMapRenderer.MAP_SIZE;
|
||||||
|
|
||||||
|
private MapManager() {}
|
||||||
|
|
||||||
|
private static File imageFolder() {
|
||||||
|
return new File(Main.getInstance().getDataFolder(), "images");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void attachView(MapView mapView, ImageMapRenderer renderer) {
|
||||||
|
mapView.getRenderers().clear();
|
||||||
|
mapView.setScale(MapView.Scale.FARTHEST);
|
||||||
|
mapView.addRenderer(renderer);
|
||||||
|
mapView.setTrackingPosition(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getImagePath(int mapId) {
|
||||||
|
return new File(imageFolder(), String.format("mapId_%d.png", mapId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getIndexPath(int mapId) {
|
||||||
|
return new File(imageFolder(), String.format("mapId_%d.mcmap", mapId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean ensureFolder() {
|
||||||
|
File folder = imageFolder();
|
||||||
|
if (!folder.exists() && !folder.mkdirs()) {
|
||||||
|
Main.getInstance().getLogger().warning("Failed to create image output folder: " + folder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveImage(BufferedImage image, int id) {
|
||||||
|
if (!ensureFolder()) return;
|
||||||
|
try {
|
||||||
|
ImageIO.write(image, "png", getImagePath(id));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Main.getInstance().getLogger().warning("Failed to save map image " + id + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveIndices(byte[] indices, int id) {
|
||||||
|
if (indices == null || indices.length != INDEX_BYTES || !ensureFolder()) return;
|
||||||
|
try {
|
||||||
|
Files.write(getIndexPath(id).toPath(), indices);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Main.getInstance().getLogger().warning("Failed to save map index cache " + id + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads the cached dithered indices, or {@code null} if absent/invalid. */
|
||||||
|
public static byte[] readIndices(int id) {
|
||||||
|
File path = getIndexPath(id);
|
||||||
|
if (!path.isFile()) return null;
|
||||||
|
try {
|
||||||
|
byte[] data = Files.readAllBytes(path.toPath());
|
||||||
|
return data.length == INDEX_BYTES ? data : null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads the stored image, or {@code null} if it does not exist or cannot be read. */
|
||||||
|
public static BufferedImage readImage(int id) {
|
||||||
|
File path = getImagePath(id);
|
||||||
|
if (!path.isFile()) return null;
|
||||||
|
try {
|
||||||
|
return ImageIO.read(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Main.getInstance().getLogger().warning("Failed to read map image " + id + ": " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isManaged(int id) {
|
||||||
|
return getIndexPath(id).isFile() || getImagePath(id).isFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cleanup support ---
|
||||||
|
|
||||||
|
/** A map id stored on disk together with the newest of its files' last-modified time. */
|
||||||
|
public record StoredMap(int id, long lastModified) {}
|
||||||
|
|
||||||
|
/** All map ids that have files on disk. */
|
||||||
|
public static Set<StoredMap> listStored() {
|
||||||
|
Set<StoredMap> result = new HashSet<>();
|
||||||
|
File folder = imageFolder();
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
if (files == null) return result;
|
||||||
|
Set<Integer> seen = new HashSet<>();
|
||||||
|
for (File f : files) {
|
||||||
|
Matcher m = FILE_ID.matcher(f.getName());
|
||||||
|
if (!m.matches()) continue;
|
||||||
|
int id = Integer.parseInt(m.group(1));
|
||||||
|
if (!seen.add(id)) continue;
|
||||||
|
long lm = Math.max(lastModified(getImagePath(id)), lastModified(getIndexPath(id)));
|
||||||
|
result.add(new StoredMap(id, lm));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long lastModified(File f) {
|
||||||
|
return f.isFile() ? f.lastModified() : 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map ids referenced by currently-loaded item frames and online players (inventory + ender chest). */
|
||||||
|
public static Set<Integer> collectInUseMapIds() {
|
||||||
|
Set<Integer> inUse = new HashSet<>();
|
||||||
|
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||||
|
addFromContents(inUse, p.getInventory().getContents());
|
||||||
|
addFromContents(inUse, p.getEnderChest().getContents());
|
||||||
|
addFromItem(inUse, p.getItemOnCursor());
|
||||||
|
}
|
||||||
|
for (World w : Bukkit.getWorlds()) {
|
||||||
|
for (ItemFrame frame : w.getEntitiesByClass(ItemFrame.class)) {
|
||||||
|
addFromItem(inUse, frame.getItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inUse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addFromContents(Set<Integer> set, ItemStack[] contents) {
|
||||||
|
if (contents == null) return;
|
||||||
|
for (ItemStack item : contents) addFromItem(set, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addFromItem(Set<Integer> set, ItemStack item) {
|
||||||
|
if (item == null || item.getType() != Material.FILLED_MAP || !item.hasItemMeta()) return;
|
||||||
|
if (item.getItemMeta() instanceof MapMeta meta && meta.hasMapView() && meta.getMapView() != null) {
|
||||||
|
set.add(meta.getMapView().getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deletes both files for a map id. Returns true if anything was deleted. */
|
||||||
|
public static boolean delete(int id) {
|
||||||
|
boolean deleted = false;
|
||||||
|
File png = getImagePath(id);
|
||||||
|
File idx = getIndexPath(id);
|
||||||
|
if (png.isFile()) deleted |= png.delete();
|
||||||
|
if (idx.isFile()) deleted |= idx.delete();
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
name: PixelPic
|
name: PixelPics
|
||||||
version: '1.0-SNAPSHOT'
|
version: '${version}'
|
||||||
main: eu.mhsl.minecraft.pixelpics.Main
|
main: eu.mhsl.minecraft.pixelpics.Main
|
||||||
api-version: '1.21.7'
|
api-version: '1.21'
|
||||||
commands:
|
commands:
|
||||||
pixelPic:
|
pixelPic:
|
||||||
permission: "pixelpic.use"
|
permission: "pixelpic.use"
|
||||||
usage: "pixelpic take"
|
usage: "/pixelPic [cleanup [confirm] [days]]"
|
||||||
test:
|
permissions:
|
||||||
|
pixelpic.use:
|
||||||
|
description: "Allows taking PixelPics camera screenshots"
|
||||||
|
default: true
|
||||||
|
pixelpic.admin:
|
||||||
|
description: "Allows managing PixelPics (e.g. cleanup)"
|
||||||
|
default: op
|
||||||
|
|||||||
Reference in New Issue
Block a user