Add SpongePlugin implementation and fix and improve a lot of different things

This commit is contained in:
Blue (Lukas Rieger) 2019-11-11 20:36:31 +01:00
parent 987cc5a01b
commit 3fd724c74b
32 changed files with 1304 additions and 44 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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;
}

View File

@ -37,7 +37,7 @@ import de.bluecolored.bluemap.core.world.BlockState;
public class DoorExtension implements BlockStateExtension {
private static final Collection<String> AFFECTED_BLOCK_IDS = Lists.newArrayList(
"minecraft:wooden_door",
"minecraft:oak_door",
"minecraft:iron_door",
"minecraft:spruce_door",
"minecraft:birch_door",

View File

@ -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<String> 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<String, String> prop : otherPlant.getProperties().entrySet()) {
if (!state.getProperties().containsKey(prop.getKey())) {
state = state.with(prop.getKey(), prop.getValue());
}
}
return otherPlant.with("half", "upper");
}
return state;

View File

@ -33,7 +33,21 @@ public class GlassPaneConnectExtension extends ConnectSameOrFullBlockExtension {
private static final HashSet<String> 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"
);

View File

@ -44,14 +44,14 @@ public class RedstoneExtension implements BlockStateExtension {
private static final Set<String> 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

View File

@ -35,15 +35,15 @@ import de.bluecolored.bluemap.core.world.BlockState;
public class SnowyExtension implements BlockStateExtension {
private static final Collection<String> 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");

View File

@ -38,7 +38,7 @@ public class StairShapeExtension implements BlockStateExtension {
private static final HashSet<String> 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) {

View File

@ -32,7 +32,7 @@ import com.google.common.collect.Sets;
public class WoodenFenceConnectExtension extends ConnectSameOrFullBlockExtension {
private static final HashSet<String> AFFECTED_BLOCK_IDS = Sets.newHashSet(
"minecraft:fence",
"minecraft:oak_fence",
"minecraft:spruce_fence",
"minecraft:birch_fence",
"minecraft:jungle_fence",

View File

@ -162,6 +162,12 @@ public class EmptyBlockContext implements ExtendedBlockContext {
public Collection<Vector2i> getChunkList(long modifiedSince) {
return Collections.emptyList();
}
@Override
public void invalidateChunkCache() {}
@Override
public void invalidateChunkCache(Vector2i chunk) {}
}

View File

@ -37,12 +37,14 @@ public interface ExtendedBlockContext extends BlockContext {
* This returns neighbour blocks.<br>
* The distance can not be larger than two blocks in each direction!<br>
*/
@Override
Block getRelativeBlock(Vector3i direction);
/**
* This returns neighbour blocks.<br>
* The distance can not be larger than two blocks in each direction!<br>
*/
@Override
default Block getRelativeBlock(int x, int y, int z){
return getRelativeBlock(new Vector3i(x, y, z));
}

View File

@ -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())
);
}

View File

@ -74,7 +74,7 @@ public class ResourceModelBuilder {
BlockStateModel model = new BlockStateModel();
for (WeighedArrayList<BlockModelResource> 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));
}

View File

@ -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);
}

View File

@ -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<Path, Resource> 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);
}
}

View File

@ -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();
}
}

View File

@ -74,5 +74,25 @@ public interface World extends WorldChunk {
* <i>(Be aware that the collection is not cached and recollected each time from the world-files!)</i>
*/
public Collection<Vector2i> 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
);
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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<org.spongepowered.api.world.World> 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.<org.spongepowered.api.world.World>getOne("world").orElse(null);
if (spongeWorld == null && source instanceof Locatable) {
Location<org.spongepowered.api.world.World> 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> uuid = args.<UUID>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> uuid = args.<UUID>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<Text> createStatusMessage(){
List<Text> 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<Vector2i> 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<Vector2i> 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!"));
}
}

View File

@ -0,0 +1,92 @@
/*
* This file is part of BlueMapSponge, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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;
}
}

View File

@ -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<MapType, Vector2i> 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<MapType> 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<BlockSnapshot> tr : evt.getTransactions()) {
if (!tr.isValid()) continue;
Optional<Location<org.spongepowered.api.world.World>> 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();
}
}

View File

@ -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<RenderTicket> renderTickets;
private Map<RenderTicket, RenderTicket> renderTicketMap;
private Deque<RenderTask> 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<RenderTicket> createTickets(MapType mapType, Collection<Vector2i> tiles) {
if (tiles.size() < 0) return Collections.emptyList();
Collection<RenderTicket> 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;
}
}

View File

@ -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<Vector2i> 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<Vector2i> 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;
}
}

View File

@ -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());
}
}

View File

@ -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<UUID, World> worlds;
private Map<String, MapType> 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<MapType> 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;
}
}