diff --git a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java index 3b009d28..cec70113 100644 --- a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java +++ b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java @@ -250,7 +250,7 @@ public class BlueMapCLI { throw new IOException("Failed to create temporary resource file!", e); } try { - ResourcePack.createDefaultResource(defaultResourceFile); + ResourcePack.downloadDefaultResource(defaultResourceFile); } catch (IOException e) { throw new IOException("Failed to create default resources!", e); } diff --git a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderManager.java b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderManager.java index 3f0638b3..83477dfa 100644 --- a/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderManager.java +++ b/BlueMapCLI/src/main/java/de/bluecolored/bluemap/cli/RenderManager.java @@ -135,7 +135,7 @@ public class RenderManager extends Thread { String durationString = DurationFormatUtils.formatDurationWords(time, true, true); double pct = (double)renderedTiles / (double)tileCount; - long ert = (long)(((double) time / pct) * (1d - pct)); + long ert = (long)((time / pct) * (1d - pct)); String ertDurationString = DurationFormatUtils.formatDurationWords(ert, true, true); Logger.global.logInfo("Rendered " + renderedTiles + " of " + tileCount + " tiles in " + durationString); diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java index 9bc1ce32..e0092b8f 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/config/ConfigurationFile.java @@ -27,7 +27,10 @@ package de.bluecolored.bluemap.core.config; import java.io.File; import java.io.IOException; import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; @@ -36,6 +39,7 @@ import org.apache.commons.io.FileUtils; import com.google.common.base.Preconditions; import de.bluecolored.bluemap.core.render.RenderSettings; +import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.web.WebServerConfig; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.commented.CommentedConfigurationNode; @@ -46,6 +50,8 @@ public class ConfigurationFile implements WebServerConfig { private String configVersion; + private boolean downloadAccepted; + private boolean webserverEnabled; private int webserverPort; private int webserverMaxConnections; @@ -66,10 +72,11 @@ public class ConfigurationFile implements WebServerConfig { CommentedConfigurationNode rootNode = configLoader.load(); configVersion = rootNode.getNode("version").getString("-"); + downloadAccepted = rootNode.getNode("accept-download").getBoolean(false); loadWebConfig(rootNode.getNode("web")); - int defaultCount = (int) Math.max(Math.min((double) Runtime.getRuntime().availableProcessors() * 0.75, 16), 1); + int defaultCount = (int) Math.max(Math.min(Runtime.getRuntime().availableProcessors() * 0.75, 16), 1); renderThreadCount = rootNode.getNode("renderThreadCount").getInt(defaultCount); if (renderThreadCount <= 0) renderThreadCount = defaultCount; @@ -143,6 +150,10 @@ public class ConfigurationFile implements WebServerConfig { return configVersion; } + public boolean isDownloadAccepted() { + return downloadAccepted; + } + public int getRenderThreadCount() { return renderThreadCount; } @@ -165,6 +176,12 @@ public class ConfigurationFile implements WebServerConfig { configFile.getParentFile().mkdirs(); FileUtils.copyURLToFile(ConfigurationFile.class.getResource("/bluemap.conf"), configFile, 10000, 10000); + + //replace placeholder + String content = new String(Files.readAllBytes(configFile.toPath()), StandardCharsets.UTF_8); + content = content.replaceAll("%resource-file%", ResourcePack.MINECRAFT_CLIENT_URL); + content = content.replaceAll("%date%", LocalDateTime.now().withNano(0).toString()); + Files.write(configFile.toPath(), content.getBytes(StandardCharsets.UTF_8)); } return new ConfigurationFile(configFile); @@ -226,7 +243,7 @@ public class ConfigurationFile implements WebServerConfig { return name; } - public String getWorldId() { + public String getWorldPath() { return world; } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil112.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil112.java index e17ae016..d300457e 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil112.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil112.java @@ -95,7 +95,7 @@ class ChunkAnvil112 extends Chunk { int z = pos.getZ() & 0xF; int biomeByteIndex = z * 16 + x; - return biomeIdMapper.get(biomes[biomeByteIndex]); + return biomeIdMapper.get(biomes[biomeByteIndex] & 0xFF); } private class Section { diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil113.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil113.java index c6c2e2c9..e97056be 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil113.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/ChunkAnvil113.java @@ -74,7 +74,7 @@ class ChunkAnvil113 extends Chunk { biomes = new int[bs.length]; for (int i = 0; i < bs.length; i++) { - biomes[i] = bs[i]; + biomes[i] = bs[i] & 0xFF; } } else if (tag instanceof IntArrayTag) { diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/MCAWorld.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/MCAWorld.java index 5ecc3144..47205c33 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/MCAWorld.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/MCAWorld.java @@ -172,8 +172,10 @@ public class MCAWorld implements World { private BlockState getExtendedBlockState(Chunk chunk, Vector3i pos) throws ChunkNotGeneratedException { BlockState blockState = chunk.getBlockState(pos); - for (BlockStateExtension ext : BLOCK_STATE_EXTENSIONS.get(blockState.getId())) { - blockState = ext.extend(this, pos, blockState); + if (chunk instanceof ChunkAnvil112) { // only use extensions if old format chunk (1.12) in the new format block-states are saved witch extensions + for (BlockStateExtension ext : BLOCK_STATE_EXTENSIONS.get(blockState.getFullId())) { + blockState = ext.extend(this, pos, blockState); + } } return blockState; @@ -314,6 +316,16 @@ public class MCAWorld implements World { return spawnPoint; } + @Override + public void invalidateChunkCache() { + CHUNK_CACHE.invalidateAll(); + } + + @Override + public void invalidateChunkCache(Vector2i chunk) { + CHUNK_CACHE.invalidate(new WorldChunkHash(this, chunk)); + } + public BlockIdMapper getBlockIdMapper() { return blockIdMapper; } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoorExtension.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoorExtension.java index 07aa4051..3c55d110 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoorExtension.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoorExtension.java @@ -37,7 +37,7 @@ import de.bluecolored.bluemap.core.world.BlockState; public class DoorExtension implements BlockStateExtension { private static final Collection AFFECTED_BLOCK_IDS = Lists.newArrayList( - "minecraft:wooden_door", + "minecraft:oak_door", "minecraft:iron_door", "minecraft:spruce_door", "minecraft:birch_door", diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoublePlantExtension.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoublePlantExtension.java index c5203205..79f9ea5b 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoublePlantExtension.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/DoublePlantExtension.java @@ -25,7 +25,6 @@ package de.bluecolored.bluemap.core.mca.extensions; import java.util.Collection; -import java.util.Map.Entry; import com.flowpowered.math.vector.Vector3i; import com.google.common.collect.Lists; @@ -37,7 +36,12 @@ import de.bluecolored.bluemap.core.world.BlockState; public class DoublePlantExtension implements BlockStateExtension { private static final Collection AFFECTED_BLOCK_IDS = Lists.newArrayList( - "minecraft:double_plant" + "minecraft:sunflower", + "minecraft:lilac", + "minecraft:tall_grass", + "minecraft:large_fern", + "minecraft:rose_bush", + "minecraft:peony" ); @Override @@ -45,12 +49,7 @@ public class DoublePlantExtension implements BlockStateExtension { if (state.getProperties().get("half").equals("upper")) { BlockState otherPlant = world.getBlockState(pos.add(Direction.DOWN.toVector())); - //copy all properties from the other half - for (Entry prop : otherPlant.getProperties().entrySet()) { - if (!state.getProperties().containsKey(prop.getKey())) { - state = state.with(prop.getKey(), prop.getValue()); - } - } + return otherPlant.with("half", "upper"); } return state; diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/GlassPaneConnectExtension.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/GlassPaneConnectExtension.java index c24b4c81..a8bb94b1 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/GlassPaneConnectExtension.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/GlassPaneConnectExtension.java @@ -33,7 +33,21 @@ public class GlassPaneConnectExtension extends ConnectSameOrFullBlockExtension { private static final HashSet AFFECTED_BLOCK_IDS = Sets.newHashSet( "minecraft:glass_pane", - "minecraft:stained_glass_pane", + "minecraft:white_stained_glass_pane", + "minecraft:orange_stained_glass_pane", + "minecraft:magenta_stained_glass_pane", + "minecraft:light_blue_white_stained_glass_pane", + "minecraft:yellow_stained_glass_pane", + "minecraft:lime_stained_glass_pane", + "minecraft:pink_stained_glass_pane", + "minecraft:gray_stained_glass_pane", + "minecraft:light_gray_stained_glass_pane", + "minecraft:cyan_stained_glass_pane", + "minecraft:purple_stained_glass_pane", + "minecraft:blue_stained_glass_pane", + "minecraft:green_stained_glass_pane", + "minecraft:red_stained_glass_pane", + "minecraft:black_stained_glass_pane", "minecraft:iron_bars" ); diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/RedstoneExtension.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/RedstoneExtension.java index 1bc58378..41090c13 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/RedstoneExtension.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/RedstoneExtension.java @@ -44,14 +44,14 @@ public class RedstoneExtension implements BlockStateExtension { private static final Set CONNECTIBLE = Sets.newHashSet( "minecraft:redstone_wire", - "minecraft:unlit_redstone_torch", + "minecraft:redstone_wall_torch", "minecraft:redstone_torch", "minecraft:stone_button", - "minecraft:wooden_button", + "minecraft:oak_button", "minecraft:stone_button", "minecraft:lever", "minecraft:stone_pressure_plate", - "minecraft:wooden_pressure_plate", + "minecraft:oak_pressure_plate", "minecraft:light_weighted_pressure_plate", "minecraft:heavy_weighted_pressure_plate" ); @@ -69,7 +69,7 @@ public class RedstoneExtension implements BlockStateExtension { private String connection(MCAWorld world, Vector3i pos, BlockState state, Direction direction) { BlockState next = world.getBlockState(pos.add(direction.toVector())); - if (CONNECTIBLE.contains(next.getId())) return "side"; + if (CONNECTIBLE.contains(next.getFullId())) return "side"; //TODO: up diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/SnowyExtension.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/SnowyExtension.java index d72b4a94..814da461 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/SnowyExtension.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/SnowyExtension.java @@ -35,15 +35,15 @@ import de.bluecolored.bluemap.core.world.BlockState; public class SnowyExtension implements BlockStateExtension { private static final Collection AFFECTED_BLOCK_IDS = Lists.newArrayList( - "minecraft:grass", - "minecraft:dirt" + "minecraft:grass_block", + "minecraft:podzol" ); @Override public BlockState extend(MCAWorld world, Vector3i pos, BlockState state) { BlockState above = world.getBlockState(pos.add(0, 1, 0)); - if (above.getId().equals("minecraft:snow_layer") || above.getId().equals("minecraft:snow")) { + if (above.getFullId().equals("minecraft:snow") || above.getFullId().equals("minecraft:snow_block")) { return state.with("snowy", "true"); } else { return state.with("snowy", "false"); diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/StairShapeExtension.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/StairShapeExtension.java index f20df86d..1873e55e 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/StairShapeExtension.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/StairShapeExtension.java @@ -38,7 +38,7 @@ public class StairShapeExtension implements BlockStateExtension { private static final HashSet AFFECTED_BLOCK_IDS = Sets.newHashSet( "minecraft:oak_stairs", - "minecraft:stone_stairs", + "minecraft:cobblestone_stairs", "minecraft:brick_stairs", "minecraft:stone_brick_stairs", "minecraft:nether_brick_stairs", @@ -102,7 +102,7 @@ public class StairShapeExtension implements BlockStateExtension { } private boolean isStairs(BlockState state) { - return AFFECTED_BLOCK_IDS.contains(state.getId()); + return AFFECTED_BLOCK_IDS.contains(state.getFullId()); } private boolean isEqualStairs(BlockState stair1, BlockState stair2) { diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/WoodenFenceConnectExtension.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/WoodenFenceConnectExtension.java index 169dfd36..61c15523 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/WoodenFenceConnectExtension.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/mca/extensions/WoodenFenceConnectExtension.java @@ -32,7 +32,7 @@ import com.google.common.collect.Sets; public class WoodenFenceConnectExtension extends ConnectSameOrFullBlockExtension { private static final HashSet AFFECTED_BLOCK_IDS = Sets.newHashSet( - "minecraft:fence", + "minecraft:oak_fence", "minecraft:spruce_fence", "minecraft:birch_fence", "minecraft:jungle_fence", diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/EmptyBlockContext.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/EmptyBlockContext.java index 14e8f4f2..0fa3ec13 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/EmptyBlockContext.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/EmptyBlockContext.java @@ -162,6 +162,12 @@ public class EmptyBlockContext implements ExtendedBlockContext { public Collection getChunkList(long modifiedSince) { return Collections.emptyList(); } + + @Override + public void invalidateChunkCache() {} + + @Override + public void invalidateChunkCache(Vector2i chunk) {} } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/ExtendedBlockContext.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/ExtendedBlockContext.java index faa278b2..6e5c0228 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/ExtendedBlockContext.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/context/ExtendedBlockContext.java @@ -37,12 +37,14 @@ public interface ExtendedBlockContext extends BlockContext { * This returns neighbour blocks.
* The distance can not be larger than two blocks in each direction!
*/ + @Override Block getRelativeBlock(Vector3i direction); /** * This returns neighbour blocks.
* The distance can not be larger than two blocks in each direction!
*/ + @Override default Block getRelativeBlock(int x, int y, int z){ return getRelativeBlock(new Vector3i(x, y, z)); } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/HiresModelManager.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/HiresModelManager.java index 00722b15..4c1938bc 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/HiresModelManager.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/HiresModelManager.java @@ -153,8 +153,8 @@ public class HiresModelManager { public Vector2i posToTile(Vector3d pos){ pos = pos.sub(new Vector3d(gridOrigin.getX(), 0.0, gridOrigin.getY())); return Vector2i.from( - (int) Math.floor(pos.getX() / (double) getTileSize().getX()), - (int) Math.floor(pos.getZ() / (double) getTileSize().getY()) + (int) Math.floor(pos.getX() / getTileSize().getX()), + (int) Math.floor(pos.getZ() / getTileSize().getY()) ); } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/blockmodel/ResourceModelBuilder.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/blockmodel/ResourceModelBuilder.java index 293d5444..317efa2a 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/blockmodel/ResourceModelBuilder.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/render/hires/blockmodel/ResourceModelBuilder.java @@ -74,7 +74,7 @@ public class ResourceModelBuilder { BlockStateModel model = new BlockStateModel(); for (WeighedArrayList bmrList : resource.getModelResources()){ - BlockModelResource bmr = bmrList.get((int) Math.floor(MathUtil.hashToFloat(context.getPosition(), 23489756) * (float) bmrList.size())); + BlockModelResource bmr = bmrList.get((int) Math.floor(MathUtil.hashToFloat(context.getPosition(), 23489756) * bmrList.size())); model.merge(fromModelResource(bmr)); } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/BlockColorProvider.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/BlockColorProvider.java index 6cf6961b..9ba74f87 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/BlockColorProvider.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/BlockColorProvider.java @@ -226,7 +226,7 @@ public class BlockColorProvider { throw new NoSuchElementException("No biome found with id: " + biomeId); } - float adjTemp = (float) GenericMath.clamp(bi.temp - (0.00166667 * (double) blocksAboveSeaLevel), 0d, 1d); + float adjTemp = (float) GenericMath.clamp(bi.temp - (0.00166667 * blocksAboveSeaLevel), 0d, 1d); float adjHumidity = (float) GenericMath.clamp(bi.humidity, 0d, 1d) * adjTemp; return new Vector2f(1 - adjTemp, 1 - adjHumidity); } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/ResourcePack.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/ResourcePack.java index 3f6f481f..37027a6b 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/ResourcePack.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/ResourcePack.java @@ -29,6 +29,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -50,6 +51,8 @@ import de.bluecolored.bluemap.core.world.BlockState; public class ResourcePack { + public static final String MINECRAFT_CLIENT_URL = "https://launcher.mojang.com/v1/objects/8c325a0c5bd674dd747d6ebaa4c791fd363ad8a9/client.jar"; + private Map resources; private TextureProvider textureProvider; @@ -121,6 +124,13 @@ public class ResourcePack { if (file.isDirectory()) continue; Path resourcePath = Paths.get("", file.getName().split("/")); + if ( + !resourcePath.startsWith(Paths.get("assets", "minecraft", "blockstates")) && + !resourcePath.startsWith(Paths.get("assets", "minecraft", "models", "block")) && + !resourcePath.startsWith(Paths.get("assets", "minecraft", "textures", "block")) && + !resourcePath.startsWith(Paths.get("assets", "minecraft", "textures", "colormap")) + ) continue; + InputStream fileInputStream = zipFile.getInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(Math.max(8, (int) file.getSize())); @@ -179,11 +189,10 @@ public class ResourcePack { } - public static void createDefaultResource(File file) throws IOException { - if (!file.exists()) { - file.getParentFile().mkdirs(); - FileUtils.copyURLToFile(ResourcePack.class.getResource("/DefaultResources.zip"), file, 10000, 10000); - } + public static void downloadDefaultResource(File file) throws IOException { + if (file.exists()) file.delete(); + file.getParentFile().mkdirs(); + FileUtils.copyURLToFile(new URL(MINECRAFT_CLIENT_URL), file, 10000, 10000); } } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/Block.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/Block.java index 7799bff3..b085e011 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/Block.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/Block.java @@ -25,6 +25,7 @@ package de.bluecolored.bluemap.core.world; import com.flowpowered.math.vector.Vector3i; +import com.google.common.base.MoreObjects; import de.bluecolored.bluemap.core.render.context.BlockContext; import de.bluecolored.bluemap.core.util.Direction; @@ -87,5 +88,15 @@ public abstract class Block { blockLight = (float) Math.max(neighbor.getBlockLightLevel(), blockLight); } } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("pos", getPosition()) + .add("biome", getBiome()) + .add("blocklight", getBlockLightLevel()) + .add("sunlight", getSunLightLevel()) + .toString(); + } } diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/World.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/World.java index f15d5ae3..7b130fe4 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/World.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/World.java @@ -74,5 +74,25 @@ public interface World extends WorldChunk { * (Be aware that the collection is not cached and recollected each time from the world-files!) */ public Collection getChunkList(long modifiedSince); + + /** + * Invalidates the complete chunk cache (if there is a cache), so that every chunk has to be reloaded from disk + */ + public void invalidateChunkCache(); + + /** + * Invalidates the chunk from the chunk-cache (if there is a cache), so that the chunk has to be reloaded from disk + */ + public void invalidateChunkCache(Vector2i chunk); + + /** + * Returns the ChunkPosition for a BlockPosition + */ + public default Vector2i blockPosToChunkPos(Vector3i block) { + return new Vector2i( + block.getX() >> 4, + block.getZ() >> 4 + ); + } } diff --git a/BlueMapCore/src/main/resources/DefaultResources.zip b/BlueMapCore/src/main/resources/DefaultResources.zip deleted file mode 100644 index 8ba93d77..00000000 Binary files a/BlueMapCore/src/main/resources/DefaultResources.zip and /dev/null differ diff --git a/BlueMapCore/src/main/resources/bluemap.conf b/BlueMapCore/src/main/resources/bluemap.conf index 9e82ba49..cfc95e0e 100644 --- a/BlueMapCore/src/main/resources/bluemap.conf +++ b/BlueMapCore/src/main/resources/bluemap.conf @@ -10,6 +10,15 @@ # and update configuration correctly. version: "1.0.0" +# By changing the setting (accept-download) below to TRUE you are indicating that you have accepted mojang's EULA (https://account.mojang.com/documents/minecraft_eula), +# you confirm that you own a license to Minecraft (Java Edition) +# and you agree that BlueMap will download and use this file for you: %resource-file% +# (Alternatively you can download the file yourself and store it here: ./config/bluemap/resourcepacks/client.jar) +# This file contains resources that belong to mojang and you must not redistribute it or do anything else that is not compilant with mojang's EULA. +# BlueMap uses resources in this file to generate the 3D-Models used for the map and texture them. (BlueMap will not work without those resources.) +# %date% +accept-download: false + web { # With this setting you can disable the web-server. # This is usefull if you want to only render the map-data for later use, or if you setup your own webserver. diff --git a/BlueMapCore/src/main/resources/webroot.zip b/BlueMapCore/src/main/resources/webroot.zip index c9412339..2fb7fe6e 100644 Binary files a/BlueMapCore/src/main/resources/webroot.zip and b/BlueMapCore/src/main/resources/webroot.zip differ diff --git a/BlueMapCore/src/main/webroot/js/libs/bluemap.js b/BlueMapCore/src/main/webroot/js/libs/bluemap.js index f846d529..67ab8b64 100644 --- a/BlueMapCore/src/main/webroot/js/libs/bluemap.js +++ b/BlueMapCore/src/main/webroot/js/libs/bluemap.js @@ -351,8 +351,8 @@ BlueMap.prototype.loadHiresMaterial = function (callback) { texture.generateMipmaps = false; texture.magFilter = THREE.NearestFilter; texture.minFilter = THREE.NearestFilter; - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; + texture.wrapS = THREE.ClampToEdgeWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; texture.flipY = false; texture.needsUpdate = true; texture.flatShading = true; diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/Commands.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/Commands.java new file mode 100644 index 00000000..74e7f694 --- /dev/null +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/Commands.java @@ -0,0 +1,312 @@ +package de.bluecolored.bluemap.sponge; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import org.apache.commons.lang3.time.DurationFormatUtils; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.command.CommandResult; +import org.spongepowered.api.command.CommandSource; +import org.spongepowered.api.command.args.GenericArguments; +import org.spongepowered.api.command.spec.CommandSpec; +import org.spongepowered.api.text.Text; +import org.spongepowered.api.text.action.TextActions; +import org.spongepowered.api.text.format.TextColors; +import org.spongepowered.api.world.Locatable; +import org.spongepowered.api.world.Location; + +import com.flowpowered.math.vector.Vector2i; +import com.flowpowered.math.vector.Vector3i; +import com.google.common.collect.Lists; + +import de.bluecolored.bluemap.core.logger.Logger; +import de.bluecolored.bluemap.core.render.hires.HiresModelManager; +import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException; +import de.bluecolored.bluemap.core.world.Block; +import de.bluecolored.bluemap.core.world.ChunkNotGeneratedException; +import de.bluecolored.bluemap.core.world.World; + +public class Commands { + + private SpongePlugin plugin; + + public Commands(SpongePlugin plugin) { + this.plugin = plugin; + } + + public CommandSpec createRootCommand() { + + @SuppressWarnings("unused") + CommandSpec debugCommand = CommandSpec.builder() + .executor((source, args) -> { + if (source instanceof Locatable) { + try { + Location loc = ((Locatable) source).getLocation(); + UUID worldUuid = loc.getExtent().getUniqueId(); + World world = plugin.getWorld(worldUuid); + Block block = world.getBlock(loc.getBlockPosition()); + Block blockBelow = world.getBlock(loc.getBlockPosition().add(0, -1, 0)); + + source.sendMessages(Lists.newArrayList( + Text.of("Block: " + block), + Text.of("Block below: " + blockBelow) + )); + } catch (ChunkNotGeneratedException e) { + Logger.global.logError("Failed to debug!", e); + } + } + + return CommandResult.success(); + }) + .build(); + + return CommandSpec.builder() + .description(Text.of("Displays BlueMaps render status")) + .permission("bluemap.status") + .childArgumentParseExceptionFallback(false) + .child(createReloadCommand(), "reload") + .child(createPauseRenderCommand(), "pause") + .child(createResumeRenderCommand(), "resume") + .child(createRenderCommand(), "render") + //.child(debugCommand, "debug") + .executor((source, args) -> { + source.sendMessages(createStatusMessage()); + return CommandResult.success(); + }) + .build(); + } + + public CommandSpec createReloadCommand() { + return CommandSpec.builder() + .description(Text.of("Reloads all resources and configuration-files")) + .permission("bluemap.reload") + .executor((source, args) -> { + try { + plugin.reload(); + + if (plugin.isLoaded()) { + source.sendMessage(Text.of(TextColors.GREEN, "BlueMap reloaded!")); + return CommandResult.success(); + } else { + source.sendMessage(Text.of(TextColors.RED, "Could not load BlueMap! See the console for details!")); + return CommandResult.empty(); + } + } catch (IOException | NoSuchResourceException ex) { + Logger.global.logError("Failed to reload BlueMap!", ex); + + source.sendMessage(Text.of(TextColors.RED, "There was an error reloading BlueMap! See the console for details!")); + return CommandResult.empty(); + } + }) + .build(); + } + + public CommandSpec createPauseRenderCommand() { + return CommandSpec.builder() + .description(Text.of("Pauses all rendering")) + .permission("bluemap.pause") + .executor((source, args) -> { + if (plugin.getRenderManager().isRunning()) { + plugin.getRenderManager().stop(); + source.sendMessage(Text.of(TextColors.GREEN, "BlueMap rendering paused!")); + return CommandResult.success(); + } else { + source.sendMessage(Text.of(TextColors.RED, "BlueMap rendering are already paused!")); + return CommandResult.empty(); + } + }) + .build(); + } + + public CommandSpec createResumeRenderCommand() { + return CommandSpec.builder() + .description(Text.of("Resumes all paused rendering")) + .permission("bluemap.resume") + .executor((source, args) -> { + if (!plugin.getRenderManager().isRunning()) { + plugin.getRenderManager().start(); + source.sendMessage(Text.of(TextColors.GREEN, "BlueMap renders resumed!")); + return CommandResult.success(); + } else { + source.sendMessage(Text.of(TextColors.RED, "BlueMap renders are already running!")); + return CommandResult.empty(); + } + }) + .build(); + } + + public CommandSpec createRenderCommand() { + return CommandSpec.builder() + .description(Text.of("Renders the whole world")) + .permission("bluemap.rendertask.create.world") + .childArgumentParseExceptionFallback(false) + .child(createPrioritizeTaskCommand(), "prioritize") + .child(createRemoveTaskCommand(), "remove") + .arguments(GenericArguments.optional(GenericArguments.world(Text.of("world")))) + .executor((source, args) -> { + org.spongepowered.api.world.World spongeWorld = args.getOne("world").orElse(null); + if (spongeWorld == null && source instanceof Locatable) { + Location loc = ((Locatable) source).getLocation(); + spongeWorld = loc.getExtent(); + } else { + source.sendMessage(Text.of(TextColors.RED, "You have to define a world to render!")); + return CommandResult.empty(); + } + + World world = plugin.getWorld(spongeWorld.getUniqueId()); + if (world == null) { + source.sendMessage(Text.of(TextColors.RED, "This world is not loaded with BlueMap! Maybe it is not configured?")); + } + + world.invalidateChunkCache(); + + Sponge.getScheduler().createTaskBuilder() + .async() + .execute(() -> createWorldRenderTask(source, world)) + .submit(plugin); + + return CommandResult.success(); + }) + .build(); + } + + public CommandSpec createPrioritizeTaskCommand() { + return CommandSpec.builder() + .description(Text.of("Prioritizes the render-task with the given uuid")) + .permission("bluemap.rendertask.prioritize") + .arguments(GenericArguments.uuid(Text.of("task-uuid"))) + .executor((source, args) -> { + Optional uuid = args.getOne("task-uuid"); + if (!uuid.isPresent()) { + source.sendMessage(Text.of("You need to specify a task-uuid")); + return CommandResult.empty(); + } + + for (RenderTask task : plugin.getRenderManager().getRenderTasks()) { + if (task.getUuid().equals(uuid.get())) { + plugin.getRenderManager().prioritizeRenderTask(task); + break; + } + } + + source.sendMessages(createStatusMessage()); + return CommandResult.success(); + }) + .build(); + } + + public CommandSpec createRemoveTaskCommand() { + return CommandSpec.builder() + .description(Text.of("Removes the render-task with the given uuid")) + .permission("bluemap.rendertask.remove") + .arguments(GenericArguments.uuid(Text.of("task-uuid"))) + .executor((source, args) -> { + Optional uuid = args.getOne("task-uuid"); + if (!uuid.isPresent()) { + source.sendMessage(Text.of("You need to specify a task-uuid")); + return CommandResult.empty(); + } + + for (RenderTask task : plugin.getRenderManager().getRenderTasks()) { + if (task.getUuid().equals(uuid.get())) { + plugin.getRenderManager().removeRenderTask(task); + break; + } + } + + source.sendMessages(createStatusMessage()); + return CommandResult.success(); + }) + .build(); + } + + private List createStatusMessage(){ + List lines = new ArrayList<>(); + + RenderManager renderer = plugin.getRenderManager(); + + lines.add(Text.EMPTY); + lines.add(Text.of(TextColors.BLUE, "Tile-Updates:")); + + if (renderer.isRunning()) { + lines.add(Text.of(TextColors.WHITE, " Render-Threads are ", Text.of(TextActions.runCommand("/bluemap pause"), TextActions.showText(Text.of("click to pause rendering")), TextColors.GREEN, "running"), TextColors.GRAY, "!")); + } else { + lines.add(Text.of(TextColors.WHITE, " Render-Threads are ", Text.of(TextActions.runCommand("/bluemap resume"), TextActions.showText(Text.of("click to resume rendering")), TextColors.RED, "paused"), TextColors.GRAY, "!")); + } + + lines.add(Text.of(TextColors.WHITE, " Scheduled tile-updates: ", Text.of(TextActions.showText(Text.of("tiles waiting for a free render-thread")), TextColors.GOLD, renderer.getQueueSize()), Text.of(TextActions.showText(Text.of("tiles waiting for world-save")), TextColors.GRAY, " + " + plugin.getUpdateHandler().getUpdateBufferCount()))); + + RenderTask[] tasks = renderer.getRenderTasks(); + if (tasks.length > 0) { + RenderTask task = tasks[0]; + + long time = task.getActiveTime(); + String durationString = DurationFormatUtils.formatDurationWords(time, true, true); + double pct = (double)task.getRenderedTileCount() / (double)(task.getRenderedTileCount() + task.getRemainingTileCount()); + + long ert = (long)((time / pct) * (1d - pct)); + String ertDurationString = DurationFormatUtils.formatDurationWords(ert, true, true); + + lines.add(Text.of(TextColors.BLUE, "Current task:")); + lines.add(Text.of(" ", createCancelTaskText(task), TextColors.WHITE, " Task ", TextColors.GOLD, task.getName(), TextColors.WHITE, " for map ", TextActions.showText(Text.of(TextColors.WHITE, "World: ", TextColors.GOLD, task.getMapType().getWorld().getName())), TextColors.GOLD, task.getMapType().getName())); + lines.add(Text.of(TextColors.WHITE, " rendered ", TextColors.GOLD, task.getRenderedTileCount(), TextColors.WHITE, " tiles ", TextColors.GRAY, "(" + (Math.round(pct * 1000)/10.0) + "%)", TextColors.WHITE, " in ", TextColors.GOLD, durationString)); + lines.add(Text.of(TextColors.WHITE, " with ", TextColors.GOLD, task.getRemainingTileCount(), TextColors.WHITE, " tiles to go. ETA: ", TextColors.GOLD, ertDurationString)); + } + + if (tasks.length > 1) { + lines.add(Text.of(TextColors.BLUE, "Waiting tasks:")); + for (int i = 1; i < tasks.length; i++) { + RenderTask task = tasks[i]; + lines.add(Text.of(" ", createCancelTaskText(task), createPrioritizeTaskText(task), TextColors.WHITE, " Task ", TextColors.GOLD, task.getName(), TextColors.WHITE, " for map ", Text.of(TextActions.showText(Text.of(TextColors.WHITE, "World: ", TextColors.GOLD, task.getMapType().getWorld().getName())), TextColors.GOLD, task.getMapType().getName()), TextColors.GRAY, " (" + task.getRemainingTileCount() + " tiles)")); + } + } + + return lines; + } + + private Text createCancelTaskText(RenderTask task) { + return Text.of(TextActions.runCommand("/bluemap render remove " + task.getUuid()), TextActions.showText(Text.of("click to remove this render-task")), TextColors.RED, "[X]"); + } + + private Text createPrioritizeTaskText(RenderTask task) { + return Text.of(TextActions.runCommand("/bluemap render prioritize " + task.getUuid()), TextActions.showText(Text.of("click to prioritize this render-task")), TextColors.GREEN, "[^]"); + } + + private void createWorldRenderTask(CommandSource source, World world) { + source.sendMessage(Text.of(TextColors.GOLD, "Collecting chunks to render...")); + Collection chunks = world.getChunkList(); + source.sendMessage(Text.of(TextColors.GREEN, chunks.size() + " chunks found!")); + + for (MapType map : SpongePlugin.getInstance().getMapTypes()) { + if (!map.getWorld().getUUID().equals(world.getUUID())) continue; + + source.sendMessage(Text.of(TextColors.GOLD, "Collecting tiles for map '" + map.getId() + "'")); + + HiresModelManager hmm = map.getTileRenderer().getHiresModelManager(); + Set tiles = new HashSet<>(); + for (Vector2i chunk : chunks) { + Vector3i minBlockPos = new Vector3i(chunk.getX() * 16, 0, chunk.getY() * 16); + tiles.add(hmm.posToTile(minBlockPos)); + tiles.add(hmm.posToTile(minBlockPos.add(0, 0, 15))); + tiles.add(hmm.posToTile(minBlockPos.add(15, 0, 0))); + tiles.add(hmm.posToTile(minBlockPos.add(15, 0, 15))); + } + + RenderTask task = new RenderTask("world-render", map); + task.addTiles(tiles); + task.optimizeQueue(); + plugin.getRenderManager().addRenderTask(task); + + source.sendMessage(Text.of(TextColors.GREEN, tiles.size() + " tiles found! Task created.")); + } + + source.sendMessage(Text.of(TextColors.GREEN, "All render tasks created! Use /bluemap to view the progress!")); + } + +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/MapType.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/MapType.java new file mode 100644 index 00000000..b13a5eda --- /dev/null +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/MapType.java @@ -0,0 +1,92 @@ +/* + * This file is part of BlueMapSponge, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.sponge; + +import java.io.IOException; + +import com.flowpowered.math.vector.Vector2i; +import com.google.common.base.Preconditions; + +import de.bluecolored.bluemap.core.render.TileRenderer; +import de.bluecolored.bluemap.core.render.WorldTile; +import de.bluecolored.bluemap.core.world.ChunkNotGeneratedException; +import de.bluecolored.bluemap.core.world.World; + +public class MapType { + + private final String id; + private String name; + private World world; + private TileRenderer tileRenderer; + + public MapType(String id, String name, World world, TileRenderer tileRenderer) { + Preconditions.checkNotNull(id); + Preconditions.checkNotNull(name); + Preconditions.checkNotNull(world); + Preconditions.checkNotNull(tileRenderer); + + this.id = id; + this.name = name; + this.world = world; + this.tileRenderer = tileRenderer; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public World getWorld() { + return world; + } + + public TileRenderer getTileRenderer() { + return tileRenderer; + } + + public void renderTile(Vector2i tile) throws IOException, ChunkNotGeneratedException { + getTileRenderer().render(new WorldTile(getWorld(), tile)); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj != null && obj instanceof MapType) { + MapType that = (MapType) obj; + + return this.id.equals(that.id); + } + + return false; + } + +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/MapUpdateHandler.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/MapUpdateHandler.java new file mode 100644 index 00000000..13b8ed02 --- /dev/null +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/MapUpdateHandler.java @@ -0,0 +1,114 @@ +package de.bluecolored.bluemap.sponge; + +import java.util.Iterator; +import java.util.Optional; +import java.util.UUID; + +import org.spongepowered.api.Sponge; +import org.spongepowered.api.block.BlockSnapshot; +import org.spongepowered.api.data.Transaction; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.Order; +import org.spongepowered.api.event.block.ChangeBlockEvent; +import org.spongepowered.api.event.filter.type.Exclude; +import org.spongepowered.api.event.world.SaveWorldEvent; +import org.spongepowered.api.event.world.chunk.PopulateChunkEvent; +import org.spongepowered.api.world.Location; + +import com.flowpowered.math.vector.Vector2i; +import com.flowpowered.math.vector.Vector3i; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; + +public class MapUpdateHandler { + + public Multimap updateBuffer; + + public MapUpdateHandler() { + updateBuffer = MultimapBuilder.hashKeys().hashSetValues().build(); + + Sponge.getEventManager().registerListeners(SpongePlugin.getInstance(), this); + } + + @Listener(order = Order.POST) + public void onWorldSave(SaveWorldEvent.Post evt) { + UUID worldUuid = evt.getTargetWorld().getUniqueId(); + RenderManager renderManager = SpongePlugin.getInstance().getRenderManager(); + + synchronized (updateBuffer) { + Iterator iterator = updateBuffer.keys().iterator(); + while (iterator.hasNext()) { + MapType map = iterator.next(); + if (map.getWorld().getUUID().equals(worldUuid)) { + renderManager.createTickets(map, updateBuffer.get(map)); + iterator.remove(); + } + } + + } + } + + @Listener(order = Order.POST) + @Exclude({ChangeBlockEvent.Post.class, ChangeBlockEvent.Pre.class, ChangeBlockEvent.Modify.class}) + public void onBlockChange(ChangeBlockEvent evt) { + synchronized (updateBuffer) { + for (Transaction tr : evt.getTransactions()) { + if (!tr.isValid()) continue; + + Optional> ow = tr.getFinal().getLocation(); + if (ow.isPresent()) { + updateBlock(ow.get().getExtent().getUniqueId(), ow.get().getPosition().toInt()); + } + } + } + } + + @Listener(order = Order.POST) + public void onChunkPopulate(PopulateChunkEvent.Post evt) { + UUID world = evt.getTargetChunk().getWorld().getUniqueId(); + + int x = evt.getTargetChunk().getPosition().getX(); + int z = evt.getTargetChunk().getPosition().getZ(); + + // also update the chunks around, because they might be modified or not rendered yet due to finalizations + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + updateChunk(world, new Vector2i(x + dx, z + dz)); + } + } + } + + private void updateChunk(UUID world, Vector2i chunkPos) { + Vector3i min = new Vector3i(chunkPos.getX() * 16, 0, chunkPos.getY() * 16); + Vector3i max = min.add(15, 255, 15); + + Vector3i xmin = new Vector3i(min.getX(), 0, max.getY()); + Vector3i xmax = new Vector3i(max.getX(), 255, min.getY()); + + //update all corners so we always update all tiles containing this chunk + synchronized (updateBuffer) { + updateBlock(world, min); + updateBlock(world, max); + updateBlock(world, xmin); + updateBlock(world, xmax); + } + } + + private void updateBlock(UUID world, Vector3i pos){ + synchronized (updateBuffer) { + for (MapType mapType : SpongePlugin.getInstance().getMapTypes()) { + if (mapType.getWorld().getUUID().equals(world)) { + mapType.getWorld().invalidateChunkCache(mapType.getWorld().blockPosToChunkPos(pos)); + + Vector2i tile = mapType.getTileRenderer().getHiresModelManager().posToTile(pos); + updateBuffer.put(mapType, tile); + } + } + } + } + + public int getUpdateBufferCount() { + return updateBuffer.size(); + } + +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderManager.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderManager.java new file mode 100644 index 00000000..2e3fc7cd --- /dev/null +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderManager.java @@ -0,0 +1,154 @@ +package de.bluecolored.bluemap.sponge; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; + +import com.flowpowered.math.vector.Vector2i; + +import de.bluecolored.bluemap.core.logger.Logger; + +public class RenderManager { + + private boolean running; + + private Thread[] renderThreads; + private ArrayDeque renderTickets; + private Map renderTicketMap; + private Deque renderTasks; + + public RenderManager(int threadCount) { + running = false; + renderThreads = new Thread[threadCount]; + renderTickets = new ArrayDeque<>(1000); + renderTicketMap = new HashMap<>(1000); + renderTasks = new ArrayDeque<>(); + } + + public synchronized void start() { + stop(); //ensure everything is stopped first + + for (int i = 0; i < renderThreads.length; i++) { + renderThreads[i] = new Thread(this::renderThread); + renderThreads[i].setDaemon(true); + renderThreads[i].setPriority(Thread.MIN_PRIORITY); + renderThreads[i].start(); + } + + running = true; + } + + public synchronized void stop() { + for (int i = 0; i < renderThreads.length; i++) { + if (renderThreads[i] != null) { + renderThreads[i].interrupt(); + renderThreads[i] = null; + } + } + + running = false; + } + + public void addRenderTask(RenderTask task) { + synchronized (renderTasks) { + renderTasks.add(task); + } + } + + public RenderTicket createTicket(MapType mapType, Vector2i tile) { + RenderTicket ticket = new RenderTicket(mapType, tile); + synchronized (renderTickets) { + if (renderTicketMap.putIfAbsent(ticket, ticket) == null) { + renderTickets.add(ticket); + return ticket; + } else { + return renderTicketMap.get(ticket); + } + } + } + + public Collection createTickets(MapType mapType, Collection tiles) { + if (tiles.size() < 0) return Collections.emptyList(); + + Collection tickets = new ArrayList<>(tiles.size()); + synchronized (renderTickets) { + for (Vector2i tile : tiles) { + tickets.add(createTicket(mapType, tile)); + } + } + + return tickets; + } + + public boolean prioritizeRenderTask(RenderTask renderTask) { + synchronized (renderTasks) { + if (renderTasks.remove(renderTask)) { + renderTasks.addFirst(renderTask); + return true; + } + + return false; + } + } + + public boolean removeRenderTask(RenderTask renderTask) { + synchronized (renderTasks) { + return renderTasks.remove(renderTask); + } + } + + private void renderThread() { + RenderTicket ticket = null; + + while (!Thread.interrupted()) { + synchronized (renderTickets) { + ticket = renderTickets.poll(); + if (ticket != null) renderTicketMap.remove(ticket); + } + + if (ticket == null) { + synchronized (renderTasks) { + RenderTask task = renderTasks.peek(); + if (task != null) { + ticket = task.poll(); + if (task.isFinished()) renderTasks.poll(); + task.getMapType().getTileRenderer().save(); + } + } + } + + if (ticket != null) { + try { + ticket.render(); + } catch (IOException e) { + Logger.global.logError("Failed to render tile " + ticket.getTile() + " of map '" + ticket.getMapType().getId() + "'!", e); + } + } else { + try { + Thread.sleep(1000); // we don't need a super fast response time, so waiting a second is totally fine + } catch (InterruptedException e) { break; } + } + } + } + + public int getQueueSize() { + return renderTickets.size(); + } + + /** + * Returns a copy of the deque with the render tasks in order as array + */ + public RenderTask[] getRenderTasks(){ + return renderTasks.toArray(new RenderTask[renderTasks.size()]); + } + + public boolean isRunning() { + return running; + } + +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderTask.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderTask.java new file mode 100644 index 00000000..9688ed81 --- /dev/null +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderTask.java @@ -0,0 +1,139 @@ +package de.bluecolored.bluemap.sponge; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collection; +import java.util.Deque; +import java.util.UUID; + +import com.flowpowered.math.vector.Vector2d; +import com.flowpowered.math.vector.Vector2i; + +public class RenderTask { + + private final UUID uuid; + private String name; + + private final MapType mapType; + private Deque renderTiles; + + private long firstTileTime; + private long additionalRunTime; + private int renderedTiles; + + private UUID taskOwner; + + public RenderTask(String name, MapType mapType) { + this.uuid = UUID.randomUUID(); + this.name = name; + this.mapType = mapType; + this.renderTiles = new ArrayDeque<>(); + this.firstTileTime = -1; + this.additionalRunTime = 0; + this.renderedTiles = 0; + this.taskOwner = null; + } + + public void optimizeQueue() { + //Find a good grid size to match the MCAWorlds chunk-cache size of 500 + Vector2d sortGridSize = new Vector2d(20, 20).div(mapType.getTileRenderer().getHiresModelManager().getTileSize().toDouble().div(16)).ceil().max(1, 1); + + synchronized (renderTiles) { + Vector2i[] array = renderTiles.toArray(new Vector2i[renderTiles.size()]); + Arrays.sort(array, (v1, v2) -> { + Vector2i v1SortGridPos = v1.toDouble().div(sortGridSize).floor().toInt(); + Vector2i v2SortGridPos = v2.toDouble().div(sortGridSize).floor().toInt(); + + if (v1SortGridPos != v2SortGridPos){ + int v1Dist = v1SortGridPos.distanceSquared(Vector2i.ZERO); + int v2Dist = v2SortGridPos.distanceSquared(Vector2i.ZERO); + + if (v1Dist < v2Dist) return -1; + if (v1Dist > v2Dist) return 1; + } + + if (v1.getY() < v1.getY()) return -1; + if (v1.getY() > v1.getY()) return 1; + if (v1.getX() < v1.getX()) return -1; + if (v1.getX() > v1.getX()) return 1; + + return 0; + }); + renderTiles.clear(); + for (Vector2i tile : array) { + renderTiles.add(tile); + } + } + } + + public void addTile(Vector2i tile) { + synchronized (renderTiles) { + renderTiles.add(tile); + } + } + + public void addTiles(Collection tiles) { + synchronized (renderTiles) { + renderTiles.addAll(tiles); + } + } + + public RenderTicket poll() { + synchronized (renderTiles) { + Vector2i tile = renderTiles.poll(); + if (tile != null) { + renderedTiles++; + if (firstTileTime < 0) firstTileTime = System.currentTimeMillis(); + return new RenderTicket(mapType, tile); + } else { + return null; + } + } + } + + /** + * Pauses the render-time counter. + * So if the rendering gets paused, the statistics remain correct. + * It will resume as soon as a new ticket gets polled + */ + public void pause() { + synchronized (renderTiles) { + additionalRunTime += System.currentTimeMillis() - firstTileTime; + firstTileTime = -1; + } + } + + public long getActiveTime() { + if (firstTileTime < 0) return additionalRunTime; + return (System.currentTimeMillis() - firstTileTime) + additionalRunTime; + } + + public UUID getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public MapType getMapType() { + return mapType; + } + + public int getRenderedTileCount() { + return renderedTiles; + } + + public int getRemainingTileCount() { + return renderTiles.size(); + } + + public boolean isFinished() { + return renderTiles.isEmpty(); + } + + public UUID getTaskOwner() { + return taskOwner; + } + +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderTicket.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderTicket.java new file mode 100644 index 00000000..052fd8f0 --- /dev/null +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/RenderTicket.java @@ -0,0 +1,62 @@ +package de.bluecolored.bluemap.sponge; + +import java.io.IOException; +import java.util.Objects; + +import com.flowpowered.math.vector.Vector2i; + +import de.bluecolored.bluemap.core.world.ChunkNotGeneratedException; + +public class RenderTicket { + + private final MapType map; + private final Vector2i tile; + + private boolean finished; + + public RenderTicket(MapType map, Vector2i tile) { + this.map = map; + this.tile = tile; + this.finished = false; + } + + public synchronized void render() throws IOException { + if (!finished) { + + try { + map.renderTile(tile); + } catch (ChunkNotGeneratedException e) { + //ignore + } + + finished = true; + } + } + + public MapType getMapType() { + return map; + } + + public Vector2i getTile() { + return tile; + } + + public boolean isFinished() { + return finished; + } + + @Override + public int hashCode() { + return Objects.hash(map.getId(), tile); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof RenderTicket)) return false; + RenderTicket ticket = (RenderTicket) other; + + if (!ticket.tile.equals(tile)) return false; + return ticket.map.getId().equals(map.getId()); + } + +} diff --git a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java index bcf2033c..dc0b551c 100644 --- a/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java +++ b/BlueMapSponge/src/main/java/de/bluecolored/bluemap/sponge/SpongePlugin.java @@ -24,14 +24,43 @@ */ package de.bluecolored.bluemap.sponge; +import java.io.File; +import java.io.IOException; import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import javax.inject.Inject; +import org.spongepowered.api.Sponge; import org.spongepowered.api.config.ConfigDir; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.game.GameReloadEvent; +import org.spongepowered.api.event.game.state.GameStartingServerEvent; +import org.spongepowered.api.event.game.state.GameStoppingEvent; import org.spongepowered.api.plugin.Plugin; +import org.spongepowered.api.scheduler.SpongeExecutorService; +import com.flowpowered.math.vector.Vector2i; +import com.google.common.collect.Lists; + +import de.bluecolored.bluemap.core.config.ConfigurationFile; +import de.bluecolored.bluemap.core.config.ConfigurationFile.MapConfig; import de.bluecolored.bluemap.core.logger.Logger; +import de.bluecolored.bluemap.core.mca.MCAWorld; +import de.bluecolored.bluemap.core.render.TileRenderer; +import de.bluecolored.bluemap.core.render.hires.HiresModelManager; +import de.bluecolored.bluemap.core.render.lowres.LowresModelManager; +import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException; +import de.bluecolored.bluemap.core.resourcepack.ResourcePack; +import de.bluecolored.bluemap.core.web.BlueMapWebServer; +import de.bluecolored.bluemap.core.web.WebFilesManager; +import de.bluecolored.bluemap.core.web.WebSettings; +import de.bluecolored.bluemap.core.world.World; +import net.querz.nbt.CompoundTag; +import net.querz.nbt.NBTUtil; @Plugin( id = SpongePlugin.PLUGIN_ID, @@ -46,25 +75,284 @@ public class SpongePlugin { public static final String PLUGIN_NAME = "BlueMap"; public static final String PLUGIN_VERSION = "0.0.0"; - private static Object plugin; + private static SpongePlugin instance; @Inject @ConfigDir(sharedRoot = false) private Path configurationDir; + private ConfigurationFile config; + private ResourcePack resourcePack; + + private Map worlds; + private Map maps; + + private RenderManager renderManager; + private MapUpdateHandler updateHandler; + private BlueMapWebServer webServer; + + private SpongeExecutorService syncExecutor; + private SpongeExecutorService asyncExecutor; + + private boolean loaded = false; + @Inject public SpongePlugin(org.slf4j.Logger logger) { - plugin = this; - Logger.global = new Slf4jLogger(logger); + + this.maps = new HashMap<>(); + this.worlds = new HashMap<>(); + + instance = this; + } + + public synchronized void load() throws IOException, NoSuchResourceException { + if (loaded) return; + unload(); //ensure nothing is left running (from a failed load or something) + + //init commands + Sponge.getCommandManager().register(this, new Commands(this).createRootCommand(), "bluemap"); + + //load configs + File configFile = getConfigPath().resolve("bluemap.conf").toFile(); + config = ConfigurationFile.loadOrCreate(configFile); + + //load resources + File defaultResourceFile = getConfigPath().resolve("resourcepacks").resolve("client.jar").toFile(); + File textureExportFile = config.getWebDataPath().resolve("textures.json").toFile(); + + if (!defaultResourceFile.exists()) { + handleMissingResources(defaultResourceFile, configFile); + return; + } + + resourcePack = new ResourcePack(Lists.newArrayList(defaultResourceFile), textureExportFile); + + //load maps + for (MapConfig mapConfig : config.getMapConfigs()) { + String id = mapConfig.getId(); + String name = mapConfig.getName(); + + File worldFolder = new File(mapConfig.getWorldPath()); + if (!worldFolder.exists() || !worldFolder.isDirectory()) { + Logger.global.logError("Failed to load map '" + id + "': '" + worldFolder + "' does not exist or is no directory!", new IOException()); + continue; + } + + UUID worldUUID; + try { + CompoundTag levelSponge = (CompoundTag) NBTUtil.readTag(new File(worldFolder, "level_sponge.dat")); + CompoundTag spongeData = levelSponge.getCompoundTag("SpongeData"); + long least = spongeData.getLong("UUIDLeast"); + long most = spongeData.getLong("UUIDMost"); + worldUUID = new UUID(most, least); + } catch (Exception e) { + Logger.global.logError("Failed to load map '" + id + "': Failed to read level_sponge.dat", e); + continue; + } + + World world = worlds.get(worldUUID); + if (world == null) { + try { + world = MCAWorld.load(worldFolder.toPath(), worldUUID); + worlds.put(worldUUID, world); + } catch (IOException e) { + Logger.global.logError("Failed to load map '" + id + "': Failed to read level.dat", e); + continue; + } + } + + HiresModelManager hiresModelManager = new HiresModelManager( + config.getWebDataPath().resolve("hires").resolve(id), + resourcePack, + new Vector2i(mapConfig.getHiresTileSize(), mapConfig.getHiresTileSize()), + getAsyncExecutor() + ); + + LowresModelManager lowresModelManager = new LowresModelManager( + config.getWebDataPath().resolve("lowres").resolve(id), + new Vector2i(mapConfig.getLowresPointsPerLowresTile(), mapConfig.getLowresPointsPerLowresTile()), + new Vector2i(mapConfig.getLowresPointsPerHiresTile(), mapConfig.getLowresPointsPerHiresTile()) + ); + + TileRenderer tileRenderer = new TileRenderer(hiresModelManager, lowresModelManager, mapConfig); + + MapType mapType = new MapType(id, name, world, tileRenderer); + maps.put(id, mapType); + } + + //initialize render manager + renderManager = new RenderManager(config.getRenderThreadCount()); + renderManager.start(); + + //start map updater + updateHandler = new MapUpdateHandler(); + + //create/update webfiles + WebFilesManager webFilesManager = new WebFilesManager(config.getWebRoot()); + if (webFilesManager.needsUpdate()) { + webFilesManager.updateFiles(); + } + + WebSettings webSettings = new WebSettings(config.getWebDataPath().resolve("settings.json").toFile()); + for (MapType map : maps.values()) { + webSettings.setName(map.getName(), map.getId()); + webSettings.setFrom(map.getTileRenderer(), map.getId()); + } + for (ConfigurationFile.MapConfig map : config.getMapConfigs()) { + webSettings.setHiresViewDistance(map.getHiresViewDistance(), map.getId()); + webSettings.setLowresViewDistance(map.getLowresViewDistance(), map.getId()); + } + webSettings.save(); + + //start webserver + if (config.isWebserverEnabled()) { + webServer = new BlueMapWebServer(config); + webServer.updateWebfiles(); + webServer.start(); + } + + loaded = true; + } + + public synchronized void unload() { + + //stop services + if (renderManager != null) renderManager.stop(); + if (webServer != null) webServer.close(); + + //unregister listeners + if (updateHandler != null) Sponge.getEventManager().unregisterListeners(updateHandler); + updateHandler = null; + + //unregister commands + Sponge.getCommandManager().getOwnedBy(this).forEach(Sponge.getCommandManager()::removeMapping); + + //stop scheduled tasks + Sponge.getScheduler().getScheduledTasks(this).forEach(t -> t.cancel()); + + //save renders + for (MapType map : maps.values()) { + map.getTileRenderer().save(); + } + + //clear resources and configs + renderManager = null; + webServer = null; + resourcePack = null; + config = null; + maps.clear(); + worlds.clear(); + + loaded = false; + } + + public synchronized void reload() throws IOException, NoSuchResourceException { + unload(); + load(); + } + + @Listener + public void onServerStart(GameStartingServerEvent evt) { + syncExecutor = Sponge.getScheduler().createSyncExecutor(this); + asyncExecutor = Sponge.getScheduler().createAsyncExecutor(this); + + try { + load(); + if (isLoaded()) Logger.global.logInfo("Loaded!"); + } catch (IOException | NoSuchResourceException e) { + Logger.global.logError("Failed to load!", e); + } + } + + @Listener + public void onServerStop(GameStoppingEvent evt) { + unload(); + Logger.global.logInfo("Saved and stopped!"); + } + + @Listener + public void onServerReload(GameReloadEvent evt) { + try { + reload(); + Logger.global.logInfo("Reloaded!"); + } catch (IOException | NoSuchResourceException e) { + Logger.global.logError("Failed to load!", e); + } + } + + private void handleMissingResources(File resourceFile, File configFile) { + if (config.isDownloadAccepted()) { + + //download file async + Sponge.getScheduler().createTaskBuilder() + .async() + .execute(() -> { + try { + Logger.global.logInfo("Downloading " + ResourcePack.MINECRAFT_CLIENT_URL + " to " + resourceFile + " ..."); + ResourcePack.downloadDefaultResource(resourceFile); + } catch (IOException e) { + Logger.global.logError("Failed to download resources!", e); + return; + } + + //reload bluemap on server thread + Sponge.getScheduler().createTaskBuilder() + .execute(() -> { + try { + Logger.global.logInfo("Download finished! Reloading..."); + reload(); + Logger.global.logInfo("Reloaded!"); + } catch (IOException | NoSuchResourceException e) { + Logger.global.logError("Failed to reload BlueMap!", e); + } + }) + .submit(SpongePlugin.getInstance()); + }) + .submit(SpongePlugin.getInstance()); + + } else { + Logger.global.logWarning("BlueMap is missing important resources!"); + Logger.global.logWarning("You need to accept the download of the required files in order of BlueMap to work!"); + Logger.global.logWarning("Please check: " + configFile); + Logger.global.logInfo("If you have changed the config you can simply reload the plugin using: /bluemap reload"); + } + } + + public SpongeExecutorService getSyncExecutor(){ + return syncExecutor; + } + + public SpongeExecutorService getAsyncExecutor(){ + return asyncExecutor; + } + + public World getWorld(UUID uuid){ + return worlds.get(uuid); + } + + public Collection getMapTypes(){ + return maps.values(); + } + + public RenderManager getRenderManager() { + return renderManager; + } + + public MapUpdateHandler getUpdateHandler() { + return updateHandler; + } + + public boolean isLoaded() { + return loaded; } public Path getConfigPath(){ return configurationDir; } - public static Object getPlugin() { - return plugin; + public static SpongePlugin getInstance() { + return instance; } }