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.SkinCache; import eu.mhsl.minecraft.pixelpics.assets.TextureCache; import eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont; import eu.mhsl.minecraft.pixelpics.assets.font.FontLoader; import eu.mhsl.minecraft.pixelpics.commands.PixelPicsCommand; import eu.mhsl.minecraft.pixelpics.listeners.OnMapInitialize; import eu.mhsl.minecraft.pixelpics.render.RenderManager; import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker; import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker; import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemModelLoader; import eu.mhsl.minecraft.pixelpics.render.entity.cem.DatapackVariants; import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import eu.mhsl.minecraft.pixelpics.survival.CameraListener; import eu.mhsl.minecraft.pixelpics.survival.CraftingListener; import eu.mhsl.minecraft.pixelpics.survival.ItemGuardListener; import eu.mhsl.minecraft.pixelpics.survival.JoinListener; import eu.mhsl.minecraft.pixelpics.survival.PendingPhotos; import eu.mhsl.minecraft.pixelpics.survival.SurvivalRecipes; import eu.mhsl.minecraft.pixelpics.utils.MapColorPalette; import eu.mhsl.minecraft.pixelpics.utils.MapManager; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.List; import java.util.Objects; import java.util.Optional; import org.bukkit.Bukkit; import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; public final class Main extends JavaPlugin { private static Main instance; private ResourcePack resourcePack; private DefaultScreenRenderer screenRenderer; private RenderManager renderManager; private PendingPhotos pendingPhotos; // PDC keys: photo picture id (STRING), camera/film markers (BYTE), loaded film on a camera (INTEGER) public final NamespacedKey pictureIdFlag = new NamespacedKey(this, "imageid"); public final NamespacedKey cameraMarker = new NamespacedKey(this, "camera"); public final NamespacedKey filmCountKey = new NamespacedKey(this, "filmcount"); public final NamespacedKey filmMarker = new NamespacedKey(this, "film"); @Override public void onEnable() { instance = this; saveDefaultConfig(); MapManager.init(getServer().getWorlds().getFirst().getUID()); pendingPhotos = new PendingPhotos(getDataFolder()); pendingPhotos.recoverAbandoned(); initRenderManager(); Objects.requireNonNull(getCommand("pixelPic")).setExecutor(new PixelPicsCommand()); for (Listener listener : List.of( new OnMapInitialize(), new CameraListener(), new CraftingListener(), new ItemGuardListener(), new JoinListener())) { Bukkit.getPluginManager().registerEvents(listener, this); } SurvivalRecipes.register(); MapManager.reattachAfterReload(OnMapInitialize::attach); initRenderer(); } private void initRenderManager() { int cores = Runtime.getRuntime().availableProcessors(); int threads = getConfig().getInt("render.threads", 0); if (threads <= 0) threads = Math.max(1, cores - 2); threads = Math.min(threads, cores); int maxConcurrent = Math.max(1, getConfig().getInt("render.max-concurrent", 2)); int queueSize = Math.max(0, getConfig().getInt("render.queue-size", 8)); int timeoutSeconds = Math.max(1, getConfig().getInt("render.timeout-seconds", 30)); this.renderManager = new RenderManager(this, threads, maxConcurrent, queueSize, timeoutSeconds); getLogger() .info("Render pool: " + threads + " core(s), max " + maxConcurrent + " concurrent, queue " + queueSize + ", timeout " + timeoutSeconds + "s."); } private void initRenderer() { File resourcePackDir = new File(getDataFolder(), "resourcepack"); if (!resourcePackDir.exists() && !resourcePackDir.mkdirs()) { getLogger().warning("Could not create resource pack directory: " + resourcePackDir); } Optional pack = ResourcePackLoader.load(resourcePackDir, getLogger()); if (pack.isEmpty()) { 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; } this.resourcePack = pack.get(); AssetReader reader = new AssetReader(resourcePack); TextureCache textures = new TextureCache(resourcePack); BlockModelRegistry registry = new BlockModelRegistry(reader, textures); BiomeTintProvider tintProvider = new BiomeTintProvider(textures); CemModelLoader cemLoader = new CemModelLoader(); try (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()); } // Vanilla models dumped from the client jar (tools/VanillaModelDump.java) override the CEM set: an // optional newer dump in the data folder (no recompile on MC updates), else the bundled copy. File dump = new File(getDataFolder(), "vanilla_entity_models.json"); try (InputStream in = dump.isFile() ? new FileInputStream(dump) : getResource("cem/vanilla_entity_models.json")) { if (in != null) { long t0 = System.nanoTime(); int n = cemLoader.load(in, getLogger()); getLogger() .info("Loaded vanilla entity models" + (dump.isFile() ? " from " + dump.getName() : "") + " (" + n + " models total, " + (System.nanoTime() - t0) / 1_000_000 + " ms)."); } } catch (Exception e) { getLogger().severe("Failed to load vanilla entity models: " + e.getMessage()); } List worldFolders = getServer().getWorlds().stream().map(World::getWorldFolder).toList(); int datapackVariants = DatapackVariants.load(cemLoader, worldFolders, getLogger()); if (datapackVariants > 0) getLogger().info("Loaded " + datapackVariants + " datapack mob variants."); SkinCache skinCache = new SkinCache(); BitmapFont font = FontLoader.load(resourcePack, textures, getLogger()); getLogger().info("Loaded sign font (" + (font.isEmpty() ? "no glyphs — text disabled" : "ok") + ")."); CemBaker entityBaker = new CemBaker(cemLoader, textures, skinCache); BlockEntityBaker blockEntityBaker = new BlockEntityBaker(cemLoader, textures, skinCache, font); this.screenRenderer = new DefaultScreenRenderer( registry, tintProvider, textures, entityBaker, blockEntityBaker, getLogger(), renderManager.tracePool()); // Initialise the map palette on the main thread (MapPalette) before any off-thread dithering. MapColorPalette.size(); getLogger().info("PixelPics renderer initialized with resource pack assets."); } @Override public void onDisable() { SurvivalRecipes.unregister(); if (renderManager != null) { renderManager.shutdown(); // delivers or fails every open job synchronously (film refunds) renderManager = null; } MapManager.detachAll(); if (resourcePack != null) { resourcePack.close(); resourcePack = null; } } /** The renderer, or {@code null} when no resource pack is available (degraded mode). */ public DefaultScreenRenderer getScreenRenderer() { return this.screenRenderer; } public PendingPhotos getPendingPhotos() { return this.pendingPhotos; } public RenderManager getRenderManager() { return this.renderManager; } public static Main getInstance() { return instance; } }