Change map-updating to rely on chunks rather than tiles

and trigger the updates on chunk-save-events rather than world-save-events if possible to make it more reliable.
This commit is contained in:
Blue (Lukas Rieger) 2020-09-20 21:12:15 +02:00
parent 5784491feb
commit e5a75f25e1
9 changed files with 204 additions and 65 deletions

View File

@ -24,7 +24,10 @@
*/
package de.bluecolored.bluemap.common.plugin;
import java.util.Iterator;
import java.util.Collection;
import java.util.HashSet;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import com.flowpowered.math.vector.Vector2i;
@ -35,53 +38,44 @@ import com.google.common.collect.MultimapBuilder;
import de.bluecolored.bluemap.common.MapType;
import de.bluecolored.bluemap.common.RenderManager;
import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.core.world.World;
public class MapUpdateHandler implements ServerEventListener {
private Plugin plugin;
private Multimap<MapType, Vector2i> updateBuffer;
private Multimap<UUID, Vector2i> updateBuffer;
private Timer flushTimer;
public MapUpdateHandler(Plugin plugin) {
this.plugin = plugin;
updateBuffer = MultimapBuilder.hashKeys().hashSetValues().build();
flushTimer = new Timer("MapUpdateHandlerTimer", true);
}
@Override
public void onWorldSaveToDisk(final UUID world) {
RenderManager renderManager = plugin.getRenderManager();
new Thread(() -> {
try {
Thread.sleep(5000); // wait 5 sec before rendering so saving has finished to avoid render-errors
synchronized (updateBuffer) {
Iterator<MapType> iterator = updateBuffer.keys().iterator();
while (iterator.hasNext()) {
MapType map = iterator.next();
if (map.getWorld().getUUID().equals(world)) {
//invalidate caches of updated chunks
for (Vector2i chunk : updateBuffer.get(map)) {
map.getWorld().invalidateChunkCache(chunk);
}
renderManager.createTickets(map, updateBuffer.get(map));
iterator.remove();
}
}
}
} catch (InterruptedException ignore) { Thread.currentThread().interrupt(); }
}).start();
// wait 5 sec before rendering so saving has finished
flushTimer.schedule(new TimerTask() {
@Override public void run() { flushUpdateBufferForWorld(world); }
}, 5000);
}
@Override
public void onChunkSaveToDisk(final UUID world, final Vector2i chunkPos) {
// wait 5 sec before rendering so saving has finished
flushTimer.schedule(new TimerTask() {
@Override public void run() { flushUpdateBufferForChunk(world, chunkPos); }
}, 5000);
}
@Override
public void onBlockChange(UUID world, Vector3i blockPos) {
synchronized (updateBuffer) {
updateBlock(world, blockPos);
}
updateBlock(world, blockPos);
}
@Override
@ -97,30 +91,22 @@ public class MapUpdateHandler implements ServerEventListener {
}
}
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);
private void updateChunk(UUID worldUUID, Vector2i chunkPos) {
World world = plugin.getWorld(worldUUID);
if (world == null) return;
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);
updateBuffer.put(worldUUID, chunkPos);
}
}
private void updateBlock(UUID world, Vector3i pos){
private void updateBlock(UUID worldUUID, Vector3i pos){
World world = plugin.getWorld(worldUUID);
if (world == null) return;
synchronized (updateBuffer) {
for (MapType mapType : plugin.getMapTypes()) {
if (mapType.getWorld().getUUID().equals(world)) {
Vector2i tile = mapType.getTileRenderer().getHiresModelManager().posToTile(pos);
updateBuffer.put(mapType, tile);
}
}
Vector2i chunkPos = world.blockPosToChunkPos(pos);
updateBuffer.put(worldUUID, chunkPos);
}
}
@ -128,21 +114,107 @@ public class MapUpdateHandler implements ServerEventListener {
return updateBuffer.size();
}
public void flushTileBuffer() {
public void flushUpdateBuffer() {
RenderManager renderManager = plugin.getRenderManager();
synchronized (updateBuffer) {
for (MapType map : updateBuffer.keySet()) {
//invalidate caches of updated chunks
for (Vector2i chunk : updateBuffer.get(map)) {
map.getWorld().invalidateChunkCache(chunk);
for (MapType map : plugin.getMapTypes()) {
Collection<Vector2i> chunks = updateBuffer.get(map.getWorld().getUUID());
Collection<Vector2i> tiles = new HashSet<>(chunks.size() * 2);
for (Vector2i chunk : chunks) {
Vector3i min = new Vector3i(chunk.getX() * 16, 0, chunk.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());
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(min));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(max));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(xmin));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(xmax));
}
renderManager.createTickets(map, updateBuffer.get(map));
//invalidate caches of updated chunks
for (Vector2i chunk : chunks) {
map.getWorld().invalidateChunkCache(chunk);
}
//create render-tickets
renderManager.createTickets(map, tiles);
}
updateBuffer.clear();
}
}
public void flushUpdateBufferForWorld(UUID world) {
RenderManager renderManager = plugin.getRenderManager();
synchronized (updateBuffer) {
for (MapType map : plugin.getMapTypes()) {
if (!map.getWorld().getUUID().equals(world)) continue;
Collection<Vector2i> chunks = updateBuffer.get(world);
Collection<Vector2i> tiles = new HashSet<>(chunks.size() * 2);
for (Vector2i chunk : chunks) {
Vector3i min = new Vector3i(chunk.getX() * 16, 0, chunk.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());
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(min));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(max));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(xmin));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(xmax));
}
//invalidate caches of updated chunks
for (Vector2i chunk : chunks) {
map.getWorld().invalidateChunkCache(chunk);
}
//create render-tickets
renderManager.createTickets(map, tiles);
}
updateBuffer.removeAll(world);
}
}
public void flushUpdateBufferForChunk(UUID world, Vector2i chunkPos) {
RenderManager renderManager = plugin.getRenderManager();
synchronized (updateBuffer) {
if (!updateBuffer.containsEntry(world, chunkPos)) return;
for (MapType map : plugin.getMapTypes()) {
if (!map.getWorld().getUUID().equals(world)) continue;
Collection<Vector2i> tiles = new HashSet<>(4);
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());
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(min));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(max));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(xmin));
tiles.add(map.getTileRenderer().getHiresModelManager().posToTile(xmax));
//invalidate caches of updated chunk
map.getWorld().invalidateChunkCache(chunkPos);
//create render-tickets
renderManager.createTickets(map, tiles);
}
updateBuffer.remove(world, chunkPos);
}
}
}

View File

@ -274,7 +274,7 @@ public class Plugin {
if (webServer != null) webServer.close();
//save render-manager state
if (updateHandler != null) updateHandler.flushTileBuffer(); //first write all buffered tiles to the render manager to save them too
if (updateHandler != null) updateHandler.flushUpdateBuffer(); //first write all buffered changes to the render manager to save them too
if (renderManager != null) {
try {
saveRenderManagerState();

View File

@ -35,6 +35,8 @@ public interface ServerEventListener {
default void onWorldSaveToDisk(UUID world) {};
default void onChunkSaveToDisk(UUID world, Vector2i chunkPos) {};
default void onBlockChange(UUID world, Vector3i blockPos) {};
default void onChunkFinishedGeneration(UUID world, Vector2i chunkPos) {};

View File

@ -42,8 +42,8 @@ import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedInEvent;
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedOutEvent;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
public class ForgeEventForwarder {
@ -104,6 +104,21 @@ public class ForgeEventForwarder {
}
}
@SubscribeEvent
public synchronized void onChunkSave(ChunkDataEvent.Save evt) {
if (!(evt.getWorld() instanceof ServerWorld)) return;
Vector2i chunkPos = new Vector2i(evt.getChunk().getPos().x, evt.getChunk().getPos().z);
try {
UUID world = mod.getUUIDForWorld((ServerWorld) evt.getWorld());
for (ServerEventListener listener : eventListeners) listener.onChunkSaveToDisk(world, chunkPos);
} catch (IOException e) {
Logger.global.noFloodError("Failed to get the UUID for a world!", e);
}
}
/* Use ChunkSaveToDisk as it is the preferred event to use and more reliable on the chunk actually saved to disk
@SubscribeEvent
public synchronized void onWorldSave(WorldEvent.Save evt) {
if (!(evt.getWorld() instanceof ServerWorld)) return;
@ -115,6 +130,7 @@ public class ForgeEventForwarder {
Logger.global.noFloodError("Failed to get the UUID for a world!", e);
}
}
*/
@SubscribeEvent
public synchronized void onPlayerJoin(PlayerLoggedInEvent evt) {

View File

@ -42,8 +42,8 @@ import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedInEvent;
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedOutEvent;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
public class ForgeEventForwarder {
@ -104,6 +104,21 @@ public class ForgeEventForwarder {
}
}
@SubscribeEvent
public synchronized void onChunkSave(ChunkDataEvent.Save evt) {
if (!(evt.getWorld() instanceof ServerWorld)) return;
Vector2i chunkPos = new Vector2i(evt.getChunk().getPos().x, evt.getChunk().getPos().z);
try {
UUID world = mod.getUUIDForWorld((ServerWorld) evt.getWorld());
for (ServerEventListener listener : eventListeners) listener.onChunkSaveToDisk(world, chunkPos);
} catch (IOException e) {
Logger.global.noFloodError("Failed to get the UUID for a world!", e);
}
}
/* Use ChunkSaveToDisk as it is the preferred event to use and more reliable on the chunk actually saved to disk
@SubscribeEvent
public synchronized void onWorldSave(WorldEvent.Save evt) {
if (!(evt.getWorld() instanceof ServerWorld)) return;
@ -115,6 +130,7 @@ public class ForgeEventForwarder {
Logger.global.noFloodError("Failed to get the UUID for a world!", e);
}
}
*/
@SubscribeEvent
public synchronized void onPlayerJoin(PlayerLoggedInEvent evt) {

View File

@ -42,8 +42,8 @@ import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedInEvent;
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedOutEvent;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
public class ForgeEventForwarder {
@ -103,7 +103,22 @@ public class ForgeEventForwarder {
Logger.global.noFloodError("Failed to get the UUID for a world!", e);
}
}
@SubscribeEvent
public synchronized void onChunkSave(ChunkDataEvent.Save evt) {
if (!(evt.getWorld() instanceof ServerWorld)) return;
Vector2i chunkPos = new Vector2i(evt.getChunk().getPos().x, evt.getChunk().getPos().z);
try {
UUID world = mod.getUUIDForWorld((ServerWorld) evt.getWorld());
for (ServerEventListener listener : eventListeners) listener.onChunkSaveToDisk(world, chunkPos);
} catch (IOException e) {
Logger.global.noFloodError("Failed to get the UUID for a world!", e);
}
}
/* Use ChunkSaveToDisk as it is the preferred event to use and more reliable on the chunk actually saved to disk
@SubscribeEvent
public synchronized void onWorldSave(WorldEvent.Save evt) {
if (!(evt.getWorld() instanceof ServerWorld)) return;
@ -115,6 +130,7 @@ public class ForgeEventForwarder {
Logger.global.noFloodError("Failed to get the UUID for a world!", e);
}
}
*/
@SubscribeEvent
public synchronized void onPlayerJoin(PlayerLoggedInEvent evt) {

View File

@ -46,7 +46,7 @@ import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkPopulateEvent;
import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
@ -70,10 +70,20 @@ public class EventForwarder implements Listener {
listeners.clear();
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public synchronized void onChunkSaveToDisk(ChunkUnloadEvent evt) {
if (evt.isSaveChunk()) {
Vector2i chunkPos = new Vector2i(evt.getChunk().getX(), evt.getChunk().getZ());
for (ServerEventListener listener : listeners) listener.onChunkSaveToDisk(evt.getWorld().getUID(), chunkPos);
}
}
/* Use ChunkSaveToDisk as it is the preferred event to use and more reliable on the chunk actually saved to disk
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public synchronized void onWorldSaveToDisk(WorldSaveEvent evt) {
for (ServerEventListener listener : listeners) listener.onWorldSaveToDisk(evt.getWorld().getUID());
}
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockChange(BlockPlaceEvent evt) {

View File

@ -1,5 +1,5 @@
dependencies {
shadow "org.spongepowered:spongeapi:7.2.0"
shadow "org.spongepowered:spongeapi:7.3.0"
compile group: 'org.bstats', name: 'bstats-sponge-lite', version: '1.5'
@ -16,7 +16,7 @@ dependencies {
build.dependsOn shadowJar {
destinationDir = file '../../build/release'
archiveFileName = "BlueMap-${version}-sponge-7.2.0.jar"
archiveFileName = "BlueMap-${version}-sponge-7.3.0.jar"
relocate 'net.querz.nbt', 'de.bluecolored.shadow.querz.nbt'
relocate 'org.apache.commons.io', 'de.bluecolored.shadow.apache.commons.io'

View File

@ -34,8 +34,8 @@ import org.spongepowered.api.event.block.ChangeBlockEvent;
import org.spongepowered.api.event.filter.type.Exclude;
import org.spongepowered.api.event.message.MessageChannelEvent;
import org.spongepowered.api.event.network.ClientConnectionEvent;
import org.spongepowered.api.event.world.SaveWorldEvent;
import org.spongepowered.api.event.world.chunk.PopulateChunkEvent;
import org.spongepowered.api.event.world.chunk.SaveChunkEvent;
import org.spongepowered.api.world.Location;
import com.flowpowered.math.vector.Vector2i;
@ -52,10 +52,17 @@ public class EventForwarder {
this.listener = listener;
}
/* Use ChunkSaveToDisk as it is the preferred event to use and more reliable on the chunk actually saved to disk
@Listener(order = Order.POST)
public void onWorldSaveToDisk(SaveWorldEvent evt) {
listener.onWorldSaveToDisk(evt.getTargetWorld().getUniqueId());
}
*/
@Listener(order = Order.POST)
public void onChunkSaveToDisk(SaveChunkEvent.Pre evt) {
listener.onChunkSaveToDisk(evt.getTargetChunk().getWorld().getUniqueId(), evt.getTargetChunk().getPosition().toVector2(true));
}
@Listener(order = Order.POST)
@Exclude({ChangeBlockEvent.Post.class, ChangeBlockEvent.Pre.class, ChangeBlockEvent.Modify.class})