diff --git a/README.md b/README.md index c20f962..24ea83b 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,17 @@ The maps can be configured by adding options to the map's section in the `world. | `chunkyCpuLoad` | Percentage of CPU time to use, per Chunky thread. Note that this only throttles the CPU usage during rendering, not during scene loading or post processing. | 100 | | `texturepack` | Texturepack path, relative to `plugins/dynmap`. Use this option to specify a texturepack for a map. The texturepack in Dynmap's `configuration.txt` is ignored by ChunkyMap. | _None_ | | `chunkPadding` | Radius of additional chunks to be loaded around each chunk that is required to render a tile of the map. This can be used to reduce artifacts caused by shadows and reflections. | 0 | +| `requeueFailedTiles` | Put tiles that failed to render back into the tile queue. | true | | `templateScene` | Path to a Chunky scene file (JSON), relative to `plugins/dynmap`. Use this option to customize the scene that is used for rendering the tiles, e.g. to change the water color. | _None_ | | `texturepackVersion` | The Minecraft version that should be used as fallback textures | 1.16.2 | -| `denoiser.enabled` | Enable denoising using [Intel Open Image Denoise](https://openimagedenoise.github.io/). Only works on Linux | false | -| `denoiser.albedoSamplesPerPixel` | Samples per pixel for the albedo map. Setting this to 0 will disable the albedo and normal map. | 4 | -| `denoiser.normalSamplesPerPixel` | Samples per pixel for the normal map. Setting this to 0 will disable the normal map. | 4 | +| `denoiser/enabled` | Enable denoising using [Intel Open Image Denoise](https://openimagedenoise.github.io/). Only works on Linux | false | +| `denoiser/albedoSamplesPerPixel` | Samples per pixel for the albedo map. Setting this to 0 will disable the albedo and normal map. | 4 | +| `denoiser/normalSamplesPerPixel` | Samples per pixel for the normal map. Setting this to 0 will disable the normal map. | 4 | +| `chunkycloud/enabled` | Render tiles using the Chunky Cloud render service | false | +| `chunkycloud/apiKey` | API Key for the Chunky Cloud render service | | +| `chunkycloud/initializeLocally` | Generate the octree locally. Less data to upload, faster render times but will use a lot of CPU locally. | true | + +:warning: A forward slash (`/`) in the option name means that the right part is a nested option and needs to be put into the next line and indented properly. Take a look at the examples below. ## Example configurations @@ -99,6 +105,25 @@ perspectives: maximumheight: 100 # the bedrock layer is at 127 ``` +### Rendering ChunkyMap on ChunkyCloud + +`plugins/dynmap/worlds.txt`: + +```yml +worlds: + - name: world + maps: + - class: de.lemaik.chunkymap.dynmap.ChunkyMap + name: chunky + title: Chunky + perspective: iso_SE_30_hires + samplesPerPixel: 20 + chunkycloud: + enabled: true + initializeLocally: false + apiKey: your-secret-api-key +``` + ### Customizing the look of a map with template scenes You can change how the map looks by providing a template scene. That can be any Chunky scene (`.json`) file or a partial scene file (i.e. a `.json` file that only contains the values that should be changed). ChunkyMap will import many scene options from the template scene, including the sun position, fog and water configuration. diff --git a/banner.png b/banner.png index 6356abe..e8f287c 100644 Binary files a/banner.png and b/banner.png differ diff --git a/pom.xml b/pom.xml index 7715804..0ff0ba1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 de.lemaik.chunkymap ChunkyMap - 2.5.2 + 2.6.0-pre3 @@ -19,7 +17,7 @@ 1.8 UTF-8 UTF-8 - 2.3.0-30-g82f6ab17 + 2.3.0-32-g9623108f @@ -64,16 +62,22 @@ 3.2 - us.dynmap DynmapCore - 2.3 + 3.2.1 provided + + + + javax.servlet + javax.servlet-api + + com.squareup.okhttp3 okhttp - 3.4.1 + 3.14.9 com.google.code.gson @@ -174,4 +178,4 @@ - + \ No newline at end of file diff --git a/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java b/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java index 3e0815f..0df0433 100644 --- a/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java +++ b/src/main/java/de/lemaik/chunkymap/ChunkyMapPlugin.java @@ -19,6 +19,7 @@ package de.lemaik.chunkymap; +import de.lemaik.chunkymap.rendering.local.ChunkyLogAdapter; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -27,12 +28,19 @@ import java.util.logging.Level; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; +import se.llbit.log.Log; /** * The main class. */ public class ChunkyMapPlugin extends JavaPlugin { + @Override + public void onLoad() { + Log.setReceiver(new ChunkyLogAdapter(getLogger()), se.llbit.log.Level.ERROR, + se.llbit.log.Level.WARNING, se.llbit.log.Level.INFO); + } + @Override public void onEnable() { Plugin dynmap = getServer().getPluginManager().getPlugin("dynmap"); diff --git a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java index 4219f72..278bdc3 100644 --- a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java +++ b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMap.java @@ -3,6 +3,7 @@ import de.lemaik.chunkymap.ChunkyMapPlugin; import de.lemaik.chunkymap.rendering.Renderer; import de.lemaik.chunkymap.rendering.local.ChunkyRenderer; +import de.lemaik.chunkymap.rendering.rs.RemoteRenderer; import de.lemaik.chunkymap.util.MinecraftDownloader; import java.io.File; import java.io.FileInputStream; @@ -14,16 +15,17 @@ import java.util.logging.Level; import java.util.stream.Collectors; import okhttp3.Response; +import okhttp3.ResponseBody; import okio.BufferedSink; import okio.Okio; import org.bukkit.Bukkit; import org.dynmap.ConfigurationNode; -import org.dynmap.DynmapChunk; import org.dynmap.DynmapCore; import org.dynmap.DynmapWorld; import org.dynmap.MapTile; import org.dynmap.MapType; import org.dynmap.hdmap.HDMap; +import org.dynmap.hdmap.HDPerspective; import org.dynmap.hdmap.IsoHDPerspective; import org.dynmap.utils.TileFlags; import se.llbit.chunky.renderer.scene.Scene; @@ -41,33 +43,52 @@ public class ChunkyMap extends HDMap { private final Renderer renderer; private File defaultTexturepackPath; private File texturepackPath; + private File worldPath; + private final Object worldPathLock = new Object(); private JsonObject templateScene; - private int chunkPadding; + private final int chunkPadding; + private final boolean requeueFailedTiles; public ChunkyMap(DynmapCore dynmap, ConfigurationNode config) { super(dynmap, config); cameraAdapter = new DynmapCameraAdapter((IsoHDPerspective) getPerspective()); - renderer = new ChunkyRenderer( - config.getInteger("samplesPerPixel", 100), - config.getBoolean("denoiser/enabled", false), - config.getInteger("denoiser/albedoSamplesPerPixel", 16), - config.getInteger("denoiser/normalSamplesPerPixel", 16), - config.getInteger("chunkyThreads", 2), - Math.min(100, Math.max(0, config.getInteger("chunkyCpuLoad", 100))) - ); + if (config.getBoolean("chunkycloud/enabled", false)) { + renderer = new RemoteRenderer(config.getString("chunkycloud/apiKey", ""), + config.getInteger("samplesPerPixel", 100), + config.getString("texturepack", null), + config.getBoolean("chunkycloud/initializeLocally", true)); + if (config.getString("chunkycloud/apiKey", "").isEmpty()) { + ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger() + .warning("No ChunkyCloud API Key configured."); + } + } else { + renderer = new ChunkyRenderer( + config.getInteger("samplesPerPixel", 100), + config.getBoolean("denoiser/enabled", false), + config.getInteger("denoiser/albedoSamplesPerPixel", 16), + config.getInteger("denoiser/normalSamplesPerPixel", 16), + config.getInteger("chunkyThreads", 2), + Math.min(100, Math.max(0, config.getInteger("chunkyCpuLoad", 100))) + ); + } chunkPadding = config.getInteger("chunkPadding", 0); + requeueFailedTiles = config.getBoolean("requeueFailedTiles", true); String texturepackVersion = config.getString("texturepackVersion", DEFAULT_TEXTUREPACK_VERSION); File texturepackPath = new File( ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getDataFolder(), texturepackVersion + ".jar"); - if (!texturepackPath.exists()) { + if (texturepackPath.exists()) { + defaultTexturepackPath = texturepackPath; + } else { ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger() .info("Downloading additional textures for Minecraft " + texturepackVersion); - try (Response response = MinecraftDownloader.downloadMinecraft(texturepackVersion).get()) { - try (BufferedSink sink = Okio.buffer(Okio.sink(texturepackPath))) { - sink.writeAll(response.body().source()); - } + try ( + Response response = MinecraftDownloader.downloadMinecraft(texturepackVersion).get(); + ResponseBody body = response.body(); + BufferedSink sink = Okio.buffer(Okio.sink(texturepackPath)) + ) { + sink.writeAll(body.source()); defaultTexturepackPath = texturepackPath; } catch (IOException | ExecutionException | InterruptedException e) { ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger() @@ -77,7 +98,7 @@ public ChunkyMap(DynmapCore dynmap, ConfigurationNode config) { } if (config.containsKey("texturepack")) { - texturepackPath = Bukkit.getPluginManager().getPlugin("dynmap").getDataFolder().toPath() + this.texturepackPath = Bukkit.getPluginManager().getPlugin("dynmap").getDataFolder().toPath() .resolve(config.getString("texturepack")) .toFile(); } else { @@ -103,19 +124,11 @@ public ChunkyMap(DynmapCore dynmap, ConfigurationNode config) { .log(Level.SEVERE, "Could not read the template scene.", e); } } - - // texturepacks in chunky are static, so only load them once - if (defaultTexturepackPath != null) { - ChunkyRenderer.loadDefaultTexturepack(defaultTexturepackPath); - } - if (texturepackPath != null) { - ChunkyRenderer.loadTexturepack(texturepackPath); - } } @Override public void addMapTiles(List list, DynmapWorld world, int tx, int ty) { - list.add(new ChunkyMapTile(world, getPerspective(), this, tx, ty)); + list.add(new ChunkyMapTile(world, getPerspective(), tx, ty, getBoostZoom())); } public List getTileCoords(DynmapWorld world, int x, int y, int z) { @@ -129,25 +142,24 @@ public List getTileCoords(DynmapWorld world, int minx, int @Override public MapTile[] getAdjecentTiles(MapTile tile) { + return getAdjecentTilesOfTile(tile, getPerspective()); + } + + public static MapTile[] getAdjecentTilesOfTile(MapTile tile, HDPerspective perspective) { ChunkyMapTile t = (ChunkyMapTile) tile; DynmapWorld w = t.getDynmapWorld(); int x = t.tileOrdinalX(); int y = t.tileOrdinalY(); return new MapTile[]{ - new ChunkyMapTile(w, getPerspective(), this, x - 1, y - 1), - new ChunkyMapTile(w, getPerspective(), this, x - 1, y + 1), - new ChunkyMapTile(w, getPerspective(), this, x + 1, y - 1), - new ChunkyMapTile(w, getPerspective(), this, x + 1, y + 1), - new ChunkyMapTile(w, getPerspective(), this, x, y - 1), - new ChunkyMapTile(w, getPerspective(), this, x + 1, y), - new ChunkyMapTile(w, getPerspective(), this, x, y + 1), - new ChunkyMapTile(w, getPerspective(), this, x - 1, y)}; - } - - @Override - public List getRequiredChunks(MapTile mapTile) { - return getPerspective().getRequiredChunks(mapTile); + new ChunkyMapTile(w, perspective, x - 1, y - 1, t.boostzoom), + new ChunkyMapTile(w, perspective, x - 1, y + 1, t.boostzoom), + new ChunkyMapTile(w, perspective, x + 1, y - 1, t.boostzoom), + new ChunkyMapTile(w, perspective, x + 1, y + 1, t.boostzoom), + new ChunkyMapTile(w, perspective, x, y - 1, t.boostzoom), + new ChunkyMapTile(w, perspective, x + 1, y, t.boostzoom), + new ChunkyMapTile(w, perspective, x, y + 1, t.boostzoom), + new ChunkyMapTile(w, perspective, x - 1, y, t.boostzoom)}; } @Override @@ -187,9 +199,23 @@ int getChunkPadding() { return chunkPadding; } + public boolean getRequeueFailedTiles() { + return requeueFailedTiles; + } + void applyTemplateScene(Scene scene) { if (this.templateScene != null) { scene.importFromJson(templateScene); } } + + File getWorldFolder(DynmapWorld world) { + if (worldPath == null) { + // Fixes a ConcurrentModificationException, see https://github.com/leMaik/ChunkyMap/issues/30 + synchronized (worldPathLock) { + worldPath = Bukkit.getWorld(world.getRawName()).getWorldFolder(); + } + } + return worldPath; + } } diff --git a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java index 60834b2..ba3d20e 100644 --- a/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java +++ b/src/main/java/de/lemaik/chunkymap/dynmap/ChunkyMapTile.java @@ -4,15 +4,18 @@ import de.lemaik.chunkymap.rendering.FileBufferRenderContext; import de.lemaik.chunkymap.rendering.Renderer; import de.lemaik.chunkymap.rendering.SilentTaskTracker; +import de.lemaik.chunkymap.rendering.rs.RemoteRenderer; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.logging.Level; import java.util.stream.Collectors; import org.bukkit.Bukkit; import org.bukkit.World.Environment; -import org.dynmap.Client; -import org.dynmap.DynmapChunk; +import org.dynmap.Client.Tile; import org.dynmap.DynmapWorld; import org.dynmap.MapManager; import org.dynmap.MapTile; @@ -25,29 +28,30 @@ import org.dynmap.storage.MapStorage; import org.dynmap.storage.MapStorageTile; import org.dynmap.utils.MapChunkCache; +import se.llbit.chunky.entity.PlayerEntity; +import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.world.ChunkPosition; import se.llbit.chunky.world.World; import se.llbit.chunky.world.World.LoggedWarnings; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; public class ChunkyMapTile extends HDMapTile { - private final ChunkyMap map; - - public ChunkyMapTile(DynmapWorld world, HDPerspective perspective, ChunkyMap map, int tx, - int ty) { - super(world, perspective, tx, ty, map.getBoostZoom()); - this.map = map; + public ChunkyMapTile(DynmapWorld world, HDPerspective perspective, int tx, int ty, + int boostzoom) { + super(world, perspective, tx, ty, boostzoom); } public ChunkyMapTile(DynmapWorld world, String parm) throws Exception { // Do not remove this constructor! It is used by Dynmap to de-serialize tiles from the queue. // The serialization happens in the inherited saveTileData() method. super(world, parm); - map = (ChunkyMap) world.maps.stream().filter(m -> m instanceof ChunkyMap).findFirst().get(); } @Override - public boolean render(MapChunkCache mapChunkCache, String s) { + public boolean render(MapChunkCache mapChunkCache, String mapName) { + final long startTimestamp = System.currentTimeMillis(); IsoHDPerspective perspective = (IsoHDPerspective) this.perspective; final int scaled = (boostzoom > 0 && MarkerAPIImpl @@ -55,6 +59,11 @@ public boolean render(MapChunkCache mapChunkCache, String s) { 128.0D)) ? boostzoom : 0; // Mark the tiles we're going to render as validated + ChunkyMap map = (ChunkyMap) world.maps.stream() + .filter(m -> m instanceof ChunkyMap && (mapName == null || m.getName().equals(mapName)) + && ((ChunkyMap) m).getPerspective() == perspective + && ((ChunkyMap) m).getBoostZoom() == boostzoom) + .findFirst().get(); MapTypeState mts = world.getMapState(map); if (mts != null) { mts.validateTile(tx, ty); @@ -66,7 +75,7 @@ public boolean render(MapChunkCache mapChunkCache, String s) { renderer.setDefaultTexturepack(map.getDefaultTexturepackPath()); renderer.render(context, map.getTexturepackPath(), (scene) -> { org.bukkit.World bukkitWorld = Bukkit.getWorld(world.getRawName()); - World chunkyWorld = World.loadWorld(bukkitWorld.getWorldFolder(), + World chunkyWorld = World.loadWorld(map.getWorldFolder(world), getChunkyDimension(bukkitWorld.getEnvironment()), LoggedWarnings.SILENT); // Bukkit.getScheduler().runTask(ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class), Bukkit.getWorld(world.getRawName())::save); map.applyTemplateScene(scene); @@ -80,28 +89,88 @@ public boolean render(MapChunkCache mapChunkCache, String s) { map.cameraAdapter.apply(scene.camera(), tx, ty, map.getMapZoomOutLevels(), world.getExtraZoomOutLevels()); - scene.loadChunks(SilentTaskTracker.INSTANCE, chunkyWorld, - perspective.getRequiredChunks(this).stream() - .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream()) - .collect(Collectors.toList())); + if (renderer instanceof RemoteRenderer) { + if (((RemoteRenderer) renderer).shouldInitializeLocally()) { + scene.loadChunks(SilentTaskTracker.INSTANCE, chunkyWorld, + perspective.getRequiredChunks(this).stream() + .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream()) + .collect(Collectors.toSet())); + scene.getActors().removeIf(actor -> actor instanceof PlayerEntity); + try { + scene.saveScene(context, new TaskTracker(ProgressListener.NONE)); + } catch (IOException e) { + throw new RuntimeException("Could not save scene", e); + } + } else { + try { + Field chunks = Scene.class.getDeclaredField("chunks"); + chunks.setAccessible(true); + Collection chunksList = (Collection) chunks.get(scene); + chunksList.clear(); + chunksList.addAll(perspective.getRequiredChunks(this).stream() + .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream()) + .collect(Collectors.toSet())); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Could not set chunks", e); + } + try { + Field worldPath = Scene.class.getDeclaredField("worldPath"); + worldPath.setAccessible(true); + worldPath.set(scene, map.getWorldFolder(world).getAbsolutePath()); + Field worldDimension = Scene.class.getDeclaredField("worldDimension"); + worldDimension.setAccessible(true); + worldDimension.setInt(scene, bukkitWorld.getEnvironment().getId()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Could not set world", e); + } + try { + scene.saveDescription(context.getSceneDescriptionOutputStream(scene.name)); + } catch (IOException e) { + throw new RuntimeException("Could not save scene", e); + } + } + } else { + scene.loadChunks(SilentTaskTracker.INSTANCE, chunkyWorld, + perspective.getRequiredChunks(this).stream() + .flatMap(c -> getChunksAround(c.x, c.z, map.getChunkPadding()).stream()) + .collect(Collectors.toSet())); + } }).thenApply((image) -> { MapStorage var52 = world.getMapStorage(); MapStorageTile mtile = var52.getTile(world, map, tx, ty, 0, ImageVariant.STANDARD); - try { + MapManager mapManager = MapManager.mapman; + boolean tileUpdated = false; + if (mapManager != null) { + DataBufferInt dataBuffer = (DataBufferInt) image.getRaster().getDataBuffer(); + int[] data = dataBuffer.getData(); + long crc = MapStorage.calculateImageHashCode(data, 0, data.length); mtile.getWriteLock(); - mtile.write(image.hashCode(), image); - MapManager.mapman.pushUpdate(getDynmapWorld(), new Client.Tile(mtile.getURI())); - } finally { - mtile.releaseWriteLock(); - MapManager.mapman.updateStatistics(this, map.getPrefix(), true, true, false); + try { + if (!mtile.matchesHashCode(crc)) { + mtile.write(crc, image, startTimestamp); + mapManager.pushUpdate(getDynmapWorld(), new Tile(mtile.getURI())); + tileUpdated = true; + } + } finally { + mtile.releaseWriteLock(); + } + mapManager.updateStatistics(this, map.getPrefix(), true, true, false); } - return true; + return tileUpdated; }).get(); return true; } catch (Exception e) { ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getLogger() - .log(Level.WARNING, "Rendering tile failed", e); + .log(Level.WARNING, "Rendering tile " + tx + "_" + ty + " failed", e); + + if (map.getRequeueFailedTiles()) { + // Re-queue the failed tile + // Somewhat hacky but works surprisingly well + MapManager.mapman.tileQueue.push(this); + } return false; + } finally { + context.dispose(); } } @@ -127,14 +196,9 @@ private static Collection getChunksAround(int centerX, int center return chunks; } - @Override - public List getRequiredChunks() { - return map.getRequiredChunks(this); - } - @Override public MapTile[] getAdjecentTiles() { - return map.getAdjecentTiles(this); + return ChunkyMap.getAdjecentTilesOfTile(this, perspective); } public int hashCode() { diff --git a/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java b/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java index b2d5fe1..c3402b9 100644 --- a/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java +++ b/src/main/java/de/lemaik/chunkymap/rendering/FileBufferRenderContext.java @@ -3,7 +3,6 @@ import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; -import java.io.IOException; import java.io.OutputStream; import se.llbit.chunky.main.Chunky; import se.llbit.chunky.main.ChunkyOptions; @@ -16,8 +15,6 @@ public class FileBufferRenderContext extends RenderContext { private ByteArrayOutputStream scene; - private ByteArrayOutputStream grass; - private ByteArrayOutputStream foliage; private ByteArrayOutputStream octree; public FileBufferRenderContext() { @@ -28,16 +25,12 @@ public FileBufferRenderContext() { public OutputStream getSceneFileOutputStream(String fileName) throws FileNotFoundException { if (fileName.endsWith(".json")) { return scene = new ByteArrayOutputStream(); - } else if (fileName.endsWith(".grass")) { - return grass = new ByteArrayOutputStream(); - } else if (fileName.endsWith(".foliage")) { - return foliage = new ByteArrayOutputStream(); - } else if (fileName.endsWith(".octree")) { + } else if (fileName.endsWith(".octree2")) { return octree = new ByteArrayOutputStream(); } else { return new OutputStream() { @Override - public void write(int b) throws IOException { + public void write(int b) { // no-op } }; @@ -48,14 +41,6 @@ public byte[] getScene() { return scene.toByteArray(); } - public byte[] getGrass() { - return grass.toByteArray(); - } - - public byte[] getFoliage() { - return foliage.toByteArray(); - } - public byte[] getOctree() { return octree.toByteArray(); } @@ -63,4 +48,9 @@ public byte[] getOctree() { public void setRenderThreadCount(int threads) { config.renderThreads = threads; } + + public void dispose() { + scene = null; + octree = null; + } } diff --git a/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java b/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java index 3d101ce..b970e61 100644 --- a/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java +++ b/src/main/java/de/lemaik/chunkymap/rendering/RenderException.java @@ -8,4 +8,8 @@ public class RenderException extends Exception { public RenderException(String message, Throwable inner) { super(message, inner); } + + public RenderException(String message) { + super(message); + } } diff --git a/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java b/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java index e6c4639..7bdabe1 100644 --- a/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java +++ b/src/main/java/de/lemaik/chunkymap/rendering/Renderer.java @@ -2,6 +2,7 @@ import java.awt.image.BufferedImage; import java.io.File; +import java.io.IOException; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import se.llbit.chunky.renderer.scene.Scene; @@ -20,7 +21,8 @@ public interface Renderer { * @return future with the rendered image */ CompletableFuture render(FileBufferRenderContext context, File texturepack, - Consumer initializeScene); + Consumer initializeScene) + throws IOException; /** * set the default / fallback texturepack to use diff --git a/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyLogAdapter.java b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyLogAdapter.java new file mode 100644 index 0000000..c70507e --- /dev/null +++ b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyLogAdapter.java @@ -0,0 +1,85 @@ +package de.lemaik.chunkymap.rendering.local; + +import java.util.logging.Logger; +import se.llbit.log.Level; +import se.llbit.log.Receiver; + +/** + * Adapter for Chunky's logger that redirects log messages to the plugin's logger and suppresses + * known false-positive warnings. + */ +public class ChunkyLogAdapter extends Receiver { + + private Logger logger; + + public ChunkyLogAdapter(Logger logger) { + this.logger = logger; + } + + @Override + public void logEvent(Level level, String message) { + if (this.shouldIgnoreMessage(level, message, null)) { + return; + } + switch (level) { + case ERROR: + logger.severe(message); + break; + case WARNING: + logger.warning(message); + break; + case INFO: + default: + logger.info(message); + break; + } + } + + @Override + public void logEvent(Level level, String message, Throwable thrown) { + if (this.shouldIgnoreMessage(level, message, thrown)) { + return; + } + switch (level) { + case ERROR: + logger.log(java.util.logging.Level.SEVERE, message, thrown); + break; + case WARNING: + logger.log(java.util.logging.Level.WARNING, message, thrown); + break; + case INFO: + default: + logger.log(java.util.logging.Level.INFO, message, thrown); + break; + } + } + + @Override + public void logEvent(Level level, Throwable thrown) { + if (this.shouldIgnoreMessage(level, null, thrown)) { + return; + } + switch (level) { + case ERROR: + logger.log(java.util.logging.Level.SEVERE, thrown.getMessage(), thrown); + break; + case WARNING: + logger.log(java.util.logging.Level.WARNING, thrown.getMessage(), thrown); + break; + case INFO: + default: + logger.log(java.util.logging.Level.INFO, thrown.getMessage(), thrown); + } + } + + protected boolean shouldIgnoreMessage(Level level, String message, Throwable thrown) { + if (message == null) { + return false; + } + if (message.startsWith("Warning: Could not load settings from")) { + // this is intended + return true; + } + return false; + } +} diff --git a/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java index 363d1a3..b9373d6 100644 --- a/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java +++ b/src/main/java/de/lemaik/chunkymap/rendering/local/ChunkyRenderer.java @@ -39,8 +39,8 @@ */ public class ChunkyRenderer implements Renderer { - private static File previousTexturepack; - private static File previousDefaultTexturepack; + private static String previousTexturepacks; + private File defaultTexturepack; private final int targetSpp; private final boolean enableDenoiser; private final int albedoTargetSpp; @@ -60,27 +60,26 @@ public ChunkyRenderer(int targetSpp, boolean enableDenoiser, int albedoTargetSpp PersistentSettings.changeSettingsDirectory( new File(ChunkyMapPlugin.getPlugin(ChunkyMapPlugin.class).getDataFolder(), "chunky")); PersistentSettings.setLoadPlayers(false); + PersistentSettings.setDisableDefaultTextures(true); } @Override public void setDefaultTexturepack(File texturepack) { - // no-op, textures are static in Chunky and were already loaded + defaultTexturepack = texturepack; } - public static void loadTexturepack(File texturepack) { - if (!texturepack.equals(previousTexturepack)) { - // this means that only one texturepack can be used for all maps, if rendering with multiple chunky instances - TexturePackLoader.loadTexturePacks(texturepack.getAbsolutePath(), false); - previousTexturepack = texturepack; - } - } - - public static void loadDefaultTexturepack(File texturepack) { - if (!texturepack.equals(previousDefaultTexturepack)) { - // this means that only one texturepack can be used for all maps, if rendering with multiple chunky instances - TexturePackLoader.loadTexturePacks(texturepack.getAbsolutePath(), false); - previousDefaultTexturepack = texturepack; + private String getTexturepackPaths(File texturepack) { + if (defaultTexturepack != null) { + if (texturepack != null) { + return texturepack.getAbsolutePath() + File.pathSeparator + defaultTexturepack + .getAbsolutePath(); + } else { + return defaultTexturepack.getAbsolutePath(); + } + } else if (texturepack != null) { + return texturepack.getAbsolutePath(); } + return ""; } @Override @@ -88,6 +87,12 @@ public CompletableFuture render(FileBufferRenderContext context, Consumer initializeScene) { CompletableFuture result = new CompletableFuture<>(); + String texturepackPaths = this.getTexturepackPaths(texturepack); + if (!texturepackPaths.equals(previousTexturepacks)) { + TexturePackLoader.loadTexturePacks(texturepackPaths, false); + previousTexturepacks = texturepackPaths; + } + CombinedRayTracer combinedRayTracer = new CombinedRayTracer(); context.getChunky().setRayTracerFactory(() -> combinedRayTracer); context.setRenderThreadCount(threads); diff --git a/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java b/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java index 50b95b9..47d6d24 100644 --- a/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java +++ b/src/main/java/de/lemaik/chunkymap/rendering/rs/ApiClient.java @@ -20,13 +20,23 @@ package de.lemaik.chunkymap.rendering.rs; import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import de.lemaik.chunkymap.rendering.RenderException; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; +import java.io.Reader; import java.net.URL; +import java.util.List; +import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import java.util.stream.Collectors; import javax.imageio.ImageIO; import okhttp3.Call; import okhttp3.Callback; @@ -36,7 +46,7 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import okio.Buffer; +import okhttp3.ResponseBody; import okio.BufferedSink; import okio.Okio; import okio.Source; @@ -48,32 +58,42 @@ public class ApiClient { private final String baseUrl; private final OkHttpClient client; - public ApiClient(String baseUrl) { + public ApiClient(String baseUrl, String apiKey) { this.baseUrl = baseUrl; - client = new OkHttpClient.Builder().build(); + client = new OkHttpClient.Builder() + .addInterceptor(chain -> chain.proceed( + chain.request().newBuilder() + .header("X-Api-Key", apiKey) + .header("User-Agent", "ChunkyMap") + .build())) + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.MINUTES) + .readTimeout(10, TimeUnit.SECONDS) + .build(); } - public CompletableFuture createJob(byte[] scene, byte[] octree, byte[] grass, - byte[] foliage, byte[] skymap, String skymapName, TaskTracker taskTracker) { + public CompletableFuture createJob(byte[] scene, byte[] octree, byte[] skymap, + String skymapName, String texturepack, int targetSpp, TaskTracker taskTracker) { CompletableFuture result = new CompletableFuture<>(); MultipartBody.Builder multipartBuilder = new MultipartBody.Builder() .setType(MediaType.parse("multipart/form-data")) - .addFormDataPart("foliage", "scene.foliage", - byteBody(foliage, () -> taskTracker.task("Upload foliage..."))) - .addFormDataPart("grass", "scene.grass", - byteBody(grass, () -> taskTracker.task("Upload task..."))) .addFormDataPart("scene", "scene.json", byteBody(scene, () -> taskTracker.task("Upload scene..."))) - .addFormDataPart("octree", "scene.octree", + .addFormDataPart("octree", "scene.octree2", byteBody(octree, () -> taskTracker.task("Upload octree..."))) - .addFormDataPart("targetSpp", "100"); + .addFormDataPart("targetSpp", "" + targetSpp) + .addFormDataPart("transient", "true"); if (skymap != null) { multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymapName, byteBody(skymap, () -> taskTracker.task("Upload skymap..."))); } + if (texturepack != null) { + multipartBuilder = multipartBuilder.addFormDataPart("texturepack", texturepack); + } + client.newCall(new Request.Builder() .url(baseUrl + "/jobs") .post(multipartBuilder.build()) @@ -85,15 +105,23 @@ public void onFailure(Call call, IOException e) { } @Override - public void onResponse(Call call, Response response) throws IOException { - if (response.code() == 201) { - try (InputStreamReader reader = new InputStreamReader(response.body().byteStream())) { - result.complete(gson.fromJson(reader, RenderJob.class)); - } catch (IOException e) { - result.completeExceptionally(e); + public void onResponse(Call call, Response response) { + try { + if (response.code() == 201) { + try ( + ResponseBody body = response.body(); + Reader reader = body.charStream() + ) { + result.complete(gson.fromJson(reader, RenderJob.class)); + } catch (IOException e) { + result.completeExceptionally(e); + } + } else { + result + .completeExceptionally(new IOException("The render job could not be created")); } - } else { - result.completeExceptionally(new IOException("The render job could not be created")); + } finally { + response.close(); } } }); @@ -101,25 +129,104 @@ public void onResponse(Call call, Response response) throws IOException { return result; } - public CompletableFuture createJob(File scene, File octree, File grass, File foliage, - File skymap, TaskTracker taskTracker) throws IOException { + private CompletableFuture createJob(byte[] scene, List regionFiles, + JsonObject cachedRegions, byte[] skymap, + String skymapName, String texturepack, int targetSpp, TaskTracker taskTracker) { CompletableFuture result = new CompletableFuture<>(); + JsonObject regions = new JsonObject(); + for (Entry entry : cachedRegions.entrySet()) { + if (regionFiles.stream().noneMatch(file -> file.getName().equals(entry.getKey()))) { + // not submitted as file + regions.add(entry.getKey(), entry.getValue()); + } + } + MultipartBody.Builder multipartBuilder = new MultipartBody.Builder() .setType(MediaType.parse("multipart/form-data")) - .addFormDataPart("foliage", "scene.foliage", - fileBody(foliage, () -> taskTracker.task("Upload foliage..."))) - .addFormDataPart("grass", "scene.grass", - fileBody(grass, () -> taskTracker.task("Upload task..."))) .addFormDataPart("scene", "scene.json", - fileBody(scene, () -> taskTracker.task("Upload scene..."))) - .addFormDataPart("octree", "scene.octree", - fileBody(octree, () -> taskTracker.task("Upload octree..."))) - .addFormDataPart("targetSpp", "100"); + byteBody(scene, () -> taskTracker.task("Upload scene..."))) + .addFormDataPart("targetSpp", "" + targetSpp) + .addFormDataPart("transient", "true") + .addFormDataPart("cachedRegions", regions.toString()); + + for (File region : regionFiles) { + multipartBuilder = multipartBuilder.addFormDataPart("region", region.getName(), + fileBody(region, () -> taskTracker.task("Upload region " + region.getName()))); + } if (skymap != null) { - multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymap.getName(), - fileBody(skymap, () -> taskTracker.task("Upload skymap..."))); + multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymapName, + byteBody(skymap, () -> taskTracker.task("Upload skymap..."))); + } + + if (texturepack != null) { + multipartBuilder = multipartBuilder.addFormDataPart("texturepack", texturepack); + } + + client.newCall(new Request.Builder() + .url(baseUrl + "/jobs") + .post(multipartBuilder.build()) + .build()) + .enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + result.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, Response response) { + try { + if (response.code() == 201) { + try ( + ResponseBody body = response.body(); + Reader reader = body.charStream() + ) { + result.complete(gson.fromJson(reader, RenderJob.class)); + } catch (IOException e) { + result.completeExceptionally(e); + } + } else { + result.completeExceptionally( + new IOException( + "The render job could not be created: " + response.code() + " " + response + .message())); + } + } finally { + response.close(); + } + } + }); + + return result; + } + + public CompletableFuture createJob(byte[] scene, List regionFiles, byte[] skymap, + String skymapName, String texturepack, int targetSpp, TaskTracker taskTracker) + throws IOException { + CompletableFuture result = new CompletableFuture<>(); + + JsonObject regions = new JsonObject(); + for (File region : regionFiles) { + regions.addProperty(region.getName(), + Okio.buffer(Okio.source(region)).readByteString().md5().hex()); + } + + MultipartBody.Builder multipartBuilder = new MultipartBody.Builder() + .setType(MediaType.parse("multipart/form-data")) + .addFormDataPart("scene", "scene.json", + byteBody(scene, () -> taskTracker.task("Upload scene..."))) + .addFormDataPart("targetSpp", "" + targetSpp) + .addFormDataPart("transient", "true") + .addFormDataPart("cachedRegions", regions.toString()); + + if (skymap != null) { + multipartBuilder = multipartBuilder.addFormDataPart("skymap", skymapName, + byteBody(skymap, () -> taskTracker.task("Upload skymap..."))); + } + + if (texturepack != null) { + multipartBuilder = multipartBuilder.addFormDataPart("texturepack", texturepack); } client.newCall(new Request.Builder() @@ -134,14 +241,63 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) throws IOException { - if (response.code() == 201) { - try (InputStreamReader reader = new InputStreamReader(response.body().byteStream())) { - result.complete(gson.fromJson(reader, RenderJob.class)); - } catch (IOException e) { - result.completeExceptionally(e); + try { + if (response.code() == 201) { + try ( + ResponseBody body = response.body(); + Reader reader = body.charStream() + ) { + result.complete(gson.fromJson(reader, RenderJob.class)); + } catch (IOException e) { + result.completeExceptionally(e); + } + } else if (response.code() == 400) { + try ( + ResponseBody body = response.body(); + Reader reader = body.charStream() + ) { + JsonObject obj = gson.fromJson(reader, JsonObject.class); + if (obj.has("missing")) { + try { + ApiClient.this.createJob(scene, regionFiles.stream().filter( + file -> obj.getAsJsonArray("missing") + .contains(new JsonPrimitive(file.getName()))).collect( + Collectors.toList()), regions, skymap, skymapName, texturepack, targetSpp, + taskTracker).whenComplete((job, ex) -> { + if (ex == null) { + result.complete(job); + } else { + result.completeExceptionally(ex); + } + }); + } catch (Exception e) { + result.completeExceptionally(e); + } + } else { + result.completeExceptionally( + new IOException( + "The render job could not be created: " + response.code() + " " + + response.message() + " " + obj.toString())); + } + } catch (JsonParseException e) { + result.completeExceptionally(e); + } + } else { + String responseBody = ""; + ResponseBody body = response.body(); + if (body != null) { + try { + responseBody = body.string(); + } catch (IOException e) { + } + } + result.completeExceptionally( + new IOException( + "The render job could not be created: " + response.code() + " " + + response.message() + " " + responseBody)); } - } else { - result.completeExceptionally(new IOException("The render job could not be created")); + } finally { + response.close(); } } }); @@ -149,18 +305,26 @@ public void onResponse(Call call, Response response) throws IOException { return result; } - public CompletableFuture waitForCompletion(RenderJob renderJob) { + public CompletableFuture waitForCompletion(RenderJob renderJob, long timeout, + TimeUnit unit) { if (renderJob.getSpp() >= renderJob.getTargetSpp()) { // job is already completed return CompletableFuture.completedFuture(renderJob); } + final long then = System.currentTimeMillis(); CompletableFuture completedJob = new CompletableFuture<>(); new Thread(() -> { RenderJob current = renderJob; try { while (current.getSpp() < current.getTargetSpp()) { - Thread.sleep(10_000); + if (then + unit.toMillis(timeout) < System.currentTimeMillis()) { + completedJob + .completeExceptionally( + new RenderException("Timeout after " + unit.toMillis(timeout) + " ms")); + return; + } + Thread.sleep(500); current = getJob(current.getId()).get(); } completedJob.complete(current); @@ -184,14 +348,23 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) { - if (response.code() == 200) { - try (InputStreamReader reader = new InputStreamReader(response.body().byteStream())) { - result.complete(gson.fromJson(reader, RenderJob.class)); - } catch (IOException e) { - result.completeExceptionally(e); + try { + if (response.code() == 200) { + try ( + ResponseBody body = response.body(); + Reader reader = body.charStream() + ) { + result.complete(gson.fromJson(reader, RenderJob.class)); + } catch (IOException e) { + result.completeExceptionally(e); + } + } else { + result.completeExceptionally(new IOException( + "The job could not be downloaded " + response.code() + " " + response + .message())); } - } else { - result.completeExceptionally(new IOException("The job could not be downloaded")); + } finally { + response.close(); } } }); @@ -199,6 +372,35 @@ public void onResponse(Call call, Response response) { return result; } + public CompletableFuture cancelJob(String jobId) { + CompletableFuture result = new CompletableFuture<>(); + client.newCall(new Request.Builder() + .url(baseUrl + "/jobs/" + jobId) + .patch(new MultipartBody.Builder().addFormDataPart("action", "cancel").build()).build()) + .enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + result.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, Response response) { + try { + if (response.code() == 204) { + result.complete(null); + } else { + result.completeExceptionally(new IOException( + "The job could not be downloaded " + response.code() + " " + response + .message())); + } + } finally { + response.close(); + } + } + }); + return result; + } + private static RequestBody fileBody(final File file, Supplier taskCreator) { TaskTracker.Task task = taskCreator.get(); return new RequestBody() { @@ -214,19 +416,13 @@ public long contentLength() { @Override public void writeTo(BufferedSink bufferedSink) throws IOException { - Source source = null; - try { - source = Okio.source(file); - //sink.writeAll(source); - Buffer buf = new Buffer(); + try (Source source = Okio.source(file)) { long read = 0; - for (long readCount; (readCount = source.read(buf, 2048)) != -1; ) { - bufferedSink.write(buf, readCount); + for (long readCount; (readCount = source.read(bufferedSink.buffer(), 2048)) != -1; ) { read += readCount; + bufferedSink.flush(); task.update((int) contentLength(), (int) read); } - } catch (Exception e) { - e.printStackTrace(); } task.close(); } @@ -256,6 +452,12 @@ public void writeTo(BufferedSink bufferedSink) throws IOException { } public BufferedImage getPicture(String id) throws IOException { - return ImageIO.read(new URL(baseUrl + "/jobs/" + id + "/latest.png")); + BufferedImage image = ImageIO.read(new URL(baseUrl + "/jobs/" + id + "/latest.png")); + BufferedImage img = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics g = img.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return img; } } diff --git a/src/main/java/de/lemaik/chunkymap/rendering/rs/RemoteRenderer.java b/src/main/java/de/lemaik/chunkymap/rendering/rs/RemoteRenderer.java new file mode 100644 index 0000000..2a7d0a2 --- /dev/null +++ b/src/main/java/de/lemaik/chunkymap/rendering/rs/RemoteRenderer.java @@ -0,0 +1,91 @@ +package de.lemaik.chunkymap.rendering.rs; + +import de.lemaik.chunkymap.rendering.FileBufferRenderContext; +import de.lemaik.chunkymap.rendering.Renderer; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import se.llbit.chunky.renderer.scene.Scene; +import se.llbit.chunky.world.ChunkPosition; +import se.llbit.util.ProgressListener; +import se.llbit.util.TaskTracker; + +public class RemoteRenderer implements Renderer { + + private final ApiClient api; + private final int samplesPerPixel; + private final String texturepack; + private final boolean initializeLocally; + + public RemoteRenderer(String apiKey, int samplesPerPixel, String texturepack, + boolean initializeLocally) { + this.samplesPerPixel = samplesPerPixel; + this.texturepack = texturepack; + this.initializeLocally = initializeLocally; + this.api = new ApiClient("https://api.chunkycloud.lemaik.de", apiKey); + } + + public boolean shouldInitializeLocally() { + return initializeLocally; + } + + @Override + public CompletableFuture render(FileBufferRenderContext context, File texturepack, + Consumer initializeScene) throws IOException { + Scene scene = context.getChunky().getSceneFactory().newScene(); + initializeScene.accept(scene); + + RenderJob job = null; + try { + if (initializeLocally) { + job = api + .createJob(context.getScene(), context.getOctree(), null, null, + this.texturepack, samplesPerPixel, new TaskTracker(ProgressListener.NONE)).get(); + } else { + job = api.createJob(context.getScene(), scene.getChunks().stream().map( + ChunkPosition::getRegionPosition).collect(Collectors.toSet()).stream() + .map(position -> getRegionFile(scene, position)) + .filter(File::exists) + .collect(Collectors.toList()), null, null, + this.texturepack, samplesPerPixel, new TaskTracker(ProgressListener.NONE)).get(); + } + api.waitForCompletion(job, 10, TimeUnit.MINUTES).get(); + return CompletableFuture.completedFuture(api.getPicture(job.getId())); + } catch (InterruptedException | ExecutionException e) { + if (job != null) { + try { + api.cancelJob(job.getId()).get(); + } catch (InterruptedException | ExecutionException ignore) { + } + } + throw new IOException("Rendering failed", e); + } + } + + private File getRegionFile(Scene scene, ChunkPosition position) { + try { + Field worldPath = Scene.class.getDeclaredField("worldPath"); + worldPath.setAccessible(true); + Field worldDimension = Scene.class.getDeclaredField("worldDimension"); + worldDimension.setAccessible(true); + File world = new File((String) worldPath.get(scene)); + int dimension = worldDimension.getInt(scene); + File dimWorld = dimension == 0 ? world : new File(world, "DIM" + dimension); + return Paths.get(dimWorld.getAbsolutePath(), "region", position.getMcaName()).toFile(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Could not get region file", e); + } + } + + @Override + public void setDefaultTexturepack(File texturepack) { + // no-op + } +} diff --git a/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java b/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java index 17dccdd..3fa4943 100644 --- a/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java +++ b/src/main/java/de/lemaik/chunkymap/util/MinecraftDownloader.java @@ -4,12 +4,14 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.io.IOException; +import java.io.Reader; import java.util.concurrent.CompletableFuture; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import okhttp3.ResponseBody; /** * A utility class to download Minecraft jars. @@ -55,14 +57,23 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) throws IOException { - JsonObject parsed = new JsonParser().parse(response.body().string()).getAsJsonObject(); - for (JsonElement versionData : parsed.getAsJsonArray("versions")) { - if (versionData.getAsJsonObject().get("id").getAsString().equals(version)) { - result.complete(versionData.getAsJsonObject().get("url").getAsString()); - return; + try ( + ResponseBody body = response.body(); + Reader reader = body.charStream() + ) { + JsonObject parsed = new JsonParser().parse(reader) + .getAsJsonObject(); + for (JsonElement versionData : parsed.getAsJsonArray("versions")) { + if (versionData.getAsJsonObject().get("id").getAsString() + .equals(version)) { + result + .complete(versionData.getAsJsonObject().get("url").getAsString()); + return; + } } + result.completeExceptionally( + new Exception("Version " + version + " not found")); } - result.completeExceptionally(new Exception("Version " + version + " not found")); } }); @@ -81,9 +92,15 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, Response response) throws IOException { - JsonObject parsed = new JsonParser().parse(response.body().string()).getAsJsonObject(); - result.complete(parsed.getAsJsonObject("downloads").getAsJsonObject("client").get("url") - .getAsString()); + try ( + ResponseBody body = response.body(); + Reader reader = body.charStream() + ) { + JsonObject parsed = new JsonParser().parse(reader).getAsJsonObject(); + result.complete( + parsed.getAsJsonObject("downloads").getAsJsonObject("client").get("url") + .getAsString()); + } } }); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 885d342..604bea0 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,3 +2,4 @@ name: ChunkyMap version: ${project.version} main: de.lemaik.chunkymap.ChunkyMapPlugin loadbefore: [dynmap] +softdepend: [dynmap] diff --git a/vendor/chunky b/vendor/chunky index 82f6ab1..9623108 160000 --- a/vendor/chunky +++ b/vendor/chunky @@ -1 +1 @@ -Subproject commit 82f6ab17654141e6628b4cb847115405b8bf61f3 +Subproject commit 9623108f004ea6d5db668ace860e40e3860dd920