package de.nexuslobby.modules.mapart; import de.nexuslobby.NexusLobby; import de.nexuslobby.api.Module; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; 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.MapCanvas; import org.bukkit.map.MapRenderer; import org.bukkit.map.MapView; import org.bukkit.util.RayTraceResult; import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; public class MapArtModule implements Module, CommandExecutor { private File storageFile; private FileConfiguration storageConfig; // RAM-Schutz: Cache für bereits geladene Bild-Kacheln private final Map tileCache = new ConcurrentHashMap<>(); // Hilfs-Set um parallele Downloads der gleichen URL zu verhindern private final Map loadingShield = new ConcurrentHashMap<>(); @Override public String getName() { return "MapArt"; } @Override public void onEnable() { if (NexusLobby.getInstance().getCommand("mapart") != null) { NexusLobby.getInstance().getCommand("mapart").setExecutor(this); } initStorage(); reloadMaps(); } @Override public void onDisable() { saveStorage(); tileCache.clear(); } private void initStorage() { storageFile = new File(NexusLobby.getInstance().getDataFolder(), "mapart.yml"); if (!storageFile.exists()) { try { storageFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } storageConfig = YamlConfiguration.loadConfiguration(storageFile); } private void saveStorage() { try { storageConfig.save(storageFile); } catch (IOException e) { e.printStackTrace(); } } private void reloadMaps() { ConfigurationSection section = storageConfig.getConfigurationSection("active_maps"); if (section == null) return; for (String idStr : section.getKeys(false)) { try { int mapId = Integer.parseInt(idStr); String url = section.getString(idStr + ".url"); int x = section.getInt(idStr + ".x"); int y = section.getInt(idStr + ".y"); int totalW = section.getInt(idStr + ".totalW"); int totalH = section.getInt(idStr + ".totalH"); @SuppressWarnings("deprecation") MapView view = Bukkit.getMap(mapId); if (view != null) { applyRenderer(view, url, x, y, totalW, totalH); } } catch (Exception ignored) {} } } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { if (!(sender instanceof Player player)) return true; if (!player.hasPermission("nexuslobby.mapart")) { player.sendMessage("§8[§6Nexus§8] §cKeine Rechte."); return true; } if (args.length == 0) { sendHelp(player); return true; } if (args[0].equalsIgnoreCase("delete")) { int radius = 3; if (args.length == 2) { try { radius = Integer.parseInt(args[1]); } catch (NumberFormatException e) { player.sendMessage("§8[§6Nexus§8] §cUngültiger Radius."); return true; } } deleteNearbyMaps(player, radius); return true; } if (args.length != 2) { sendHelp(player); return true; } RayTraceResult rayTrace = player.rayTraceBlocks(5); if (rayTrace == null || rayTrace.getHitBlock() == null || rayTrace.getHitBlockFace() == null) { player.sendMessage("§8[§6Nexus§8] §cBitte schaue eine Wand direkt an."); return true; } Block targetBlock = rayTrace.getHitBlock(); BlockFace hitFace = rayTrace.getHitBlockFace(); if (hitFace == BlockFace.UP || hitFace == BlockFace.DOWN) { player.sendMessage("§8[§6Nexus§8] §cAktuell nur an vertikalen Wänden möglich."); return true; } String url = args[0]; int width, height; try { String[] parts = args[1].toLowerCase().split("x"); width = Integer.parseInt(parts[0]); height = Integer.parseInt(parts[1]); } catch (Exception e) { player.sendMessage("§8[§6Nexus§8] §cUngültiges Format (z.B. 6x4)."); return true; } player.sendMessage("§8[§6Nexus§8] §7Bild wird verarbeitet..."); createRaster(player, targetBlock, hitFace, url, width, height); return true; } private void sendHelp(Player p) { p.sendMessage("§8§m------------------------------------"); p.sendMessage("§6§lNexus MapArt"); p.sendMessage("§e/mapart §7- Bild erstellen"); p.sendMessage("§e/mapart delete [Radius] §7- Bilder in der Nähe löschen"); p.sendMessage("§8§m------------------------------------"); } private void deleteNearbyMaps(Player player, int radius) { AtomicInteger count = new AtomicInteger(0); player.getNearbyEntities(radius, radius, radius).forEach(entity -> { if (entity instanceof ItemFrame frame) { ItemStack item = frame.getItem(); if (item != null && item.getType() == Material.FILLED_MAP && item.getItemMeta() instanceof MapMeta meta) { if (meta.hasMapView()) { int id = meta.getMapView().getId(); if (storageConfig.contains("active_maps." + id)) { storageConfig.set("active_maps." + id, null); frame.remove(); count.incrementAndGet(); } } } } }); saveStorage(); player.sendMessage("§8[§6Nexus§8] §aErfolgreich §e" + count.get() + " §aKartenelemente gelöscht."); } private void createRaster(Player player, Block startBlock, BlockFace face, String url, int gridW, int gridH) { BlockFace rightDirection; switch (face) { case NORTH: rightDirection = BlockFace.WEST; break; case SOUTH: rightDirection = BlockFace.EAST; break; case EAST: rightDirection = BlockFace.NORTH; break; case WEST: rightDirection = BlockFace.SOUTH; break; default: rightDirection = BlockFace.EAST; } Block origin = startBlock.getRelative(face); for (int y = 0; y < gridH; y++) { for (int x = 0; x < gridW; x++) { Block currentPos = origin.getRelative(rightDirection, x).getRelative(BlockFace.DOWN, y); spawnPersistentFrame(player, currentPos, face, url, x, y, gridW, gridH); } } player.sendMessage("§8[§6Nexus§8] §aBild-Raster wurde permanent platziert!"); } private void spawnPersistentFrame(Player player, Block pos, BlockFace face, String url, int x, int y, int totalW, int totalH) { MapView view = Bukkit.createMap(player.getWorld()); applyRenderer(view, url, x, y, totalW, totalH); String path = "active_maps." + view.getId(); storageConfig.set(path + ".url", url); storageConfig.set(path + ".x", x); storageConfig.set(path + ".y", y); storageConfig.set(path + ".totalW", totalW); storageConfig.set(path + ".totalH", totalH); saveStorage(); ItemStack mapStack = new ItemStack(Material.FILLED_MAP); MapMeta meta = (MapMeta) mapStack.getItemMeta(); if (meta != null) { meta.setMapView(view); mapStack.setItemMeta(meta); } try { ItemFrame frame = player.getWorld().spawn(pos.getLocation(), ItemFrame.class); frame.setFacingDirection(face); frame.setItem(mapStack); frame.setVisible(false); frame.setFixed(true); } catch (Exception ignored) {} } private void applyRenderer(MapView view, String url, int tileX, int tileY, int totalW, int totalH) { view.getRenderers().forEach(view::removeRenderer); view.addRenderer(new MapRenderer() { private final String cacheKey = url + "_" + totalW + "x" + totalH + "_" + tileX + "_" + tileY; private boolean errorLogged = false; @Override public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player p) { // 1. Wenn Kachel im Cache, sofort zeichnen (extrem schnell) if (tileCache.containsKey(cacheKey)) { canvas.drawImage(0, 0, tileCache.get(cacheKey)); return; } // 2. Wenn bereits ein Thread für diese URL lädt, abbrechen (verhindert Spam) if (loadingShield.containsKey(url)) return; // 3. Bild asynchron laden loadingShield.put(url, true); Bukkit.getScheduler().runTaskAsynchronously(NexusLobby.getInstance(), () -> { try { BufferedImage original = ImageIO.read(new URL(url)); if (original != null) { int targetW = totalW * 128; int targetH = totalH * 128; BufferedImage fullScaled = new BufferedImage(targetW, targetH, BufferedImage.TYPE_INT_ARGB); Graphics2D g = fullScaled.createGraphics(); g.drawImage(original.getScaledInstance(targetW, targetH, Image.SCALE_SMOOTH), 0, 0, null); g.dispose(); // Alle benötigten Kacheln für dieses Bild in den Cache legen for (int ty = 0; ty < totalH; ty++) { for (int tx = 0; tx < totalW; tx++) { String key = url + "_" + totalW + "x" + totalH + "_" + tx + "_" + ty; tileCache.put(key, fullScaled.getSubimage(tx * 128, ty * 128, 128, 128)); } } } } catch (Exception e) { if (!errorLogged) { NexusLobby.getInstance().getLogger().warning("Fehler beim Laden von MapArt: " + url); errorLogged = true; } } finally { loadingShield.remove(url); } }); } }); } }