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