diff --git a/Iris.iml b/Iris.iml index 4b6a86478..820d54702 100644 --- a/Iris.iml +++ b/Iris.iml @@ -17,6 +17,7 @@ + diff --git a/pom.xml b/pom.xml index 78b097a29..492d49abf 100644 --- a/pom.xml +++ b/pom.xml @@ -194,6 +194,12 @@ provided + + com.bergerkiller.bukkit + BKCommonLib + 1.16.4-v2 + v1 + com.sk89q.worldedit worldedit-bukkit diff --git a/src/main/java/com/volmit/iris/Iris.java b/src/main/java/com/volmit/iris/Iris.java index ab8677f0d..e7126b736 100644 --- a/src/main/java/com/volmit/iris/Iris.java +++ b/src/main/java/com/volmit/iris/Iris.java @@ -6,6 +6,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; +import com.volmit.iris.link.BKLink; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.World.Environment; @@ -64,6 +65,7 @@ public class Iris extends VolmitPlugin public static StructureManager struct; public static EditManager edit; public static IrisBoardManager board; + public static BKLink linkBK; public static MultiverseCoreLink linkMultiverseCore; public static MythicMobsLink linkMythicMobs; public static CitizensLink linkCitizens; @@ -193,6 +195,7 @@ public class Iris extends VolmitPlugin struct = new StructureManager(); board = new IrisBoardManager(); linkMultiverseCore = new MultiverseCoreLink(); + linkBK = new BKLink(); linkMythicMobs = new MythicMobsLink(); edit = new EditManager(); J.a(() -> IO.delete(getTemp())); diff --git a/src/main/java/com/volmit/iris/link/BKLink.java b/src/main/java/com/volmit/iris/link/BKLink.java new file mode 100644 index 000000000..96e263117 --- /dev/null +++ b/src/main/java/com/volmit/iris/link/BKLink.java @@ -0,0 +1,50 @@ +package com.volmit.iris.link; + +import com.bergerkiller.bukkit.common.utils.BlockUtil; +import com.bergerkiller.bukkit.common.utils.ChunkUtil; +import com.volmit.iris.util.KList; +import com.volmit.iris.v2.lighting.LightingChunk; +import com.volmit.iris.v2.lighting.LightingService; +import com.volmit.iris.v2.scaffold.parallel.MultiBurst; +import io.lumine.xikage.mythicmobs.MythicMobs; +import io.lumine.xikage.mythicmobs.mobs.MythicMob; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.plugin.Plugin; + +public class BKLink +{ + public BKLink() + { + + } + + public void updateBlock(Block b) { + BlockData d = b.getBlockData(); + b.setType(Material.AIR, false); + b.setBlockData(d, true); + } + + public boolean supported() + { + return getBK() != null; + } + + public Plugin getBK() + { + Plugin p = Bukkit.getPluginManager().getPlugin("BKCommonLib"); + + if(p == null) + { + return null; + } + + return p; + } +} diff --git a/src/main/java/com/volmit/iris/manager/BlockSignal.java b/src/main/java/com/volmit/iris/manager/BlockSignal.java index ce9c1f7b7..8a12d0c5e 100644 --- a/src/main/java/com/volmit/iris/manager/BlockSignal.java +++ b/src/main/java/com/volmit/iris/manager/BlockSignal.java @@ -1,6 +1,8 @@ package com.volmit.iris.manager; import com.volmit.iris.util.J; +import com.volmit.iris.v2.scaffold.parallel.MultiBurst; +import net.minecraft.server.v1_16_R2.BlockSign; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; @@ -9,9 +11,20 @@ import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Entity; import org.bukkit.entity.FallingBlock; +import org.bukkit.entity.Player; import org.bukkit.util.Vector; public class BlockSignal { + public static void of(Block block, int ticks) + { + new BlockSignal(block, ticks); + } + + public static void of(Block block) + { + of(block, 100); + } + public BlockSignal(Block block, int ticks) { Location tg = block.getLocation().clone().add(0.5, 0, 0.5).clone(); @@ -28,8 +41,13 @@ public class BlockSignal { J.s(() -> { e.remove(); BlockData type = block.getBlockData(); - block.setType(Material.AIR, false); - block.setBlockData(type, false); + + MultiBurst.burst.lazy(() -> { + for(Player i : block.getWorld().getPlayers()) + { + i.sendBlockChange(block.getLocation(), block.getBlockData()); + } + }); }, ticks); } } diff --git a/src/main/java/com/volmit/iris/noise/CNG.java b/src/main/java/com/volmit/iris/noise/CNG.java index 7f4c74a64..80caca1d0 100644 --- a/src/main/java/com/volmit/iris/noise/CNG.java +++ b/src/main/java/com/volmit/iris/noise/CNG.java @@ -2,7 +2,6 @@ package com.volmit.iris.noise; import java.util.List; -import com.oracle.webservices.internal.api.databinding.DatabindingMode; import com.volmit.iris.Iris; import com.volmit.iris.v2.scaffold.stream.ProceduralStream; import com.volmit.iris.v2.scaffold.stream.sources.CNGStream; diff --git a/src/main/java/com/volmit/iris/util/B.java b/src/main/java/com/volmit/iris/util/B.java index e56b67112..1312065cd 100644 --- a/src/main/java/com/volmit/iris/util/B.java +++ b/src/main/java/com/volmit/iris/util/B.java @@ -353,6 +353,11 @@ public class B //@done } + public static boolean isTrulyLit(BlockData mat) + { + return isLit(mat) || mat.getMaterial().equals(Material.LAVA); + } + public static boolean isLit(BlockData mat) { // @builder diff --git a/src/main/java/com/volmit/iris/v2/generator/IrisEngine.java b/src/main/java/com/volmit/iris/v2/generator/IrisEngine.java index cdb9e34e0..70244922b 100644 --- a/src/main/java/com/volmit/iris/v2/generator/IrisEngine.java +++ b/src/main/java/com/volmit/iris/v2/generator/IrisEngine.java @@ -1,25 +1,26 @@ package com.volmit.iris.v2.generator; -import com.sun.org.apache.xpath.internal.operations.Mult; import com.volmit.iris.Iris; +import com.volmit.iris.link.BKLink; +import com.volmit.iris.manager.BlockSignal; import com.volmit.iris.object.*; import com.volmit.iris.util.*; import com.volmit.iris.v2.scaffold.cache.Cache; import com.volmit.iris.v2.scaffold.engine.Engine; import com.volmit.iris.v2.scaffold.engine.EngineFramework; import com.volmit.iris.v2.scaffold.engine.EngineTarget; +import com.volmit.iris.v2.scaffold.engine.EngineWorldManager; import com.volmit.iris.v2.scaffold.hunk.Hunk; import com.volmit.iris.v2.scaffold.parallel.MultiBurst; -import io.papermc.lib.PaperLib; import lombok.Getter; import lombok.Setter; -import net.minecraft.server.v1_16_R2.*; import org.bukkit.Chunk; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Biome; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_16_R2.block.CraftBlock; import org.bukkit.generator.BlockPopulator; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; @@ -38,6 +39,9 @@ public class IrisEngine extends BlockPopulator implements Engine @Getter private final EngineFramework framework; + @Getter + private final EngineWorldManager worldManager; + @Setter @Getter private volatile int parallelism; @@ -51,9 +55,17 @@ public class IrisEngine extends BlockPopulator implements Engine Iris.info("Initializing Engine: " + target.getWorld().getName() + "/" + target.getDimension().getLoadKey() + " (" + target.getHeight() + " height)"); this.target = target; this.framework = new IrisEngineFramework(this); + worldManager = new IrisWorldManager(this); minHeight = 0; } + @Override + public void close() + { + getWorldManager().close(); + getFramework().close(); + } + @Override public double modifyX(double x) { return x / getDimension().getTerrainZoom(); @@ -97,13 +109,30 @@ public class IrisEngine extends BlockPopulator implements Engine if(B.isUpdatable(data)) { getParallax().updateBlock(x,y,z); + getParallax().getMetaRW(x>>4, z>>4).setUpdates(true); } } @Override public void populate(@NotNull World world, @NotNull Random random, @NotNull Chunk c) { + getWorldManager().spawnInitialEntities(c); + updateChunk(c); + } + public void updateChunk(Chunk c) + { + if(getParallax().getMetaR(c.getX(), c.getZ()).isUpdates()) + { + Hunk b = getParallax().getUpdatesR(c.getX(), c.getZ()); + + b.iterateSync((x,y,z,v) -> { + if(v != null && v) + { + update(x,y,z, c, new RNG(Cache.key(c.getX(), c.getZ()))); + } + }); + } } private void update(int x, int y, int z, Chunk c, RNG rf) @@ -114,8 +143,6 @@ public class IrisEngine extends BlockPopulator implements Engine if(B.isStorage(data)) { RNG rx = rf.nextParallelRNG(x).nextParallelRNG(z).nextParallelRNG(y); - block.setType(Material.AIR, false); - block.setBlockData(data, true); InventorySlotType slot = null; if(B.isStorageChest(data)) @@ -143,8 +170,7 @@ public class IrisEngine extends BlockPopulator implements Engine else if(B.isLit(data)) { - block.setType(Material.AIR, false); - block.setBlockData(data, true); + Iris.linkBK.updateBlock(block); } } diff --git a/src/main/java/com/volmit/iris/v2/generator/IrisEngineFramework.java b/src/main/java/com/volmit/iris/v2/generator/IrisEngineFramework.java index afd9a77d0..234fb1cad 100644 --- a/src/main/java/com/volmit/iris/v2/generator/IrisEngineFramework.java +++ b/src/main/java/com/volmit/iris/v2/generator/IrisEngineFramework.java @@ -20,7 +20,7 @@ public class IrisEngineFramework implements EngineFramework { private final IrisComplex complex; @Getter - final EngineParallax engineParallax; + final EngineParallaxManager engineParallax; @Getter private final EngineActuator terrainActuator; @@ -56,4 +56,17 @@ public class IrisEngineFramework implements EngineFramework { this.caveModifier = new IrisCaveModifier(engine); this.postModifier = new IrisPostModifier(engine); } + + @Override + public void close() + { + getEngineParallax().close(); + getTerrainActuator().close(); + getDecorantActuator().close(); + getBiomeActuator().close(); + getDepositModifier().close(); + getRavineModifier().close(); + getCaveModifier().close(); + getPostModifier().close(); + } } diff --git a/src/main/java/com/volmit/iris/v2/generator/IrisEngineParallax.java b/src/main/java/com/volmit/iris/v2/generator/IrisEngineParallax.java index 7c21a6d02..f7102d0cf 100644 --- a/src/main/java/com/volmit/iris/v2/generator/IrisEngineParallax.java +++ b/src/main/java/com/volmit/iris/v2/generator/IrisEngineParallax.java @@ -1,17 +1,16 @@ package com.volmit.iris.v2.generator; import com.volmit.iris.v2.scaffold.engine.Engine; -import com.volmit.iris.v2.scaffold.engine.EngineFramework; -import com.volmit.iris.v2.scaffold.engine.EngineParallax; -import com.volmit.iris.v2.scaffold.engine.EngineStructure; +import com.volmit.iris.v2.scaffold.engine.EngineParallaxManager; +import com.volmit.iris.v2.scaffold.engine.EngineStructureManager; import lombok.Getter; -public class IrisEngineParallax implements EngineParallax { +public class IrisEngineParallax implements EngineParallaxManager { @Getter private final Engine engine; @Getter - private final EngineStructure structureManager; + private final EngineStructureManager structureManager; @Getter private final int parallaxSize; diff --git a/src/main/java/com/volmit/iris/v2/generator/IrisEngineStructure.java b/src/main/java/com/volmit/iris/v2/generator/IrisEngineStructure.java index 138fa3ce1..a973a7755 100644 --- a/src/main/java/com/volmit/iris/v2/generator/IrisEngineStructure.java +++ b/src/main/java/com/volmit/iris/v2/generator/IrisEngineStructure.java @@ -1,9 +1,9 @@ package com.volmit.iris.v2.generator; import com.volmit.iris.v2.scaffold.engine.Engine; -import com.volmit.iris.v2.scaffold.engine.EngineAssignedStructure; +import com.volmit.iris.v2.scaffold.engine.EngineAssignedStructureManager; -public class IrisEngineStructure extends EngineAssignedStructure { +public class IrisEngineStructure extends EngineAssignedStructureManager { public IrisEngineStructure(Engine engine) { super(engine); } diff --git a/src/main/java/com/volmit/iris/v2/generator/IrisWorldManager.java b/src/main/java/com/volmit/iris/v2/generator/IrisWorldManager.java new file mode 100644 index 000000000..b2b9ca9c7 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/generator/IrisWorldManager.java @@ -0,0 +1,44 @@ +package com.volmit.iris.v2.generator; + +import com.volmit.iris.v2.scaffold.engine.Engine; +import com.volmit.iris.v2.scaffold.engine.EngineAssignedWorldManager; +import org.bukkit.Chunk; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntitySpawnEvent; + +public class IrisWorldManager extends EngineAssignedWorldManager { + public IrisWorldManager(Engine engine) { + super(engine); + } + + @Override + public void onEntitySpawn(EntitySpawnEvent e) { + + } + + @Override + public void onTick() { + + } + + @Override + public void onSave() { + getEngine().getParallax().saveAll(); + } + + @Override + public void spawnInitialEntities(Chunk chunk) { + + } + + @Override + public void onBlockBreak(BlockBreakEvent e) { + + } + + @Override + public void onBlockPlace(BlockPlaceEvent e) { + + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/BlockFaceSetSection.java b/src/main/java/com/volmit/iris/v2/lighting/BlockFaceSetSection.java new file mode 100644 index 000000000..1558dfa4c --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/BlockFaceSetSection.java @@ -0,0 +1,18 @@ +package com.volmit.iris.v2.lighting; + +import com.bergerkiller.bukkit.common.collections.BlockFaceSet; + +/** + * Maps {@link BlockFaceSet} values to a 16x16x16 area of blocks + */ +public class BlockFaceSetSection { + private final byte[] _maskData = new byte[4096]; + + public void set(int x, int y, int z, BlockFaceSet faces) { + _maskData[(y << 8) | (z << 4) | x] = (byte) faces.mask(); + } + + public BlockFaceSet get(int x, int y, int z) { + return BlockFaceSet.byMask((int) _maskData[(y << 8) | (z << 4) | x]); + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/FlatRegionInfo.java b/src/main/java/com/volmit/iris/v2/lighting/FlatRegionInfo.java new file mode 100644 index 000000000..c068fad94 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/FlatRegionInfo.java @@ -0,0 +1,153 @@ +package com.volmit.iris.v2.lighting; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.stream.IntStream; + +import org.bukkit.World; + +import com.bergerkiller.bukkit.common.utils.WorldUtil; + +/** + * Loads region information, storing whether or not + * the 32x32 (1024) chunks are available. + */ +public class FlatRegionInfo { + private static final int[] DEFAULT_RY_0 = new int[] {0}; // Optimization + public final World world; + public final int rx, rz; + public final int[] ry; + public final int cx, cz; + private final BitSet _chunks; + private boolean _loadedFromDisk; + + public FlatRegionInfo(World world, int rx, int ry, int rz) { + this(world, rx, (ry==0) ? DEFAULT_RY_0 : new int[] {ry}, rz); + } + + public FlatRegionInfo(World world, int rx, int[] ry, int rz) { + this.world = world; + this.rx = rx; + this.rz = rz; + this.ry = ry; + this.cx = (rx << 5); + this.cz = (rz << 5); + this._chunks = new BitSet(1024); + this._loadedFromDisk = false; + } + + private FlatRegionInfo(FlatRegionInfo copy, int[] new_ry) { + this.world = copy.world; + this.rx = copy.rx; + this.ry = new_ry; + this.rz = copy.rz; + this.cx = copy.cx; + this.cz = copy.cz; + this._chunks = copy._chunks; + this._loadedFromDisk = copy._loadedFromDisk; + } + + public void addChunk(int cx, int cz) { + cx -= this.cx; + cz -= this.cz; + if (cx < 0 || cx >= 32 || cz < 0 || cz >= 32) { + return; + } + this._chunks.set((cz << 5) | cx); + } + + /** + * Gets the number of chunks in this region. + * If not loaded yet, the default 1024 is returned. + * + * @return chunk count + */ + public int getChunkCount() { + return this._chunks.cardinality(); + } + + /** + * Gets the region Y-coordinates as a sorted, immutable distinct stream + * + * @return ry int stream + */ + public IntStream getRYStream() { + return IntStream.of(this.ry); + } + + /** + * Loads the region information, now telling what chunks are contained + */ + public void load() { + if (!this._loadedFromDisk) { + this._loadedFromDisk = true; + for (int ry : this.ry) { + this._chunks.or(WorldUtil.getWorldSavedRegionChunks3(this.world, this.rx, ry, this.rz)); + } + } + } + + /** + * Ignores loading region chunk information from chunks that aren't loaded + */ + public void ignoreLoad() { + this._loadedFromDisk = true; + } + + /** + * Gets whether the chunk coordinates specified are within the range + * of coordinates of this region + * + * @param cx - chunk coordinates (world coordinates) + * @param cz - chunk coordinates (world coordinates) + * @return True if in range + */ + public boolean isInRange(int cx, int cz) { + cx -= this.cx; + cz -= this.cz; + return cx >= 0 && cz >= 0 && cx < 32 && cz < 32; + } + + /** + * Gets whether a chunk is contained and exists inside this region + * + * @param cx - chunk coordinates (world coordinates) + * @param cz - chunk coordinates (world coordinates) + * @return True if the chunk is contained + */ + public boolean containsChunk(int cx, int cz) { + cx -= this.cx; + cz -= this.cz; + if (cx < 0 || cx >= 32 || cz < 0 || cz >= 32) { + return false; + } + + // Load region file information the first time this is accessed + this.load(); + + // Check in bitset + return this._chunks.get((cz << 5) | cx); + } + + /** + * Adds another Region Y-coordinate to the list. + * The set of chunks and other properties are copied. + * + * @param ry + * @return new flat region info object with updated ry + */ + public FlatRegionInfo addRegionYCoordinate(int ry) { + int index = Arrays.binarySearch(this.ry, ry); + if (index >= 0) { + return this; // Already contained + } + + // Insert at this index (undo insertion point - 1) + index = -index - 1; + int[] new_y_coordinates = new int[this.ry.length + 1]; + System.arraycopy(this.ry, 0, new_y_coordinates, 0, index); + new_y_coordinates[index] = ry; + System.arraycopy(this.ry, index, new_y_coordinates, index+1, this.ry.length - index); + return new FlatRegionInfo(this, new_y_coordinates); + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/FlatRegionInfoMap.java b/src/main/java/com/volmit/iris/v2/lighting/FlatRegionInfoMap.java new file mode 100644 index 000000000..c72188598 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/FlatRegionInfoMap.java @@ -0,0 +1,188 @@ +package com.volmit.iris.v2.lighting; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.IntStream; + +import org.bukkit.Chunk; +import org.bukkit.World; + +import com.bergerkiller.bukkit.common.bases.IntVector3; +import com.bergerkiller.bukkit.common.utils.MathUtil; +import com.bergerkiller.bukkit.common.utils.WorldUtil; +import com.bergerkiller.bukkit.common.wrappers.LongHashMap; + +/** + * A map of region information + */ +public class FlatRegionInfoMap { + private final World _world; + private final LongHashMap _regions; + + private FlatRegionInfoMap(World world, LongHashMap regions) { + this._world = world; + this._regions = regions; + } + + public World getWorld() { + return this._world; + } + + public int getRegionCount() { + return this._regions.size(); + } + + public Collection getRegions() { + return this._regions.getValues(); + } + + public FlatRegionInfo getRegion(int rx, int rz) { + return this._regions.get(rx, rz); + } + + public FlatRegionInfo getRegionAtChunk(int cx, int cz) { + return this._regions.get(cx >> 5, cz >> 5); + } + + /** + * Gets whether a chunk exists + * + * @param cx + * @param cz + * @return True if the chunk exists + */ + public boolean containsChunk(int cx, int cz) { + FlatRegionInfo region = getRegionAtChunk(cx, cz); + return region != null && region.containsChunk(cx, cz); + } + + /** + * Gets whether a chunk, and all its 8 neighbours, exist + * + * @param cx + * @param cz + * @return True if the chunk and all its neighbours exist + */ + public boolean containsChunkAndNeighbours(int cx, int cz) { + FlatRegionInfo region = getRegionAtChunk(cx, cz); + if (region == null) { + return false; + } + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) { + int mx = cx + dx; + int mz = cz + dz; + if (region.isInRange(mx, mz)) { + if (!region.containsChunk(mx, mz)) { + return false; + } + } else { + if (!this.containsChunk(mx, mz)) { + return false; + } + } + } + } + return true; + } + + /** + * Computes all the region Y-coordinates used by a region and its neighbouring 8 regions. + * The returned array is sorted in increasing order and is distinct (no duplicate values). + * + * @param region + * @return region and neighbouring regions' Y-coordinates + */ + public int[] getRegionYCoordinatesSelfAndNeighbours(FlatRegionInfo region) { + IntStream region_y_coord_stream = region.getRYStream(); + for (int drx = -1; drx <= 1; drx++) { + for (int drz = -1; drz <= 1; drz++) { + if (drx == 0 && drz == 0) { + continue; + } + + FlatRegionInfo neigh_region = this.getRegion(region.rx + drx, region.rz + drz); + if (neigh_region != null) { + region_y_coord_stream = IntStream.concat(region_y_coord_stream, neigh_region.getRYStream()); + } + } + } + + //TODO: There's technically a way to significantly speed up sorting two concatenated sorted streams + // Sadly, the java 8 SDK doesn't appear to do any optimizations here :( + return region_y_coord_stream.sorted().distinct().toArray(); + } + + /** + * Creates a region information mapping of all existing chunks of a world + * that are currently loaded. No further loading is required. + * + * @param world + * @return region info map + */ + public static FlatRegionInfoMap createLoaded(World world) { + LongHashMap regions = new LongHashMap(); + for (Chunk chunk : world.getLoadedChunks()) { + int rx = WorldUtil.chunkToRegionIndex(chunk.getX()); + int rz = WorldUtil.chunkToRegionIndex(chunk.getZ()); + FlatRegionInfo prev_info = regions.get(rx, rz); + FlatRegionInfo new_info = prev_info; + if (new_info == null) { + new_info = new FlatRegionInfo(world, rx, 0, rz); + new_info.ignoreLoad(); + } + + // Refresh y-coordinates + for (Integer y_coord : WorldUtil.getLoadedSectionCoordinates(chunk)) { + new_info = new_info.addRegionYCoordinate(WorldUtil.chunkToRegionIndex(y_coord.intValue())); + } + + // Add chunk to region bitset + new_info.addChunk(chunk.getX(), chunk.getZ()); + + // Store if new or changed + if (new_info != prev_info) { + regions.put(rx, rz, new_info); + } + } + + return new FlatRegionInfoMap(world, regions); + } + + /** + * Creates a region information mapping of all existing chunks of a world + * + * @param world + * @return region info map + */ + public static FlatRegionInfoMap create(World world) { + LongHashMap regions = new LongHashMap(); + + // Obtain the region coordinates in 3d space (vertical too!) + Set regionCoordinates = WorldUtil.getWorldRegions3(world); + + // For each region, create a RegionInfo entry + for (IntVector3 region : regionCoordinates) { + long key = MathUtil.longHashToLong(region.x, region.z); + FlatRegionInfo prev = regions.get(key); + if (prev != null) { + regions.put(key, prev.addRegionYCoordinate(region.y)); + } else { + regions.put(key, new FlatRegionInfo(world, region.x, region.y, region.z)); + } + } + + // For all loaded chunks, add those chunks to their region up-front + // They may not yet have been saved to the region file + for (Chunk chunk : world.getLoadedChunks()) { + int rx = WorldUtil.chunkToRegionIndex(chunk.getX()); + int rz = WorldUtil.chunkToRegionIndex(chunk.getZ()); + FlatRegionInfo info = regions.get(rx, rz); + if (info != null) { + info.addChunk(chunk.getX(), chunk.getZ()); + } + } + + return new FlatRegionInfoMap(world, regions); + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingAutoClean.java b/src/main/java/com/volmit/iris/v2/lighting/LightingAutoClean.java new file mode 100644 index 000000000..e3a8920d6 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingAutoClean.java @@ -0,0 +1,101 @@ +package com.volmit.iris.v2.lighting; + +import java.util.HashMap; + +import com.volmit.iris.Iris; +import org.bukkit.World; + +import com.bergerkiller.bukkit.common.Task; +import com.bergerkiller.bukkit.common.utils.WorldUtil; +import com.bergerkiller.bukkit.common.wrappers.LongHashSet; + +/** + * Handles the automatic cleanup of chunk lighting when chunks are generated + */ +public class LightingAutoClean { + private static HashMap queues = new HashMap(); + private static Task autoCleanTask = null; + + /** + * Checks all neighbouring chunks to see if they are fully surrounded by chunks (now), and + * schedules lighting repairs. This function only does anything when automatic cleaning is activated. + * + * @param world the chunk is in + * @param chunkX coordinate + * @param chunkZ coordinate + */ + public static void handleChunkGenerated(World world, int chunkX, int chunkZ) { + + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + if (!WorldUtil.isChunkAvailable(world, chunkX + dx, chunkZ + dz)) { + continue; + } + + // Check that all chunks surrounding this chunk are all available + boolean allNeighboursLoaded = true; + for (int dx2 = -1; dx2 <= 1 && allNeighboursLoaded; dx2++) { + for (int dz2 = -1; dz2 <= 1 && allNeighboursLoaded; dz2++) { + if (dx2 == 0 && dz2 == 0) { + continue; // ignore self + } + if (dx2 == -dx && dz2 == -dz) { + continue; // ignore the original generated chunk + } + allNeighboursLoaded &= WorldUtil.isChunkAvailable(world, chunkX + dx + dx2, chunkZ + dz + dz2); + } + } + + // If all neighbours are available, schedule it for fixing + if (allNeighboursLoaded) { + schedule(world, chunkX + dx, chunkZ + dz); + } + } + } + } + + private static synchronized void processAutoClean() { + while (queues.size() > 0) { + World world = queues.keySet().iterator().next(); + LongHashSet chunks = queues.remove(world); + LightingService.schedule(world, chunks); + } + } + + public static void schedule(World world, int chunkX, int chunkZ) { + schedule(world, chunkX, chunkZ, 80); + } + + public static synchronized void schedule(World world, int chunkX, int chunkZ, int tickDelay) { + LongHashSet queue = queues.get(world); + if (queue == null) { + queue = new LongHashSet(9); + queues.put(world, queue); + } + + // Queue this chunk, and all its neighbours + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + queue.add(chunkX + dx, chunkZ + dz); + } + } + + // Initialize clean task if it hasn't been yet + if (autoCleanTask == null) { + autoCleanTask = new Task(Iris.instance) { + @Override + public void run() { + processAutoClean(); + } + }; + } + + // Postpone the tick task while there are less than 100 chunks in the queue + if (queue.size() < 100) { + autoCleanTask.stop().start(tickDelay); + } + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingCategory.java b/src/main/java/com/volmit/iris/v2/lighting/LightingCategory.java new file mode 100644 index 000000000..aa9603976 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingCategory.java @@ -0,0 +1,198 @@ +package com.volmit.iris.v2.lighting; + +import com.bergerkiller.bukkit.common.collections.BlockFaceSet; + +/** + * Represents a category of light being processed. All conditional logic + * for this is handled by this class. + */ +public enum LightingCategory { + SKY() { + @Override + public String getName() { + return "Sky"; + } + + @Override + public void initialize(LightingChunk chunk) { + if (!chunk.hasSkyLight) { + return; + } + + // Find out the highest possible Y-position + int x, y, z, light, height, opacity; + BlockFaceSet opaqueFaces; + LightingCube cube = null; + // Apply initial sky lighting from top to bottom + for (z = chunk.start.z; z <= chunk.end.z; z++) { + for (x = chunk.start.x; x <= chunk.end.x; x++) { + light = 15; + height = chunk.getHeight(x, z) + 1; + for (y = chunk.maxY; y >= chunk.minY; y--) { + if ((cube = chunk.nextCube(cube, y)) == null) { + // Skip the remaining 15: they are all inaccessible as well + y -= 15; + + // If not full skylight, reset light level, assuming it dimmed out + if (light != 15) { + light = 0; + } + continue; + } + + // Set quickly when light level is at 0, or we are above height level + if (y > height || light <= 0) { + cube.skyLight.set(x, y & 0xf, z, light); + continue; + } + + // If opaque at the top, set light to 0 instantly + opaqueFaces = cube.getOpaqueFaces(x, y & 0xf, z); + if (opaqueFaces.up()) { + light = 0; + } else { + // Apply the opacity to the light level + opacity = cube.opacity.get(x, y & 0xf, z); + if (light < 15 && opacity == 0) { + opacity = 1; + } + if ((light -= opacity) <= 0) { + light = 0; + } + } + + // Apply sky light to block + cube.skyLight.set(x, y & 0xf, z, light); + + // If opaque at the bottom, reset light to 0 for next block + // The block itself is lit + if (opaqueFaces.down()) { + light = 0; + } + } + } + } + } + + @Override + public int getStartY(LightingChunk chunk, int x, int z) { + return chunk.getHeight(x, z); + } + + @Override + public void setDirty(LightingChunk chunk, boolean dirty) { + chunk.isSkyLightDirty = dirty; + } + + @Override + public int get(LightingCube section, int x, int y, int z) { + return section.skyLight.get(x, y, z); + } + + @Override + public void set(LightingCube section, int x, int y, int z, int level) { + section.skyLight.set(x, y, z, level); + } + }, + BLOCK() { + @Override + public String getName() { + return "Block"; + } + + @Override + public void initialize(LightingChunk chunk) { + // Some blocks that emit light, also have opaque faces + // They still emit light through the opaque faces to other blocks + // To fix this, run an initial processing step that spreads all + // emitted light to the neighbouring blocks' block light, ignoring own opaque faces + int x, y, z; + for (LightingCube cube : chunk.getSections()) { + for (y = 0; y < 16; y++) { + for (z = chunk.start.z; z <= chunk.end.z; z++) { + for (x = chunk.start.x; x <= chunk.end.x; x++) { + cube.spreadBlockLight(x, y, z); + } + } + } + } + } + + @Override + public int getStartY(LightingChunk chunk, int x, int z) { + return chunk.maxY; + } + + @Override + public void setDirty(LightingChunk chunk, boolean dirty) { + chunk.isBlockLightDirty = dirty; + } + + @Override + public int get(LightingCube section, int x, int y, int z) { + return section.blockLight.get(x, y, z); + } + + @Override + public void set(LightingCube section, int x, int y, int z, int level) { + section.blockLight.set(x, y, z, level); + } + }; + + /** + * Gets the name of this type of light, used when logging + * + * @return category name + */ + public abstract String getName(); + + /** + * Initializes the lighting in the chunk for this category + * + * @param chunk + */ + public abstract void initialize(LightingChunk chunk); + + /** + * Gets the y-coordinate to start processing from when spreading light around + * + * @param chunk + * @param x + * @param z + * @return start y-coordinate + */ + public abstract int getStartY(LightingChunk chunk, int x, int z); + + /** + * Sets whether this category of light is dirty, indicating this category of light is all good, + * or that more work is needed spreading light around. + * + * @param chunk + * @param dirty + */ + public abstract void setDirty(LightingChunk chunk, boolean dirty); + + /** + * Gets the light level in a section at the coordinates specified. + * No bounds checking is performed. + * + * @param section + * @param x + * @param y + * @param z + * @return light level + */ + public abstract int get(LightingCube section, int x, int y, int z); + + /** + * Sets the light level in a section at the coordinates specified. + * No bounds checking is performed. + * + * @param section + * @param x + * @param y + * @param z + * @param level + */ + public abstract void set(LightingCube section, int x, int y, int z, int level); +} \ No newline at end of file diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingChunk.java b/src/main/java/com/volmit/iris/v2/lighting/LightingChunk.java new file mode 100644 index 000000000..0facf6e34 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingChunk.java @@ -0,0 +1,458 @@ +package com.volmit.iris.v2.lighting; + +import com.bergerkiller.bukkit.common.bases.IntVector2; +import com.bergerkiller.bukkit.common.chunk.ForcedChunk; +import com.bergerkiller.bukkit.common.collections.BlockFaceSet; +import com.bergerkiller.bukkit.common.utils.ChunkUtil; +import com.bergerkiller.bukkit.common.utils.WorldUtil; +import com.bergerkiller.bukkit.common.wrappers.ChunkSection; +import com.bergerkiller.bukkit.common.wrappers.HeightMap; +import com.bergerkiller.bukkit.common.wrappers.IntHashMap; +import com.bergerkiller.generated.net.minecraft.server.ChunkHandle; + +import com.volmit.iris.Iris; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Represents a single chunk full with lighting-relevant information. + * Initialization and use of this chunk in the process is as follows:
+ * - New lighting chunks are created for all chunks to be processed
+ * - notifyAccessible is called for all chunks, passing in all chunks
+ * - fill/fillSection is called for all chunks, after which initLight is called
+ * - spread is called on all chunks until all spreading is finished
+ * - data from all LightingChunks/Sections is gathered and saved to chunks or region files
+ * - possible chunk resends are performed + */ +public class LightingChunk { + public static final int OB = ~0xf; // Outside blocks + public static final int OC = ~0xff; // Outside chunk + public IntHashMap sections; + public final LightingChunkNeighboring neighbors = new LightingChunkNeighboring(); + public final int[] heightmap = new int[256]; + public final World world; + public final int chunkX, chunkZ; + public boolean hasSkyLight = true; + public boolean isSkyLightDirty = true; + public boolean isBlockLightDirty = true; + public boolean isFilled = false; + public boolean isApplied = false; + public IntVector2 start = new IntVector2(1, 1); + public IntVector2 end = new IntVector2(14, 14); + public int minY = 0; + public int maxY = 0; + public final ForcedChunk forcedChunk = ForcedChunk.none(); + public volatile boolean loadingStarted = false; + + public LightingChunk(World world, int x, int z) { + this.world = world; + this.chunkX = x; + this.chunkZ = z; + } + + /** + * Gets all the sections inside this chunk. + * Elements are never null. + * + * @return sections + */ + public Collection getSections() { + return this.sections.values(); + } + + /** + * Efficiently iterates the vertical cubes of a chunk, only + * querying the lookup table every 16 blocks + * + * @param previous The previous cube we iterated + * @param y Block y-coordinate + * @return the cube at the Block y-coordinate, or null if this cube does not exist + */ + public LightingCube nextCube(LightingCube previous, int y) { + int cy = y >> 4; + if (previous != null && previous.cy == cy) { + return previous; + } else { + return this.sections.get(cy); + } + } + + /** + * Notifies that a new chunk is accessible. + * + * @param chunk that is accessible + */ + public void notifyAccessible(LightingChunk chunk) { + final int dx = chunk.chunkX - this.chunkX; + final int dz = chunk.chunkZ - this.chunkZ; + // Only check neighbours, ignoring the corners and self + if (Math.abs(dx) > 1 || Math.abs(dz) > 1 || (dx != 0) == (dz != 0)) { + return; + } + // Results in -16, 16 or 0 for the x/z coordinates + neighbors.set(dx, dz, chunk); + // Update start/end coordinates + if (dx == 1) { + end = new IntVector2(15, end.z); + } else if (dx == -1) { + start = new IntVector2(0, start.z); + } else if (dz == 1) { + end = new IntVector2(end.x, 15); + } else if (dz == -1) { + start = new IntVector2(start.x, 0); + } + } + + /** + * Initializes the neighboring cubes of all the cubes of this + * lighting chunk. This initializes the neighbors both within + * the same chunk (vertical) and for neighboring chunks (horizontal). + */ + public void detectCubeNeighbors() { + for (LightingCube cube : this.sections.values()) { + // Neighbors above and below + cube.neighbors.set(0, 1, 0, this.sections.get(cube.cy + 1)); + cube.neighbors.set(0, -1, 0, this.sections.get(cube.cy - 1)); + // Neighbors in neighboring chunks + cube.neighbors.set(-1, 0, 0, this.neighbors.getCube(-1, 0, cube.cy)); + cube.neighbors.set( 1, 0, 0, this.neighbors.getCube( 1, 0, cube.cy)); + cube.neighbors.set( 0, 0, -1, this.neighbors.getCube( 0, -1, cube.cy)); + cube.neighbors.set( 0, 0, 1, this.neighbors.getCube( 0, 1, cube.cy)); + } + } + + public void fill(Chunk chunk, int[] region_y_coordinates) { + // Fill using chunk sections + hasSkyLight = WorldUtil.getDimensionType(chunk.getWorld()).hasSkyLight(); + + List lightingChunkSectionList; + { + // First create a list of ChunkSection objects storing the data + // We must do this sequentially, because asynchronous access is not permitted + List chunkSectionList = IntStream.of(region_y_coordinates) + .map(WorldUtil::regionToChunkIndex) + .flatMap(base_cy -> IntStream.range(base_cy, base_cy + WorldUtil.CHUNKS_PER_REGION_AXIS)) + .mapToObj(cy -> WorldUtil.getSection(chunk, cy)) + .filter(section -> section != null) + .collect(Collectors.toList()); + + // Then process all the gathered chunk sections into a LightingChunkSection in parallel + lightingChunkSectionList = chunkSectionList.stream() + .parallel() + .map(section -> new LightingCube(this, section, hasSkyLight)) + .collect(Collectors.toList()); + } + + // Add to mapping + this.sections = new IntHashMap(); + for (LightingCube lightingChunkSection : lightingChunkSectionList) { + this.sections.put(lightingChunkSection.cy, lightingChunkSection); + } + + // Compute min/max y using sections that are available + // Make use of the fact that they are pre-sorted by y-coordinate + this.minY = 0; + this.maxY = 0; + if (!lightingChunkSectionList.isEmpty()) { + this.minY = lightingChunkSectionList.get(0).cy << 4; + this.maxY = (lightingChunkSectionList.get(lightingChunkSectionList.size()-1).cy << 4) + 15; + } + + // Initialize and then load sky light heightmap information + if (this.hasSkyLight) { + HeightMap heightmap = ChunkUtil.getLightHeightMap(chunk, true); + for (int x = 0; x < 16; ++x) { + for (int z = 0; z < 16; ++z) { + this.heightmap[this.getHeightKey(x, z)] = Math.max(this.minY, heightmap.getHeight(x, z)); + } + } + } else { + Arrays.fill(this.heightmap, this.maxY); + } + + this.isFilled = true; + } + + private int getHeightKey(int x, int z) { + return x | (z << 4); + } + + /** + * Gets the height level (the top block that does not block light) + * + * @param x - coordinate + * @param z - coordinate + * @return height + */ + public int getHeight(int x, int z) { + return this.heightmap[getHeightKey(x, z)]; + } + + private final int getMaxLightLevel(LightingCube section, LightingCategory category, int lightLevel, int x, int y, int z) { + BlockFaceSet selfOpaqueFaces = section.getOpaqueFaces(x, y, z); + if (x >= 1 && z >= 1 && x <= 14 && z <= 14) { + // All within this chunk - simplified calculation + if (!selfOpaqueFaces.west()) { + lightLevel = section.getLightIfHigher(category, lightLevel, + BlockFaceSet.MASK_EAST, x - 1, y, z); + } + if (!selfOpaqueFaces.east()) { + lightLevel = section.getLightIfHigher(category, lightLevel, + BlockFaceSet.MASK_WEST, x + 1, y, z); + } + if (!selfOpaqueFaces.north()) { + lightLevel = section.getLightIfHigher(category, lightLevel, + BlockFaceSet.MASK_SOUTH, x, y, z - 1); + } + if (!selfOpaqueFaces.south()) { + lightLevel = section.getLightIfHigher(category, lightLevel, + BlockFaceSet.MASK_NORTH, x, y, z + 1); + } + + // If dy is also within this section, we can simplify it + if (y >= 1 && y <= 14) { + if (!selfOpaqueFaces.down()) { + lightLevel = section.getLightIfHigher(category, lightLevel, + BlockFaceSet.MASK_UP, x, y - 1, z); + } + if (!selfOpaqueFaces.up()) { + lightLevel = section.getLightIfHigher(category, lightLevel, + BlockFaceSet.MASK_DOWN, x, y + 1, z); + } + return lightLevel; + } + } else { + // Crossing chunk boundaries - requires neighbor checks + if (!selfOpaqueFaces.west()) { + lightLevel = section.getLightIfHigherNeighbor(category, lightLevel, + BlockFaceSet.MASK_EAST, x - 1, y, z); + } + if (!selfOpaqueFaces.east()) { + lightLevel = section.getLightIfHigherNeighbor(category, lightLevel, + BlockFaceSet.MASK_WEST, x + 1, y, z); + } + if (!selfOpaqueFaces.north()) { + lightLevel = section.getLightIfHigherNeighbor(category, lightLevel, + BlockFaceSet.MASK_SOUTH, x, y, z - 1); + } + if (!selfOpaqueFaces.south()) { + lightLevel = section.getLightIfHigherNeighbor(category, lightLevel, + BlockFaceSet.MASK_NORTH, x, y, z + 1); + } + } + + // Above and below, may need to check cube boundaries + // Below + if (!selfOpaqueFaces.down()) { + lightLevel = section.getLightIfHigherNeighbor(category, lightLevel, + BlockFaceSet.MASK_UP, x, y - 1, z); + } + + // Above + if (!selfOpaqueFaces.up()) { + lightLevel = section.getLightIfHigherNeighbor(category, lightLevel, + BlockFaceSet.MASK_DOWN, x, y + 1, z); + } + + return lightLevel; + } + + /** + * Gets whether this lighting chunk has faults that need to be fixed + * + * @return True if there are faults, False if not + */ + public boolean hasFaults() { + return isSkyLightDirty || isBlockLightDirty; + } + + public void forceSpreadBlocks() + { + spread(LightingCategory.BLOCK); + } + + /** + * Spreads the light from sources to 'zero' light level blocks + * + * @return Number of processing loops executed. 0 indicates no faults were found. + */ + public int spread() { + if (hasFaults()) { + int count = 0; + if (isSkyLightDirty) { + count += spread(LightingCategory.SKY); + } + if (isBlockLightDirty) { + count += spread(LightingCategory.BLOCK); + } + return count; + } else { + return 0; + } + } + + private int spread(LightingCategory category) { + if ((category == LightingCategory.SKY) && !hasSkyLight) { + this.isSkyLightDirty = false; + return 0; + } + + int x, y, z, light, factor, startY, newlight; + int loops = 0; + int lasterrx = 0, lasterry = 0, lasterrz = 0; + boolean haserror; + + boolean err_neigh_nx = false; + boolean err_neigh_px = false; + boolean err_neigh_nz = false; + boolean err_neigh_pz = false; + + LightingCube cube = null; + // Keep spreading the light in this chunk until it is done + boolean mode = false; + IntVector2 loop_start, loop_end; + int loop_increment; + while (true) { + haserror = false; + + // Alternate iterating positive and negative + // This allows proper optimized spreading in all directions + mode = !mode; + if (mode) { + loop_start = start; + loop_end = end.add(1, 1); + loop_increment = 1; + } else { + loop_start = end; + loop_end = start.subtract(1, 1); + loop_increment = -1; + } + + // Go through all blocks, using the heightmap for sky light to skip a few + for (x = loop_start.x; x != loop_end.x; x += loop_increment) { + for (z = loop_start.z; z != loop_end.z; z += loop_increment) { + startY = category.getStartY(this, x, z); + for (y = startY; y >= this.minY; y--) { + if ((cube = nextCube(cube, y)) == null) { + // Skip this section entirely by setting y to the bottom of the section + y &= ~0xf; + continue; + } + + // Take block opacity into account, skip if fully solid + factor = Math.max(1, cube.opacity.get(x, y & 0xf, z)); + if (factor == 15) { + continue; + } + + // Read the old light level and try to find a light level around it that exceeds + light = category.get(cube, x, y & 0xf, z); + newlight = light + factor; + if (newlight < 15) { + newlight = getMaxLightLevel(cube, category, newlight, x, y & 0xf, z); + } + newlight -= factor; + + // pick the highest value + if (newlight > light) { + category.set(cube, x, y & 0xf, z, newlight); + lasterrx = x; + lasterry = y; + lasterrz = z; + err_neigh_nx |= (x == 0); + err_neigh_nz |= (z == 0); + err_neigh_px |= (x == 15); + err_neigh_pz |= (z == 15); + haserror = true; + } + } + } + } + + if (!haserror) { + break; + } else if (++loops > 100) { + lasterrx += this.chunkX << 4; + lasterrz += this.chunkZ << 4; + StringBuilder msg = new StringBuilder(); + msg.append("Failed to fix all " + category.getName() + " lighting at ["); + msg.append(lasterrx).append('/').append(lasterry); + msg.append('/').append(lasterrz).append(']'); + Iris.warn(msg.toString()); + break; + } + } + + // Set self as no longer dirty, all light is good + category.setDirty(this, false); + + // When we change blocks at our chunk borders, neighbours have to do another spread cycle + if (err_neigh_nx) setNeighbourDirty(-1, 0, category); + if (err_neigh_px) setNeighbourDirty(1, 0, category); + if (err_neigh_nz) setNeighbourDirty(0, -1, category); + if (err_neigh_pz) setNeighbourDirty(0, 1, category); + + return loops; + } + + private void setNeighbourDirty(int dx, int dz, LightingCategory category) { + LightingChunk n = neighbors.get(dx, dz); + if (n != null) { + category.setDirty(n, true); + } + } + + /** + * Applies the lighting information to a chunk. The returned completable future is called + * on the main thread when saving finishes. + * + * @param chunk to save to + * @return completable future completed when the chunk is saved, + * with value True passed when saving occurred, False otherwise + */ + @SuppressWarnings("unchecked") + public CompletableFuture saveToChunk(Chunk chunk) { + // Create futures for saving to all the chunk sections in parallel + List sectionsToSave = this.sections.values(); + final CompletableFuture[] futures = new CompletableFuture[sectionsToSave.size()]; + { + int futureIndex = 0; + for (LightingCube sectionToSave : sectionsToSave) { + ChunkSection sectionToWriteTo = WorldUtil.getSection(chunk, sectionToSave.cy); + if (sectionToWriteTo == null) { + futures[futureIndex++] = CompletableFuture.completedFuture(Boolean.FALSE); + } else { + futures[futureIndex++] = sectionToSave.saveToChunk(sectionToWriteTo); + } + } + } + + // When all of them complete, combine them into a single future + // If any changes were made to the chunk, return True as completed value + return CompletableFuture.allOf(futures).thenApply((o) -> { + isApplied = true; + + try { + for (CompletableFuture future : futures) { + if (future.get().booleanValue()) { + ChunkHandle.fromBukkit(chunk).markDirty(); + return Boolean.TRUE; + } + } + } catch (Throwable t) { + t.printStackTrace(); + } + + // None of the futures completed true + return Boolean.FALSE; + }); + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingChunkNeighboring.java b/src/main/java/com/volmit/iris/v2/lighting/LightingChunkNeighboring.java new file mode 100644 index 000000000..774eb1b79 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingChunkNeighboring.java @@ -0,0 +1,73 @@ +package com.volmit.iris.v2.lighting; + +/** + * Keeps track of the 4 x/z neighbors of chunks + */ +public class LightingChunkNeighboring { + public final LightingChunk[] values = new LightingChunk[4]; + + /** + * Generates a key ranging 0 - 3 for fixed x/z combinations
+ * - Bit 1 is set to contain which of the two is not 1
+ * - Bit 2 is set to contain whether x/z is 1 or -1

+ *

+ * This system requires that the x/z pairs are one the following:
+ * (0, 1) | (0, -1) | (1, 0) | (-1, 0) + * + * @param x value + * @param z value + * @return key + */ + private static final int getIndexByChunk(int x, int z) { + return (x & 1) | ((x + z + 1) & 0x2); + } + + /** + * Gets whether all 4 chunk neighbors are accessible + * + * @return True if all neighbors are accessible + */ + public boolean hasAll() { + for (int i = 0; i < 4; i++) { + if (values[i] == null) { + return false; + } + } + return true; + } + + /** + * Gets the neighbor representing the given relative chunk + * + * @param deltaChunkX + * @param deltaChunkZ + * @return neighbor + */ + public LightingChunk get(int deltaChunkX, int deltaChunkZ) { + return values[getIndexByChunk(deltaChunkX, deltaChunkZ)]; + } + + /** + * Gets a relative neighboring chunk, and then a vertical cube in that chunk, if possible. + * + * @param deltaChunkX + * @param deltaChunkZ + * @param cy Cube absolute y-coordinate + * @return cube, null if the chunk or cube is not available + */ + public LightingCube getCube(int deltaChunkX, int deltaChunkZ, int cy) { + LightingChunk chunk = get(deltaChunkX, deltaChunkZ); + return (chunk == null) ? null : chunk.sections.get(cy); + } + + /** + * Sets the neighbor representing the given relative chunk + * + * @param deltaChunkX + * @param deltaChunkZ + * @param neighbor to set to + */ + public void set(int deltaChunkX, int deltaChunkZ, LightingChunk neighbor) { + values[getIndexByChunk(deltaChunkX, deltaChunkZ)] = neighbor; + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingCube.java b/src/main/java/com/volmit/iris/v2/lighting/LightingCube.java new file mode 100644 index 000000000..51f9a9b2e --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingCube.java @@ -0,0 +1,317 @@ +package com.volmit.iris.v2.lighting; + +import java.util.concurrent.CompletableFuture; + +import com.bergerkiller.bukkit.common.collections.BlockFaceSet; +import com.bergerkiller.bukkit.common.utils.WorldUtil; +import com.bergerkiller.bukkit.common.wrappers.BlockData; +import com.bergerkiller.bukkit.common.wrappers.ChunkSection; +import com.bergerkiller.generated.net.minecraft.server.NibbleArrayHandle; + +/** + * A single 16x16x16 cube of stored block information + */ +public class LightingCube { + public static final int OOC = ~0xf; // Outside Of Cube + public final LightingChunk owner; + public final LightingCubeNeighboring neighbors = new LightingCubeNeighboring(); + public final int cy; + public final NibbleArrayHandle skyLight; + public final NibbleArrayHandle blockLight; + public final NibbleArrayHandle emittedLight; + public final NibbleArrayHandle opacity; + private final BlockFaceSetSection opaqueFaces; + + public LightingCube(LightingChunk owner, ChunkSection chunkSection, boolean hasSkyLight) { + this.owner = owner; + this.cy = chunkSection.getY(); + + if (owner.neighbors.hasAll()) { + // Block light data (is re-initialized in the fill operation below, no need to read) + this.blockLight = NibbleArrayHandle.createNew(); + + // Sky light data (is re-initialized using heightmap operation later, no need to read) + if (hasSkyLight) { + this.skyLight = NibbleArrayHandle.createNew(); + } else { + this.skyLight = null; + } + } else { + // We need to load the original light data, because we have a border that we do not update + + // Block light data + byte[] blockLightData = WorldUtil.getSectionBlockLight(owner.world, + owner.chunkX, this.cy, owner.chunkZ); + if (blockLightData != null) { + this.blockLight = NibbleArrayHandle.createNew(blockLightData); + } else { + this.blockLight = NibbleArrayHandle.createNew(); + } + + // Sky light data + if (hasSkyLight) { + byte[] skyLightData = WorldUtil.getSectionSkyLight(owner.world, + owner.chunkX, this.cy, owner.chunkZ); + if (skyLightData != null) { + this.skyLight = NibbleArrayHandle.createNew(skyLightData); + } else { + this.skyLight = NibbleArrayHandle.createNew(); + } + } else { + this.skyLight = null; + } + } + + // World coordinates + int worldX = owner.chunkX << 4; + int worldY = chunkSection.getYPosition(); + int worldZ = owner.chunkZ << 4; + + // Fill opacity and initial block lighting values + this.opacity = NibbleArrayHandle.createNew(); + this.emittedLight = NibbleArrayHandle.createNew(); + this.opaqueFaces = new BlockFaceSetSection(); + int x, y, z, opacity, blockEmission; + BlockFaceSet opaqueFaces; + BlockData info; + for (z = owner.start.z; z <= owner.end.z; z++) { + for (x = owner.start.x; x <= owner.end.x; x++) { + for (y = 0; y < 16; y++) { + info = chunkSection.getBlockData(x, y, z); + blockEmission = info.getEmission(); + opacity = info.getOpacity(owner.world, worldX+x, worldY+y, worldZ+z); + if (opacity >= 0xf) { + opacity = 0xf; + opaqueFaces = BlockFaceSet.ALL; + } else { + if (opacity < 0) { + opacity = 0; + } + opaqueFaces = info.getOpaqueFaces(owner.world, worldX+x, worldY+y, worldZ+z); + } + + this.opacity.set(x, y, z, opacity); + this.emittedLight.set(x, y, z, blockEmission); + this.blockLight.set(x, y, z, blockEmission); + this.opaqueFaces.set(x, y, z, opaqueFaces); + } + } + } + } + + /** + * Gets the opaque faces of a block + * + * @param x - coordinate + * @param y - coordinate + * @param z - coordinate + * @return opaque face set + */ + public BlockFaceSet getOpaqueFaces(int x, int y, int z) { + return this.opaqueFaces.get(x, y, z); + } + + /** + * Read light level of a neighboring block. + * If possibly more, also check opaque faces, and then return the + * higher light value if all these tests pass. + * The x/y/z coordinates are allowed to check neighboring cubes. + * + * @param category + * @param old_light + * @param faceMask + * @param x The X-coordinate of the block (-1 to 16) + * @param y The Y-coordinate of the block (-1 to 16) + * @param z The Z-coordinate of the block (-1 to 16) + * @return higher light level if propagated, otherwise the old light value + */ + public int getLightIfHigherNeighbor(LightingCategory category, int old_light, int faceMask, int x, int y, int z) { + if ((x & OOC | y & OOC | z & OOC) == 0) { + return this.getLightIfHigher(category, old_light, faceMask, x, y, z); + } else { + LightingCube neigh = this.neighbors.get(x>>4, y>>4, z>>4); + if (neigh != null) { + return neigh.getLightIfHigher(category, old_light, faceMask, x & 0xf, y & 0xf, z & 0xf); + } else { + return old_light; + } + } + } + + /** + * Read light level of a neighboring block. + * If possibly more, also check opaque faces, and then return the + * higher light value if all these tests pass. + * Requires the x/y/z coordinates to lay within this cube. + * + * @param category Category of light to check + * @param old_light Previous light value + * @param faceMask The BlockFaceSet mask indicating the light-traveling direction + * @param x The X-coordinate of the block (0 to 15) + * @param y The Y-coordinate of the block (0 to 15) + * @param z The Z-coordinate of the block (0 to 15) + * @return higher light level if propagated, otherwise the old light value + */ + public int getLightIfHigher(LightingCategory category, int old_light, int faceMask, int x, int y, int z) { + int new_light_level = category.get(this, x, y, z); + return (new_light_level > old_light && !this.getOpaqueFaces(x, y, z).get(faceMask)) + ? new_light_level : old_light; + } + + /** + * Called during initialization of block light to spread the light emitted by a block + * to all neighboring blocks. + * + * @param x The X-coordinate of the block (0 to 15) + * @param y The Y-coordinate of the block (0 to 15) + * @param z The Z-coordinate of the block (0 to 15) + */ + public void spreadBlockLight(int x, int y, int z) { + int emitted = this.emittedLight.get(x, y, z); + if (emitted <= 1) { + return; // Skip if neighbouring blocks won't receive light from it + } + if (x >= 1 && z >= 1 && x <= 14 && z <= 14) { + trySpreadBlockLightWithin(emitted, BlockFaceSet.MASK_EAST, x-1, y, z); + trySpreadBlockLightWithin(emitted, BlockFaceSet.MASK_WEST, x+1, y, z); + trySpreadBlockLightWithin(emitted, BlockFaceSet.MASK_SOUTH, x, y, z-1); + trySpreadBlockLightWithin(emitted, BlockFaceSet.MASK_NORTH, x, y, z+1); + } else { + trySpreadBlockLight(emitted, BlockFaceSet.MASK_EAST, x-1, y, z); + trySpreadBlockLight(emitted, BlockFaceSet.MASK_WEST, x+1, y, z); + trySpreadBlockLight(emitted, BlockFaceSet.MASK_SOUTH, x, y, z-1); + trySpreadBlockLight(emitted, BlockFaceSet.MASK_NORTH, x, y, z+1); + } + if (y >= 1 && y <= 14) { + trySpreadBlockLightWithin(emitted, BlockFaceSet.MASK_UP, x, y-1, z); + trySpreadBlockLightWithin(emitted, BlockFaceSet.MASK_DOWN, x, y+1, z); + } else { + trySpreadBlockLight(emitted, BlockFaceSet.MASK_UP, x, y-1, z); + trySpreadBlockLight(emitted, BlockFaceSet.MASK_DOWN, x, y+1, z); + } + } + + /** + * Tries to spread block light from an emitting block to one of the 6 sites. + * The block being spread to is allowed to be outside of the bounds of this cube, + * in which case neighboring cubes are spread to instead. + * + * @param emitted The light that is emitted by the block + * @param faceMask The BlockFaceSet mask indicating the light-traveling direction + * @param x The X-coordinate of the block to spread to (-1 to 16) + * @param y The Y-coordinate of the block to spread to (-1 to 16) + * @param z The Z-coordinate of the block to spread to (-1 to 16) + */ + public void trySpreadBlockLight(int emitted, int faceMask, int x, int y, int z) { + if ((x & OOC | y & OOC | z & OOC) == 0) { + this.trySpreadBlockLightWithin(emitted, faceMask, x, y, z); + } else { + LightingCube neigh = this.neighbors.get(x>>4, y>>4, z>>4); + if (neigh != null) { + neigh.trySpreadBlockLightWithin(emitted, faceMask, x & 0xf, y & 0xf, z & 0xf); + } + } + } + + /** + * Tries to spread block light from an emitting block to one of the 6 sides. + * Assumes that the block being spread to is within this cube. + * + * @param emitted The light that is emitted by the block + * @param faceMask The BlockFaceSet mask indicating the light-traveling direction + * @param x The X-coordinate of the block to spread to (0 to 15) + * @param y The Y-coordinate of the block to spread to (0 to 15) + * @param z The Z-coordinate of the block to spread to (0 to 15) + */ + public void trySpreadBlockLightWithin(int emitted, int faceMask, int x, int y, int z) { + if (!this.getOpaqueFaces(x, y, z).get(faceMask)) { + int new_level = emitted - Math.max(1, this.opacity.get(x, y, z)); + if (new_level > this.blockLight.get(x, y, z)) { + this.blockLight.set(x, y, z, new_level); + } + } + } + + /** + * Applies the lighting information to a chunk section + * + * @param chunkSection to save to + * @return future completed when saving is finished. Future resolves to False if no changes occurred, True otherwise. + */ + public CompletableFuture saveToChunk(ChunkSection chunkSection) { + CompletableFuture blockLightFuture = null; + CompletableFuture skyLightFuture = null; + + try { + if (this.blockLight != null) { + byte[] newBlockLight = this.blockLight.getData(); + byte[] oldBlockLight = WorldUtil.getSectionBlockLight(owner.world, + owner.chunkX, this.cy, owner.chunkZ); + boolean blockLightChanged = false; + if (oldBlockLight == null || newBlockLight.length != oldBlockLight.length) { + blockLightChanged = true; + } else { + for (int i = 0; i < oldBlockLight.length; i++) { + if (oldBlockLight[i] != newBlockLight[i]) { + blockLightChanged = true; + break; + } + } + } + + //TODO: Maybe do blockLightChanged check inside BKCommonLib? + if (blockLightChanged) { + blockLightFuture = WorldUtil.setSectionBlockLightAsync(owner.world, + owner.chunkX, this.cy, owner.chunkZ, + newBlockLight); + } + } + if (this.skyLight != null) { + byte[] newSkyLight = this.skyLight.getData(); + byte[] oldSkyLight = WorldUtil.getSectionSkyLight(owner.world, + owner.chunkX, this.cy, owner.chunkZ); + boolean skyLightChanged = false; + if (oldSkyLight == null || newSkyLight.length != oldSkyLight.length) { + skyLightChanged = true; + } else { + for (int i = 0; i < oldSkyLight.length; i++) { + if (oldSkyLight[i] != newSkyLight[i]) { + skyLightChanged = true; + break; + } + } + } + + //TODO: Maybe do skyLightChanged check inside BKCommonLib? + if (skyLightChanged) { + skyLightFuture = WorldUtil.setSectionSkyLightAsync(owner.world, + owner.chunkX, this.cy, owner.chunkZ, + newSkyLight); + } + } + } catch (Throwable t) { + CompletableFuture exceptionally = new CompletableFuture(); + exceptionally.completeExceptionally(t); + return exceptionally; + } + + // No updates performed + if (blockLightFuture == null && skyLightFuture == null) { + return CompletableFuture.completedFuture(Boolean.FALSE); + } + + // Join both completable futures as one, if needed + CompletableFuture combined; + if (blockLightFuture == null) { + combined = skyLightFuture; + } else if (skyLightFuture == null) { + combined = blockLightFuture; + } else { + combined = CompletableFuture.allOf(blockLightFuture, skyLightFuture); + } + + // When combined resolves, return one that returns True + return combined.thenApply((c) -> Boolean.TRUE); + } + +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingCubeNeighboring.java b/src/main/java/com/volmit/iris/v2/lighting/LightingCubeNeighboring.java new file mode 100644 index 000000000..bcae0cfa4 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingCubeNeighboring.java @@ -0,0 +1,64 @@ +package com.volmit.iris.v2.lighting; + +/** + * Keeps track of the 6 x/y/z neighbors of cubes + */ +public class LightingCubeNeighboring { + public final LightingCube[] values = new LightingCube[6]; + + /** + * Generates a key ranging 0 - 5 for fixed x/y/z combinations
+ * - Bit 1 is set to contain whether x/y/z is 1 or -1 + * - Bit 2 is set to 1 when the axis is x
+ * - Bit 3 is set to 1 when the axis is z

+ *

+ * This system requires that the x/y/z pairs are one the following:
+ * (0, 0, 1) | (0, 0, -1) | (0, 1, 0) | (0, -1, 0) | (1, 0, 0) | (-1, 0, 0) + * + * @param x value + * @param y value + * @param z value + * @return key + */ + private static final int getIndexByCube(int x, int y, int z) { + return (((x + y + z + 1) & 0x2) >> 1) | ((x & 0x1) << 1) | ((z & 0x1) << 2); + } + + /** + * Gets whether all 6 cube neighbors are accessible + * + * @return True if all neighbors are accessible + */ + public boolean hasAll() { + for (int i = 0; i < 6; i++) { + if (values[i] == null) { + return false; + } + } + return true; + } + + /** + * Gets the neighbor representing the given relative cube + * + * @param deltaCubeX + * @param deltaCubeY + * @param deltaCubeZ + * @return neighbor, null if no neighbor is available here + */ + public LightingCube get(int deltaCubeX, int deltaCubeY, int deltaCubeZ) { + return values[getIndexByCube(deltaCubeX, deltaCubeY, deltaCubeZ)]; + } + + /** + * Sets the neighbor representing the given relative cube + * + * @param deltaCubeX + * @param deltaCubeY + * @param deltaCubeZ + * @param neighbor to set to, is allowed to be null to set to 'none' + */ + public void set(int deltaCubeX, int deltaCubeY, int deltaCubeZ, LightingCube neighbor) { + values[getIndexByCube(deltaCubeX, deltaCubeY, deltaCubeZ)] = neighbor; + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingForcedChunkCache.java b/src/main/java/com/volmit/iris/v2/lighting/LightingForcedChunkCache.java new file mode 100644 index 000000000..13cb3da67 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingForcedChunkCache.java @@ -0,0 +1,77 @@ +package com.volmit.iris.v2.lighting; + +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.World; + +import com.bergerkiller.bukkit.common.chunk.ForcedChunk; +import com.bergerkiller.bukkit.common.utils.WorldUtil; + +/** + * Shortly remembers the forced chunks it has kept loaded from a previous operation. + * Reduces chunk unloading-loading grind. + */ +public class LightingForcedChunkCache { + private static final Map _cache = new HashMap(); + + public static ForcedChunk get(World world, int x, int z) { + ForcedChunk cached; + synchronized (_cache) { + cached = _cache.get(new Key(world, x, z)); + } + if (cached != null) { + return cached.clone(); + } else { + return WorldUtil.forceChunkLoaded(world, x, z); + } + } + + public static void store(ForcedChunk chunk) { + ForcedChunk prev; + synchronized (_cache) { + prev = _cache.put(new Key(chunk.getWorld(), chunk.getX(), chunk.getZ()), chunk.clone()); + } + if (prev != null) { + prev.close(); + } + } + + public static void reset() { + synchronized (_cache) { + for (ForcedChunk chunk : _cache.values()) { + chunk.close(); + } + _cache.clear(); + } + } + + private static final class Key { + public final World world; + public final int x; + public final int z; + + public Key(World world, int x, int z) { + this.world = world; + this.x = x; + this.z = z; + } + + @Override + public int hashCode() { + return this.x * 31 + this.z; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Key) { + Key other = (Key) o; + return other.x == this.x && + other.z == this.z && + other.world == this.world; + } else { + return false; + } + } + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingService.java b/src/main/java/com/volmit/iris/v2/lighting/LightingService.java new file mode 100644 index 000000000..3b394b396 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingService.java @@ -0,0 +1,650 @@ +package com.volmit.iris.v2.lighting; + +import com.bergerkiller.bukkit.common.AsyncTask; +import com.bergerkiller.bukkit.common.bases.IntVector2; +import com.bergerkiller.bukkit.common.bases.IntVector3; +import com.bergerkiller.bukkit.common.config.CompressedDataReader; +import com.bergerkiller.bukkit.common.config.CompressedDataWriter; +import com.bergerkiller.bukkit.common.permissions.NoPermissionException; +import com.bergerkiller.bukkit.common.utils.MathUtil; +import com.bergerkiller.bukkit.common.utils.ParseUtil; +import com.bergerkiller.bukkit.common.utils.StringUtil; +import com.bergerkiller.bukkit.common.utils.WorldUtil; +import com.bergerkiller.bukkit.common.wrappers.LongHashSet; +import com.bergerkiller.bukkit.common.wrappers.LongHashSet.LongIterator; +import com.volmit.iris.Iris; +import org.bukkit.*; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.logging.Level; + +public class LightingService extends AsyncTask { + private static final Set recipientsForDone = new HashSet(); + private static final LinkedList tasks = new LinkedList(); + private static final int PENDING_WRITE_INTERVAL = 10; + private static AsyncTask fixThread = null; + private static int taskChunkCount = 0; + private static int taskCounter = 0; + private static boolean pendingFileInUse = false; + private static LightingTask currentTask; + private static boolean paused = false; + private static boolean lowOnMemory = false; + + /** + * Gets whether this service is currently processing something + * + * @return True if processing, False if not + */ + public static boolean isProcessing() { + return fixThread != null; + } + + /** + * Starts or stops the processing service. + * Stopping the service does not instantly abort, the current task is continued. + * + * @param process to abort + */ + public static void setProcessing(boolean process) { + if (process == isProcessing()) { + return; + } + if (process) { + fixThread = new LightingService().start(true); + } else { + // Fix thread is running, abort + AsyncTask.stop(fixThread); + fixThread = null; + } + } + + /** + * Gets whether execution is paused, and pending tasks are not being processed + * + * @return True if paused + */ + public static boolean isPaused() { + return paused; + } + + /** + * Sets whether execution is paused. + * + * @param pause state to set to + */ + public static void setPaused(boolean pause) { + if (paused != pause) { + paused = pause; + } + } + + /** + * Gets the status of the currently processed task + * + * @return current task status + */ + public static String getCurrentStatus() { + final LightingTask current = currentTask; + if (lowOnMemory) { + return ChatColor.RED + "Too low on available memory (paused)"; + } else if (current == null) { + return "Finished."; + } else { + return current.getStatus(); + } + } + + /** + * Gets the time the currently processing task was started. If no task is being processed, + * an empty result is returned. If processing didn't start yet, the value will be 0. + * + * @return time when the current task was started + */ + public static java.util.OptionalLong getCurrentStartTime() { + final LightingTask current = currentTask; + return (current == null) ? java.util.OptionalLong.empty() : OptionalLong.of(current.getTimeStarted()); + } + + public static void addRecipient(CommandSender sender) { + synchronized (recipientsForDone) { + recipientsForDone.add(new RecipientWhenDone(sender)); + } + } + + public static void scheduleWorld(final World world) { + ScheduleArguments args = new ScheduleArguments(); + args.setWorld(world); + args.setEntireWorld(); + schedule(args); + } + + /** + * Schedules a square chunk area for lighting fixing + * + * @param world the chunks are in + * @param middleX + * @param middleZ + * @param radius + */ + public static void scheduleArea(World world, int middleX, int middleZ, int radius) { + ScheduleArguments args = new ScheduleArguments(); + args.setWorld(world); + args.setChunksAround(middleX, middleZ, radius); + schedule(args); + } + + @Deprecated + public static void schedule(World world, Collection chunks) { + ScheduleArguments args = new ScheduleArguments(); + args.setWorld(world); + args.setChunks(chunks); + schedule(args); + } + + public static void schedule(World world, LongHashSet chunks) { + ScheduleArguments args = new ScheduleArguments(); + args.setWorld(world); + args.setChunks(chunks); + schedule(args); + } + + public static void schedule(ScheduleArguments args) { + // World not allowed to be null + if (args.getWorld() == null) { + throw new IllegalArgumentException("Schedule arguments 'world' is null"); + } + + // If no chunks specified, entire world + if (args.isEntireWorld()) { + LightingTaskWorld task = new LightingTaskWorld(args.getWorld()); + task.applyOptions(args); + schedule(task); + return; + } + + // If less than 34x34 chunks are requested, schedule as one task + // In that case, be sure to only schedule chunks that actually exist + // This prevents generating new chunks as part of this command + LongHashSet chunks = args.getChunks(); + if (chunks.size() <= (34*34)) { + + LongHashSet chunks_filtered = new LongHashSet(chunks.size()); + Set region_coords_filtered = new HashSet(); + LongIterator iter = chunks.longIterator(); + + if (args.getLoadedChunksOnly()) { + // Remove coordinates of chunks that aren't loaded + while (iter.hasNext()) { + long chunk = iter.next(); + int cx = MathUtil.longHashMsw(chunk); + int cz = MathUtil.longHashLsw(chunk); + if (WorldUtil.isLoaded(args.getWorld(), cx, cz)) { + chunks_filtered.add(chunk); + region_coords_filtered.add(new IntVector2( + WorldUtil.chunkToRegionIndex(cx), + WorldUtil.chunkToRegionIndex(cz))); + } + } + } else if (true) { + // Remove coordinates of chunks that don't actually exist (avoid generating new chunks) + // isChunkAvailable isn't very fast, but fast enough below this threshold of chunks + // To check for border chunks, we check that all 9 chunks are are available + Map tmp = new HashMap<>(); + while (iter.hasNext()) { + long chunk = iter.next(); + int cx = MathUtil.longHashMsw(chunk); + int cz = MathUtil.longHashLsw(chunk); + + boolean fully_loaded = true; + for (int dx = -2; dx <= 2 && fully_loaded; dx++) { + for (int dz = -2; dz <= 2 && fully_loaded; dz++) { + IntVector2 pos = new IntVector2(cx + dx, cz + dz); + fully_loaded &= tmp.computeIfAbsent(pos, p -> WorldUtil.isChunkAvailable(args.getWorld(), p.x, p.z)).booleanValue(); + } + } + + if (fully_loaded) { + chunks_filtered.add(chunk); + region_coords_filtered.add(new IntVector2( + WorldUtil.chunkToRegionIndex(cx), + WorldUtil.chunkToRegionIndex(cz))); + } + } + } else { + // Remove coordinates of chunks that don't actually exist (avoid generating new chunks) + // isChunkAvailable isn't very fast, but fast enough below this threshold of chunks + while (iter.hasNext()) { + long chunk = iter.next(); + int cx = MathUtil.longHashMsw(chunk); + int cz = MathUtil.longHashLsw(chunk); + if (WorldUtil.isChunkAvailable(args.getWorld(), cx, cz)) { + chunks_filtered.add(chunk); + region_coords_filtered.add(new IntVector2( + WorldUtil.chunkToRegionIndex(cx), + WorldUtil.chunkToRegionIndex(cz))); + } + } + } + + // For all filtered chunk coordinates, compute regions + int[] regionYCoordinates; + { + Set regions = WorldUtil.getWorldRegions3ForXZ(args.getWorld(), region_coords_filtered); + + // Simplify to just the unique Y-coordinates + regionYCoordinates = regions.stream().mapToInt(r -> r.y).sorted().distinct().toArray(); + } + + // Schedule it + if (!chunks_filtered.isEmpty()) { + LightingTaskBatch task = new LightingTaskBatch(args.getWorld(), regionYCoordinates, chunks_filtered); + task.applyOptions(args); + schedule(task); + } + return; + } + + // Too many chunks requested. Separate the operations per region file with small overlap. + FlatRegionInfoMap regions; + if (args.getLoadedChunksOnly()) { + regions = FlatRegionInfoMap.createLoaded(args.getWorld()); + } else { + regions = FlatRegionInfoMap.create(args.getWorld()); + } + + LongIterator iter = chunks.longIterator(); + LongHashSet scheduledRegions = new LongHashSet(); + while (iter.hasNext()) { + long first_chunk = iter.next(); + int first_chunk_x = MathUtil.longHashMsw(first_chunk); + int first_chunk_z = MathUtil.longHashLsw(first_chunk); + FlatRegionInfo region = regions.getRegionAtChunk(first_chunk_x, first_chunk_z); + if (region == null || scheduledRegions.contains(region.rx, region.rz)) { + continue; // Does not exist or already scheduled + } + if (!region.containsChunk(first_chunk_x, first_chunk_z)) { + continue; // Chunk does not exist in world (not generated yet) or isn't loaded (loaded chunks only option) + } + + // Collect all the region Y coordinates used for this region and the neighbouring regions + // This makes sure we find all chunk slices we might need on an infinite height world + int[] region_y_coordinates = regions.getRegionYCoordinatesSelfAndNeighbours(region); + + // Collect all chunks to process for this region. + // This is an union of the 34x34 area of chunks and the region file data set + LongHashSet buffer = new LongHashSet(); + int rdx, rdz; + for (rdx = -1; rdx < 33; rdx++) { + for (rdz = -1; rdz < 33; rdz++) { + int cx = region.cx + rdx; + int cz = region.cz + rdz; + long chunk_key = MathUtil.longHashToLong(cx, cz); + if (!chunks.contains(chunk_key)) { + continue; + } + + if (true) { + // Check the chunk and the surrounding chunks are all present + if (!regions.containsChunkAndNeighbours(cx, cz)) { + continue; + } + } else { + // Only check chunk + if (!regions.containsChunk(cx, cz)) { + continue; + } + } + buffer.add(chunk_key); + } + } + + // Schedule the region + if (!buffer.isEmpty()) { + scheduledRegions.add(region.rx, region.rz); + LightingTaskBatch task = new LightingTaskBatch(args.getWorld(), region_y_coordinates, buffer); + task.applyOptions(args); + schedule(task); + } + } + } + + public static void schedule(LightingTask task) { + synchronized (tasks) { + tasks.offer(task); + taskChunkCount += task.getChunkCount(); + } + setProcessing(true); + } + + /** + * Loads the pending chunk batch operations from a save file. + * If it is there, it will start processing these again. + */ + public static void loadPendingBatches() { + pendingFileInUse = false; + } + + /** + * Saves all pending chunk batch operations to a save file. + * If the server, for whatever reason, crashes, it can restore using this file. + */ + public static void savePendingBatches() { + if (pendingFileInUse) { + return; + } + } + + /** + * Clears all pending tasks, does continue with the current tasks + */ + public static void clearTasks() { + synchronized (tasks) { + tasks.clear(); + } + final LightingTask current = currentTask; + if (current != null) { + current.abort(); + } + synchronized (tasks) { + tasks.clear(); + } + currentTask = null; + taskChunkCount = 0; + LightingForcedChunkCache.reset(); + } + + /** + * Orders this service to abort all tasks, finishing the current task in an orderly fashion. + * This method can only be called from the main Thread. + */ + public static void abort() { + // Finish the current lighting task if available + final LightingTask current = currentTask; + final AsyncTask service = fixThread; + if (service != null && current != null) { + setProcessing(false); + current.abort(); + } + // Clear lighting tasks + synchronized (tasks) { + if (current != null) { + tasks.addFirst(current); + } + if (!tasks.isEmpty()) { + } + savePendingBatches(); + clearTasks(); + } + } + + /** + * Gets the amount of chunks that are still faulty + * + * @return faulty chunk count + */ + public static int getChunkFaults() { + final LightingTask current = currentTask; + return taskChunkCount + (current == null ? 0 : current.getChunkCount()); + } + + @Override + public void run() { + // While paused, do nothing + while (paused) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + synchronized (tasks) { + if (tasks.isEmpty()) { + break; // Stop processing. + } + } + if (fixThread.isStopRequested()) { + return; + } + } + + synchronized (tasks) { + currentTask = tasks.poll(); + } + if (currentTask == null) { + // No more tasks, end this thread + // Messages + final String message = ChatColor.GREEN + "All lighting operations are completed."; + synchronized (recipientsForDone) { + for (RecipientWhenDone recipient : recipientsForDone) { + CommandSender recip = recipient.player_name == null ? + Bukkit.getConsoleSender() : Bukkit.getPlayer(recipient.player_name); + if (recip != null) { + String timeStr = LightingUtil.formatDuration(System.currentTimeMillis() - recipient.timeStarted); + recip.sendMessage(message + ChatColor.WHITE + " (Took " + timeStr + ")"); + } + } + recipientsForDone.clear(); + } + // Stop task and abort + taskCounter = 0; + setProcessing(false); + LightingForcedChunkCache.reset(); + savePendingBatches(); + return; + } else { + // Write to file? + if (taskCounter++ >= PENDING_WRITE_INTERVAL) { + taskCounter = 0; + // Start saving on another thread (IO access is slow...) + new AsyncTask() { + public void run() { + savePendingBatches(); + } + }.start(); + + // Save the world of the current task being processed + + } + // Subtract task from the task count + taskChunkCount -= currentTask.getChunkCount(); + // Process the task + try { + currentTask.process(); + } catch (Throwable t) { + t.printStackTrace(); + Iris.error("Failed to process task: " + currentTask.getStatus()); + } + } + } + + private static long calcAvailableMemory(Runtime runtime) { + long max = runtime.maxMemory(); + if (max == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } else { + long used = (runtime.totalMemory() - runtime.freeMemory()); + return (max - used); + } + } + + public static class ScheduleArguments { + private World world; + private String worldName; + private LongHashSet chunks; + private boolean debugMakeCorrupted = false; + private boolean loadedChunksOnly = false; + private int radius = Bukkit.getServer().getViewDistance(); + + public boolean getDebugMakeCorrupted() { + return this.debugMakeCorrupted; + } + + public boolean getLoadedChunksOnly() { + return this.loadedChunksOnly; + } + + public int getRadius() { + return this.radius; + } + + public boolean isEntireWorld() { + return this.chunks == null; + } + + public World getWorld() { + return this.world; + } + + public String getWorldName() { + return this.worldName; + } + + public LongHashSet getChunks() { + return this.chunks; + } + + /** + * Sets the world itself. Automatically updates the world name. + * + * @param world + * @return these arguments + */ + public ScheduleArguments setWorld(World world) { + this.world = world; + this.worldName = world.getName(); + return this; + } + + /** + * Sets the world name to perform operations on. + * If the world by this name does not exist, the world is null. + * + * @param worldName + * @return these arguments + */ + public ScheduleArguments setWorldName(String worldName) { + this.world = Bukkit.getWorld(worldName); + this.worldName = worldName; + return this; + } + + public ScheduleArguments setEntireWorld() { + this.chunks = null; + return this; + } + + public ScheduleArguments setDebugMakeCorrupted(boolean debug) { + this.debugMakeCorrupted = debug; + return this; + } + + public ScheduleArguments setLoadedChunksOnly(boolean loadedChunksOnly) { + this.loadedChunksOnly = loadedChunksOnly; + return this; + } + + public ScheduleArguments setRadius(int radius) { + this.radius = radius; + return this; + } + + public ScheduleArguments setChunksAround(Location location, int radius) { + this.setWorld(location.getWorld()); + return this.setChunksAround(location.getBlockX()>>4, location.getBlockZ()>>4, radius); + } + + public ScheduleArguments setChunksAround(int middleX, int middleZ, int radius) { + this.setRadius(radius); + + LongHashSet chunks_hashset = new LongHashSet((2*radius)*(2*radius)); + for (int a = -radius; a <= radius; a++) { + for (int b = -radius; b <= radius; b++) { + int cx = middleX + a; + int cz = middleZ + b; + chunks_hashset.add(cx, cz); + } + } + return this.setChunks(chunks_hashset); + } + + /** + * Sets the chunks to a cuboid area of chunks. + * Make sure the minimum chunk coordinates are less or equal to + * the maximum chunk coordinates. + * + * @param minChunkX Minimum chunk x-coordinate (inclusive) + * @param minChunkZ Minimum chunk z-coordinate (inclusive) + * @param maxChunkX Maximum chunk x-coordinate (inclusive) + * @param maxChunkZ Maximum chunk z-coordinate (inclusive) + * @return this + */ + public ScheduleArguments setChunkFromTo(int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ) { + int num_dx = (maxChunkX - minChunkX) + 1; + int num_dz = (maxChunkZ - minChunkZ) + 1; + if (num_dx <= 0 || num_dz <= 0) { + return this.setChunks(new LongHashSet()); // nothing + } + + LongHashSet chunks_hashset = new LongHashSet(num_dx * num_dz); + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + chunks_hashset.add(chunkX, chunkZ); + } + } + return this.setChunks(chunks_hashset); + } + + public ScheduleArguments setChunks(Collection chunks) { + LongHashSet chunks_hashset = new LongHashSet(chunks.size()); + for (IntVector2 coord : chunks) { + chunks_hashset.add(coord.x, coord.z); + } + return this.setChunks(chunks_hashset); + } + + public ScheduleArguments setChunks(LongHashSet chunks) { + this.chunks = chunks; + return this; + } + + private boolean checkRadiusPermission(CommandSender sender, int radius) throws NoPermissionException { + return false; + } + + /** + * Parses the arguments specified in a command + * + * @param sender + * @return false if the input is incorrect and operations may not proceed + * @throws NoPermissionException + */ + public boolean handleCommandInput(CommandSender sender, String[] args) throws NoPermissionException { + return true; + } + + /** + * Creates a new ScheduleArguments instance ready to be configured + * + * @return args + */ + public static ScheduleArguments create() + { + return new ScheduleArguments(); + } + } + + private static class RecipientWhenDone { + public final String player_name; + public final long timeStarted; + + public RecipientWhenDone(CommandSender sender) { + this.player_name = (sender instanceof Player) ? sender.getName() : null; + this.timeStarted = System.currentTimeMillis(); + } + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingTask.java b/src/main/java/com/volmit/iris/v2/lighting/LightingTask.java new file mode 100644 index 000000000..9da9964c2 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingTask.java @@ -0,0 +1,61 @@ +package com.volmit.iris.v2.lighting; + +import org.bukkit.World; + +/** + * A single task the Lighting Service can handle + */ +public interface LightingTask { + /** + * Gets the world this task is working on + * + * @return task world + */ + World getWorld(); + + /** + * Gets the amount of chunks this task is going to fix. + * This can be a wild estimate. While processing this amount should be + * updated as well. + * + * @return estimated total chunk count + */ + int getChunkCount(); + + /** + * Gets a descriptive status of the current task being processed + * + * @return status + */ + String getStatus(); + + /** + * Gets the timestamp (milliseconds since epoch) when this task was first started. + * If 0 is returned, then the task wasn't started yet. + * + * @return time this task was started + */ + long getTimeStarted(); + + /** + * Processes this task (called from another thread!) + */ + void process(); + + /** + * Orders this task to abort + */ + void abort(); + + /** + * Whether this task can be saved to PendingLight.dat + * + * @return True if it can be saved + */ + boolean canSave(); + + /** + * Loads additional options + */ + void applyOptions(LightingService.ScheduleArguments args); +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingTaskBatch.java b/src/main/java/com/volmit/iris/v2/lighting/LightingTaskBatch.java new file mode 100644 index 000000000..cfd38265b --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingTaskBatch.java @@ -0,0 +1,566 @@ +package com.volmit.iris.v2.lighting; + +import com.bergerkiller.bukkit.common.bases.IntVector2; +import com.bergerkiller.bukkit.common.utils.CommonUtil; +import com.bergerkiller.bukkit.common.utils.LogicUtil; +import com.bergerkiller.bukkit.common.utils.MathUtil; +import com.bergerkiller.bukkit.common.utils.WorldUtil; +import com.bergerkiller.bukkit.common.wrappers.LongHashSet; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.stream.Stream; + +import com.volmit.iris.Iris; +import com.volmit.iris.IrisSettings; +import org.bukkit.Chunk; +import org.bukkit.World; + +/** + * Contains all the chunk coordinates that have to be fixed, + * and handles the full process of this fixing. + * It is literally a batch of chunks being processed. + */ +public class LightingTaskBatch implements LightingTask { + private static boolean DEBUG_LOG = false; // logs performance stats + public final World world; + private final Object chunks_lock = new Object(); + private final int[] region_y_coords; + private volatile LightingChunk[] chunks = null; + private volatile long[] chunks_coords; + private boolean done = false; + private boolean aborted = false; + private volatile long timeStarted = 0; + private int numBeingLoaded = 0; + private volatile Stage stage = Stage.LOADING; + private LightingService.ScheduleArguments options = new LightingService.ScheduleArguments(); + + public LightingTaskBatch(World world, int[] regionYCoordinates, long[] chunkCoordinates) { + this.world = world; + this.region_y_coords = regionYCoordinates; + this.chunks_coords = chunkCoordinates; + } + + public LightingTaskBatch(World world, int[] regionYCoordinates, LongHashSet chunkCoordinates) { + this.world = world; + this.region_y_coords = regionYCoordinates; + + // Turn contents of the long hash set into an easily sortable IntVector2[] array + IntVector2[] coordinates = new IntVector2[chunkCoordinates.size()]; + { + LongHashSet.LongIterator iter = chunkCoordinates.longIterator(); + for (int i = 0; iter.hasNext(); i++) { + long coord = iter.next(); + coordinates[i] = new IntVector2(MathUtil.longHashMsw(coord), MathUtil.longHashLsw(coord)); + } + } + + // Sort the array along the axis. This makes chunk loading more efficient. + Arrays.sort(coordinates, (a, b) -> { + int comp = Integer.compare(a.x, b.x); + if (comp == 0) { + comp = Integer.compare(a.z, b.z); + } + return comp; + }); + + // Turn back into a long[] array for memory efficiency + this.chunks_coords = Stream.of(coordinates).mapToLong(c -> MathUtil.longHashToLong(c.x, c.z)).toArray(); + } + + @Override + public World getWorld() { + return world; + } + + /** + * Gets the X and Z-coordinates of all the chunk columns to process. + * The coordinates are combined into a single Long, which can be decoded + * using {@link MathUtil#longHashMsw(long)} for X and {@link MathUtil#longHashLsw(long) for Z. + * + * @return chunk coordinates + */ + public long[] getChunks() { + synchronized (this.chunks_lock) { + LightingChunk[] chunks = this.chunks; + if (chunks != null) { + long[] coords = new long[chunks.length]; + for (int i = 0; i < chunks.length; i++) { + coords[i] = MathUtil.longHashToLong(chunks[i].chunkX, chunks[i].chunkZ); + } + return coords; + } else if (this.chunks_coords != null) { + return this.chunks_coords; + } else { + return new long[0]; + } + } + } + + /** + * Gets the Y-coordinates of all the regions to look for chunk data. A region stores 32 chunk + * slices vertically, and goes up/down 512 blocks every coordinate increase/decrease. + * + * @return region Y-coordinates + */ + public int[] getRegionYCoordinates() { + return this.region_y_coords; + } + + @Override + public int getChunkCount() { + synchronized (this.chunks_lock) { + if (this.chunks == null) { + return this.done ? 0 : this.chunks_coords.length; + } else { + int faults = 0; + for (LightingChunk chunk : this.chunks) { + if (chunk.hasFaults()) { + faults++; + } + } + return faults; + } + } + } + + @Override + public long getTimeStarted() { + return this.timeStarted; + } + + private static final class BatchChunkInfo { + public final int cx; + public final int cz; + public final int count; + + public BatchChunkInfo(int cx, int cz, int count) { + this.cx = cx; + this.cz = cz; + this.count = count; + } + } + + public BatchChunkInfo getAverageChunk() { + int count = 0; + long cx = 0; + long cz = 0; + synchronized (this.chunks_lock) { + if (this.chunks != null) { + count = this.chunks.length; + for (LightingChunk chunk : this.chunks) { + cx += chunk.chunkX; + cz += chunk.chunkZ; + } + } else if (this.chunks_coords != null) { + count = this.chunks_coords.length; + for (long chunk : this.chunks_coords) { + cx += MathUtil.longHashMsw(chunk); + cz += MathUtil.longHashLsw(chunk); + } + } else { + return null; + } + } + if (count > 0) { + cx /= count; + cz /= count; + } + return new BatchChunkInfo((int) cx, (int) cz, count); + } + + @Override + public String getStatus() { + BatchChunkInfo chunk = this.getAverageChunk(); + if (chunk != null) { + String postfix = " chunks near " + + "x=" + (chunk.cx*16) + " z=" + (chunk.cz*16); + if (this.stage == Stage.LOADING) { + synchronized (this.chunks_lock) { + if (this.chunks != null) { + int num_loaded = 0; + for (LightingChunk lc : this.chunks) { + if (!lc.forcedChunk.isNone() && lc.forcedChunk.getChunkAsync().isDone()) { + num_loaded++; + } + } + return "Loaded " + num_loaded + "/" + chunk.count + postfix; + } + } + } else if (this.stage == Stage.APPLYING) { + synchronized (this.chunks_lock) { + if (this.chunks != null) { + int num_saved = 0; + for (LightingChunk lc : this.chunks) { + if (lc.isApplied) { + num_saved++; + } + } + return "Saved " + num_saved + "/" + chunk.count + postfix; + } + } + } + + return "Cleaning " + chunk.count + postfix; + } else { + return done ? "Done" : "No Data"; + } + } + + private String getShortStatus() { + BatchChunkInfo chunk = this.getAverageChunk(); + if (chunk != null) { + return "[x=" + (chunk.cx*16) + " z=" + (chunk.cz*16) + " count=" + chunk.count + "]"; + } else { + return "[Unknown]"; + } + } + + private boolean waitForCheckAborted(CompletableFuture future) { + while (!aborted) { + try { + future.get(200, TimeUnit.MILLISECONDS); + return true; + } catch (InterruptedException | TimeoutException e1) { + // Ignore + } catch (ExecutionException ex) { + ex.printStackTrace(); + Iris.error("Error while processing"); + return false; + } + } + return false; + } + + private void tryLoadMoreChunks(final CompletableFuture[] chunkFutures) { + if (this.aborted) { + return; + } + + int i = 0; + while (true) { + // While synchronized, pick the next chunk to load + LightingChunk nextChunk = null; + CompletableFuture nextChunkFuture = null; + synchronized (chunks_lock) { + for (; i < chunks.length && numBeingLoaded < Iris.getThreadCount(); i++) { + LightingChunk lc = chunks[i]; + if (lc.loadingStarted) { + continue; // Already (being) loaded + } + + // Pick it + numBeingLoaded++; + lc.loadingStarted = true; + nextChunk = lc; + nextChunkFuture = chunkFutures[i]; + break; + } + } + + // No more chunks to load / capacity reached + if (nextChunk == null) { + break; + } + + // This shouldn't happen, but just in case, a check + if (nextChunkFuture.isDone()) { + continue; + } + + // Outside of the lock, start loading the next chunk + final CompletableFuture f_nextChunkFuture = nextChunkFuture; + nextChunk.forcedChunk.move(LightingForcedChunkCache.get(world, nextChunk.chunkX, nextChunk.chunkZ)); + nextChunk.forcedChunk.getChunkAsync().whenComplete((chunk, t) -> { + synchronized (chunks_lock) { + numBeingLoaded--; + } + + f_nextChunkFuture.complete(null); + tryLoadMoreChunks(chunkFutures); + }); + } + } + + @SuppressWarnings("unchecked") + private CompletableFuture loadChunks() { + // For every LightingChunk, make a completable future + // Once all these futures are resolved the returned completable future resolves + CompletableFuture[] chunkFutures; + synchronized (this.chunks_lock) { + chunkFutures = new CompletableFuture[this.chunks.length]; + } + for (int i = 0; i < chunkFutures.length; i++) { + chunkFutures[i] = new CompletableFuture(); + } + + // Start loading up to [asyncLoadConcurrency] number of chunks right now + // When a callback for a chunk load completes, we start loading additional chunks + tryLoadMoreChunks(chunkFutures); + + return CompletableFuture.allOf(chunkFutures); + } + + @Override + public void process() { + // Begin + this.stage = Stage.LOADING; + this.timeStarted = System.currentTimeMillis(); + + // Initialize lighting chunks + synchronized (this.chunks_lock) { + LightingChunk[] chunks_new = new LightingChunk[this.chunks_coords.length]; + this.done = false; + int chunkIdx = 0; + for (long longCoord : this.chunks_coords) { + int x = MathUtil.longHashMsw(longCoord); + int z = MathUtil.longHashLsw(longCoord); + chunks_new[chunkIdx++] = new LightingChunk(this.world, x, z); + if (this.aborted) { + return; + } + } + + // Update fields. We can remove the coordinates to free memory. + this.chunks = chunks_new; + this.chunks_coords = null; + } + + // Check aborted + if (aborted) { + return; + } + + // Load all the chunks. Wait for loading to finish. + // Regularly check that this task is not aborted + CompletableFuture loadChunksFuture = this.loadChunks(); + if (!waitForCheckAborted(loadChunksFuture)) { + return; + } + + // Causes all chunks in cache not used for this task to unload + // All chunks of this task are put into the cache, instead + LightingForcedChunkCache.reset(); + for (LightingChunk lc : LightingTaskBatch.this.chunks) { + LightingForcedChunkCache.store(lc.forcedChunk); + } + + // All chunks that can be loaded, are now loaded. + // Some chunks may have failed to be loaded, get rid of those now! + // To avoid massive spam, only show the average x/z coordinates of the chunk affected + synchronized (this.chunks_lock) { + long failed_chunk_avg_x = 0; + long failed_chunk_avg_z = 0; + int failed_chunk_count = 0; + + LightingChunk[] new_chunks = this.chunks; + for (int i = new_chunks.length-1; i >= 0; i--) { + LightingChunk lc = new_chunks[i]; + if (lc.forcedChunk.getChunkAsync().isCompletedExceptionally()) { + failed_chunk_avg_x += lc.chunkX; + failed_chunk_avg_z += lc.chunkZ; + failed_chunk_count++; + new_chunks = LogicUtil.removeArrayElement(new_chunks, i); + } + } + this.chunks = new_chunks; + + // Tell all the (remaining) chunks about other neighbouring chunks before initialization + for (LightingChunk lc : new_chunks) { + for (LightingChunk neigh : new_chunks) { + lc.notifyAccessible(neigh); + } + } + + // Log when chunks fail to be loaded + if (failed_chunk_count > 0) { + failed_chunk_avg_x = ((failed_chunk_avg_x / failed_chunk_count) << 4); + failed_chunk_avg_z = ((failed_chunk_avg_z / failed_chunk_count) << 4); + Iris.error("Failed to load " + failed_chunk_count + " chunks near " + + "world=" + world.getName() + " x=" + failed_chunk_avg_x + " z=" + failed_chunk_avg_z); + } + } + + // Schedule, on the main thread, to fill all the loaded chunks with data + CompletableFuture chunkFillFuture = CompletableFuture.runAsync(() -> { + synchronized (this.chunks_lock) { + for (LightingChunk lc : chunks) { + lc.fill(lc.forcedChunk.getChunk(), region_y_coords); + } + } + }, CommonUtil.getPluginExecutor(Iris.instance)); + + if (!waitForCheckAborted(chunkFillFuture)) { + return; + } + + // Now that all chunks we can process are filled, let all the 16x16x16 cubes know of their neighbors + // This neighboring data is only used during the fix() (initialize + spread) phase + synchronized (this.chunks_lock) { + for (LightingChunk lc : chunks) { + lc.detectCubeNeighbors(); + } + } + + // Fix + this.stage = Stage.FIXING; + fix(); + if (this.aborted) { + return; + } + + // Apply and wait for it to be finished + // Wait in 200ms intervals to allow for aborting + // After 2 minutes of inactivity, stop waiting and consider applying failed + this.stage = Stage.APPLYING; + try { + CompletableFuture future = apply(); + int max_num_of_waits = (5*120); + while (true) { + if (--max_num_of_waits == 0) { + Iris.error("Failed to apply lighting data for " + getShortStatus() + ": Timeout"); + break; + } + try { + future.get(200, TimeUnit.MILLISECONDS); + break; + } catch (TimeoutException e) { + if (this.aborted) { + return; + } + } + } + } catch (InterruptedException e) { + // Ignore + } catch (ExecutionException e) { + e.printStackTrace(); + Iris.error("Failed to apply lighting data for " + getShortStatus()); + + } + + this.done = true; + synchronized (this.chunks_lock) { + this.chunks = null; + } + } + + @Override + public void abort() { + this.aborted = true; + + // Close chunks kept loaded + LightingChunk[] chunks; + synchronized (this.chunks_lock) { + chunks = this.chunks; + } + if (chunks != null) { + for (LightingChunk lc : chunks) { + lc.forcedChunk.close(); + } + } + } + + /** + * Starts applying the new data to the world. + * This is done in several ticks on the main thread. + * The completable future is resolved when applying is finished. + */ + public CompletableFuture apply() { + // Apply data to chunks and unload if needed + LightingChunk[] chunks = LightingTaskBatch.this.chunks; + CompletableFuture[] applyFutures = new CompletableFuture[chunks.length]; + for (int i = 0; i < chunks.length; i++) { + LightingChunk lc = chunks[i]; + Chunk bchunk = lc.forcedChunk.getChunk(); + + // Save to chunk + applyFutures[i] = lc.saveToChunk(bchunk).whenComplete((changed, t) -> { + if (t != null) { + t.printStackTrace(); + } else if (changed.booleanValue()) { + WorldUtil.queueChunkSendLight(world, lc.chunkX, lc.chunkZ); + } + + // Closes our forced chunk, may cause the chunk to now unload + lc.forcedChunk.close(); + }); + } + return CompletableFuture.allOf(applyFutures); + } + + /** + * Performs the (slow) fixing procedure (call from another thread) + */ + public void fix() { + // Initialize light + for (LightingCategory category : LightingCategory.values()) { + for (LightingChunk chunk : chunks) { + category.initialize(chunk); + if (this.aborted) { + return; + } + } + } + + // Skip spread phase when debug mode is active + if (this.options.getDebugMakeCorrupted()) { + return; + } + + // Before spreading, change the opacity values to have a minimum of 1 + // Spreading can never be done without losing light + // This isn't done during initialization because it is important + // for calculating the first opacity>0 block for sky light. + for (LightingChunk chunk : chunks) { + for (LightingCube section : chunk.getSections()) { + //TODO: Maybe build something into BKCommonLib for this + int x, y, z; + for (y = 0; y < 16; y++) { + for (z = 0; z < 16; z++) { + for (x = 0; x < 16; x++) { + if (section.opacity.get(x, y, z) == 0) { + section.opacity.set(x, y, z, 1); + } + } + } + } + } + } + + // Spread (timed, for debug) + boolean hasFaults; + long startTime = System.currentTimeMillis(); + int totalLoops = 0; + do { + hasFaults = false; + for (LightingChunk chunk : chunks) { + int count = chunk.spread(); + totalLoops += count; + hasFaults |= count > 0; + } + } while (hasFaults && !this.aborted); + + long duration = System.currentTimeMillis() - startTime; + if (DEBUG_LOG) { + System.out.println("Processed " + totalLoops + " in " + duration + " ms"); + } + } + + @Override + public void applyOptions(LightingService.ScheduleArguments args) { + this.options = args; + } + + @Override + public boolean canSave() { + return !this.options.getLoadedChunksOnly() && !this.options.getDebugMakeCorrupted(); + } + + private static enum Stage { + LOADING, FIXING, APPLYING + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingTaskWorld.java b/src/main/java/com/volmit/iris/v2/lighting/LightingTaskWorld.java new file mode 100644 index 000000000..78fd6ceff --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingTaskWorld.java @@ -0,0 +1,173 @@ +package com.volmit.iris.v2.lighting; + +import com.bergerkiller.bukkit.common.utils.CommonUtil; +import com.bergerkiller.bukkit.common.wrappers.LongHashSet; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.bukkit.World; + +public class LightingTaskWorld implements LightingTask { + private static final int ASSUMED_CHUNKS_PER_REGION = 34 * 34; + private final World world; + private volatile FlatRegionInfoMap regions = null; + private volatile int regionCountLoaded; + private volatile int chunkCount; + private volatile long timeStarted; + private volatile boolean aborted; + private LightingService.ScheduleArguments options = new LightingService.ScheduleArguments(); + + public LightingTaskWorld(World world) { + this.world = world; + this.regionCountLoaded = 0; + this.aborted = false; + this.chunkCount = 0; + this.timeStarted = 0; + } + + @Override + public World getWorld() { + return this.world; + } + + @Override + public int getChunkCount() { + return chunkCount; + } + + @Override + public long getTimeStarted() { + return this.timeStarted; + } + + @Override + public String getStatus() { + if (regions == null) { + return "Reading available regions from world " + getWorld().getName(); + } else { + return "Reading available chunks from world " + getWorld().getName() + " (region " + (regionCountLoaded+1) + "/" + regions.getRegionCount() + ")"; + } + } + + @Override + public void process() { + // Load regions on the main thread + // TODO: Can use main thread executor instead + this.timeStarted = System.currentTimeMillis(); + final CompletableFuture regionsLoadedFuture = new CompletableFuture(); + CommonUtil.nextTick(() -> { + try { + if (this.options.getLoadedChunksOnly()) { + this.regions = FlatRegionInfoMap.createLoaded(this.getWorld()); + this.regionCountLoaded = this.regions.getRegionCount(); + this.chunkCount = 0; + for (FlatRegionInfo region : this.regions.getRegions()) { + this.chunkCount += region.getChunkCount(); + } + } else { + this.regions = FlatRegionInfoMap.create(this.getWorld()); + this.regionCountLoaded = 0; + this.chunkCount = this.regions.getRegionCount() * ASSUMED_CHUNKS_PER_REGION; + } + regionsLoadedFuture.complete(null); + } catch (Throwable ex) { + regionsLoadedFuture.completeExceptionally(ex); + } + }); + + // Wait until region list is loaded synchronously + try { + regionsLoadedFuture.get(); + } catch (InterruptedException ex) { + // Ignore + } catch (ExecutionException ex) { + throw new RuntimeException("Failed to load regions", ex.getCause()); + } + + // Check aborted + if (this.aborted) { + return; + } + + // Start loading all chunks contained in the regions + if (!this.options.getLoadedChunksOnly()) { + for (FlatRegionInfo region : this.regions.getRegions()) { + // Abort handling + if (this.aborted) { + return; + } + + // Load and update stats + region.load(); + this.chunkCount -= ASSUMED_CHUNKS_PER_REGION - region.getChunkCount(); + this.regionCountLoaded++; + } + } + + // We now know of all the regions to be processed, convert all of them into tasks + // Use a slightly larger area to avoid cross-region errors + for (FlatRegionInfo region : regions.getRegions()) { + // Abort handling + if (this.aborted) { + return; + } + + // If empty, skip + if (region.getChunkCount() == 0) { + continue; + } + + // Find region Y-coordinates for this 34x34 section of chunks + int[] region_y_coordinates = regions.getRegionYCoordinatesSelfAndNeighbours(region); + + // Reduce count, schedule and clear the buffer + // Put the coordinates that are available + final LongHashSet buffer = new LongHashSet(34*34); + if (true) { + int dx, dz; + for (dx = -1; dx < 33; dx++) { + for (dz = -1; dz < 33; dz++) { + int cx = region.cx + dx; + int cz = region.cz + dz; + if (this.regions.containsChunkAndNeighbours(cx, cz)) { + buffer.add(cx, cz); + } + } + } + } else { + int dx, dz; + for (dx = -1; dx < 33; dx++) { + for (dz = -1; dz < 33; dz++) { + int cx = region.cx + dx; + int cz = region.cz + dz; + if (this.regions.containsChunk(cx, cz)) { + buffer.add(cx, cz); + } + } + } + } + + // Schedule and return amount of chunks + this.chunkCount -= buffer.size(); + LightingTaskBatch batch_task = new LightingTaskBatch(this.getWorld(), region_y_coordinates, buffer); + batch_task.applyOptions(this.options); + LightingService.schedule(batch_task); + } + } + + @Override + public void abort() { + this.aborted = true; + } + + @Override + public void applyOptions(LightingService.ScheduleArguments args) { + this.options = args; + } + + @Override + public boolean canSave() { + return false; + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/LightingUtil.java b/src/main/java/com/volmit/iris/v2/lighting/LightingUtil.java new file mode 100644 index 000000000..c24762fb3 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/LightingUtil.java @@ -0,0 +1,30 @@ +package com.volmit.iris.v2.lighting; + +import com.bergerkiller.bukkit.common.utils.MathUtil; + +/** + * Just some utilities used by Light Cleaner + */ +public class LightingUtil { + private static TimeDurationFormat timeFormat_hh_mm = new TimeDurationFormat("HH 'hours' mm 'minutes'"); + private static TimeDurationFormat timeFormat_mm_ss = new TimeDurationFormat("mm 'minutes' ss 'seconds'"); + + private static final long SECOND_MILLIS = 1000L; + private static final long MINUTE_MILLIS = 60L * SECOND_MILLIS; + private static final long HOUR_MILLIS = 60L * MINUTE_MILLIS; + private static final long DAY_MILLIS = 24L * HOUR_MILLIS; + + public static String formatDuration(long duration) { + if (duration < MINUTE_MILLIS) { + return MathUtil.round((double) duration / (double) SECOND_MILLIS, 2) + " seconds"; + } else if (duration < HOUR_MILLIS) { + return timeFormat_mm_ss.format(duration); + } else if (duration < (2*DAY_MILLIS)) { + return timeFormat_hh_mm.format(duration); + } else { + long num_days = duration / DAY_MILLIS; + long num_hours = (duration % DAY_MILLIS) / HOUR_MILLIS; + return num_days + " days " + num_hours + " hours"; + } + } +} diff --git a/src/main/java/com/volmit/iris/v2/lighting/TimeDurationFormat.java b/src/main/java/com/volmit/iris/v2/lighting/TimeDurationFormat.java new file mode 100644 index 000000000..b0b391e1e --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/lighting/TimeDurationFormat.java @@ -0,0 +1,44 @@ +package com.volmit.iris.v2.lighting; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Formatter for a duration String. + * Can represent a duration in milliseconds as a String. + * Taken from Traincarts (permission granted by same author)
+ *
+ * https://github.com/bergerhealer/TrainCarts/blob/master/src/main/java/com/bergerkiller/bukkit/tc/utils/TimeDurationFormat.java + */ +public class TimeDurationFormat { + private final TimeZone timeZone; + private final SimpleDateFormat sdf; + + /** + * Creates a new time duration format. The format accepts the same formatting + * tokens as the Date formatter does. + * + * @param format + * @throws IllegalArgumentException if the input format is invalid + */ + public TimeDurationFormat(String format) { + if (format == null) { + throw new IllegalArgumentException("Input format should not be null"); + } + this.timeZone = TimeZone.getTimeZone("GMT+0"); + this.sdf = new SimpleDateFormat(format, Locale.getDefault()); + this.sdf.setTimeZone(this.timeZone); + } + + /** + * Formats the duration + * + * @param durationMillis + * @return formatted string + */ + public String format(long durationMillis) { + return this.sdf.format(new Date(durationMillis - this.timeZone.getRawOffset())); + } +} diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/Engine.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/Engine.java index 5878fce51..87d5d0324 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/engine/Engine.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/Engine.java @@ -14,6 +14,10 @@ import com.volmit.iris.v2.scaffold.parallax.ParallaxAccess; public interface Engine extends DataProvider { + public void close(); + + public EngineWorldManager getWorldManager(); + public void setParallelism(int parallelism); public int getParallelism(); diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedStructure.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedStructure.java deleted file mode 100644 index 9f30db05f..000000000 --- a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedStructure.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.volmit.iris.v2.scaffold.engine; - -public abstract class EngineAssignedStructure extends EngineAssignedComponent implements EngineStructure { - public EngineAssignedStructure(Engine engine) { - super(engine, "Structure"); - } -} diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedStructureManager.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedStructureManager.java new file mode 100644 index 000000000..832d405a9 --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedStructureManager.java @@ -0,0 +1,7 @@ +package com.volmit.iris.v2.scaffold.engine; + +public abstract class EngineAssignedStructureManager extends EngineAssignedComponent implements EngineStructureManager { + public EngineAssignedStructureManager(Engine engine) { + super(engine, "Structure"); + } +} diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedWorldManager.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedWorldManager.java new file mode 100644 index 000000000..12f7b414c --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineAssignedWorldManager.java @@ -0,0 +1,73 @@ +package com.volmit.iris.v2.scaffold.engine; + +import com.volmit.iris.Iris; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.event.world.WorldSaveEvent; +import org.bukkit.event.world.WorldUnloadEvent; + +public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, Listener { + private final int taskId; + + public EngineAssignedWorldManager(Engine engine) { + super(engine, "World"); + Iris.instance.registerListener(this); + taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::onTick, 0, 0); + } + + @EventHandler + public void on(WorldSaveEvent e) + { + if(e.getWorld().equals(getTarget().getWorld())) + { + onSave(); + } + } + + @EventHandler + public void on(WorldUnloadEvent e) + { + if(e.getWorld().equals(getTarget().getWorld())) + { + getEngine().close(); + } + } + + @EventHandler + public void on(EntitySpawnEvent e) + { + if(e.getEntity().getWorld().equals(getTarget().getWorld())) + { + onEntitySpawn(e); + } + } + + @EventHandler + public void on(BlockBreakEvent e) + { + if(e.getPlayer().getWorld().equals(getTarget().getWorld())) + { + onBlockBreak(e); + } + } + + @EventHandler + public void on(BlockPlaceEvent e) + { + if(e.getPlayer().getWorld().equals(getTarget().getWorld())) + { + onBlockPlace(e); + } + } + + @Override + public void close() { + super.close(); + Iris.instance.unregisterListener(this); + Bukkit.getScheduler().cancelTask(taskId); + } +} diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineComponent.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineComponent.java index 6d7cae24c..a1ec0da09 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineComponent.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineComponent.java @@ -1,10 +1,13 @@ package com.volmit.iris.v2.scaffold.engine; +import com.volmit.iris.Iris; import com.volmit.iris.manager.IrisDataManager; import com.volmit.iris.object.IrisDimension; import com.volmit.iris.util.RollingSequence; import com.volmit.iris.v2.generator.IrisComplex; import com.volmit.iris.v2.scaffold.parallax.ParallaxAccess; +import org.bukkit.Bukkit; +import org.bukkit.event.Listener; public interface EngineComponent { public Engine getEngine(); @@ -13,6 +16,22 @@ public interface EngineComponent { public String getName(); + default void close() + { + try + { + if(this instanceof Listener) + { + Iris.instance.unregisterListener((Listener) this); + } + } + + catch(Throwable ignored) + { + + } + } + default double modX(double x) { return getEngine().modifyX(x); diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineFramework.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineFramework.java index f93797f65..8edac1261 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineFramework.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineFramework.java @@ -1,7 +1,6 @@ package com.volmit.iris.v2.scaffold.engine; import com.volmit.iris.util.M; -import com.volmit.iris.v2.generator.modifier.IrisCaveModifier; import com.volmit.iris.v2.scaffold.parallel.MultiBurst; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; @@ -16,7 +15,7 @@ public interface EngineFramework extends DataProvider public IrisComplex getComplex(); - public EngineParallax getEngineParallax(); + public EngineParallaxManager getEngineParallax(); default IrisDataManager getData() { return getComplex().getData(); @@ -46,4 +45,6 @@ public interface EngineFramework extends DataProvider public EngineModifier getDepositModifier(); public EngineModifier getPostModifier(); + + void close(); } diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineParallax.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineParallaxManager.java similarity index 98% rename from src/main/java/com/volmit/iris/v2/scaffold/engine/EngineParallax.java rename to src/main/java/com/volmit/iris/v2/scaffold/engine/EngineParallaxManager.java index f568637df..c4c7db479 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineParallax.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineParallaxManager.java @@ -1,10 +1,7 @@ package com.volmit.iris.v2.scaffold.engine; -import java.lang.reflect.Parameter; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import com.volmit.iris.gen.ParallaxTerrainProvider; import com.volmit.iris.object.*; import com.volmit.iris.util.*; import com.volmit.iris.v2.generator.actuator.IrisTerrainActuator; @@ -23,7 +20,7 @@ import com.volmit.iris.v2.scaffold.parallax.ParallaxAccess; import com.volmit.iris.v2.scaffold.parallel.BurstExecutor; import com.volmit.iris.v2.scaffold.parallel.MultiBurst; -public interface EngineParallax extends DataProvider, IObjectPlacer +public interface EngineParallaxManager extends DataProvider, IObjectPlacer { public static final BlockData AIR = B.get("AIR"); @@ -31,7 +28,7 @@ public interface EngineParallax extends DataProvider, IObjectPlacer public int getParallaxSize(); - public EngineStructure getStructureManager(); + public EngineStructureManager getStructureManager(); default EngineFramework getFramework() { @@ -438,4 +435,9 @@ public interface EngineParallax extends DataProvider, IObjectPlacer default boolean isDebugSmartBore() { return getEngine().getDimension().isDebugSmartBore(); } + + default void close() + { + + } } diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineStructure.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineStructureManager.java similarity index 98% rename from src/main/java/com/volmit/iris/v2/scaffold/engine/EngineStructure.java rename to src/main/java/com/volmit/iris/v2/scaffold/engine/EngineStructureManager.java index 0c44f28e0..92d4e5d22 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineStructure.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineStructureManager.java @@ -9,7 +9,7 @@ import com.volmit.iris.util.KSet; import com.volmit.iris.util.RNG; import com.volmit.iris.v2.scaffold.parallax.ParallaxChunkMeta; -public interface EngineStructure extends EngineComponent +public interface EngineStructureManager extends EngineComponent { default void placeStructure(IrisStructurePlacement structure, RNG rngno, int cx, int cz) { diff --git a/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineWorldManager.java b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineWorldManager.java new file mode 100644 index 000000000..0e759230d --- /dev/null +++ b/src/main/java/com/volmit/iris/v2/scaffold/engine/EngineWorldManager.java @@ -0,0 +1,23 @@ +package com.volmit.iris.v2.scaffold.engine; + +import org.bukkit.Chunk; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntitySpawnEvent; + +public interface EngineWorldManager +{ + public void close(); + + public void onEntitySpawn(EntitySpawnEvent e); + + public void onTick(); + + public void onSave(); + + public void spawnInitialEntities(Chunk chunk); + + public void onBlockBreak(BlockBreakEvent e); + + public void onBlockPlace(BlockPlaceEvent e); +} diff --git a/src/main/java/com/volmit/iris/v2/scaffold/hunk/Hunk.java b/src/main/java/com/volmit/iris/v2/scaffold/hunk/Hunk.java index e6e27b495..74bf976e4 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/hunk/Hunk.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/hunk/Hunk.java @@ -597,6 +597,22 @@ public interface Hunk return this; } + default Hunk iterateSync(Consumer4 c) + { + for(int i = 0; i < getWidth(); i++) + { + for(int j = 0; j < getHeight(); j++) + { + for(int k = 0; k < getDepth(); k++) + { + c.accept(i, j, k, get(i,j,k)); + } + } + } + + return this; + } + default Hunk iterate(int parallelism, Consumer3 c) { compute3D(parallelism, (x, y, z, h) -> diff --git a/src/main/java/com/volmit/iris/v2/scaffold/hunk/storage/MappedHunk.java b/src/main/java/com/volmit/iris/v2/scaffold/hunk/storage/MappedHunk.java index 696e3dfaa..ed81f1802 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/hunk/storage/MappedHunk.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/hunk/storage/MappedHunk.java @@ -1,5 +1,6 @@ package com.volmit.iris.v2.scaffold.hunk.storage; +import com.volmit.iris.util.Consumer4; import com.volmit.iris.v2.scaffold.hunk.Hunk; import com.volmit.iris.util.BlockPosition; import com.volmit.iris.util.KMap; @@ -7,6 +8,8 @@ import com.volmit.iris.util.KMap; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.Map; + @Data @EqualsAndHashCode(callSuper = false) public class MappedHunk extends StorageHunk implements Hunk @@ -25,6 +28,17 @@ public class MappedHunk extends StorageHunk implements Hunk data.put(new BlockPosition(x, y, z), t); } + @Override + public Hunk iterateSync(Consumer4 c) + { + for(Map.Entry g : data.entrySet()) + { + c.accept( g.getKey().getX(), g.getKey().getY(), g.getKey().getZ(), g.getValue()); + } + + return this; + } + @Override public T getRaw(int x, int y, int z) { diff --git a/src/main/java/com/volmit/iris/v2/scaffold/parallax/ParallaxChunkMeta.java b/src/main/java/com/volmit/iris/v2/scaffold/parallax/ParallaxChunkMeta.java index c6f45df7b..793beab54 100644 --- a/src/main/java/com/volmit/iris/v2/scaffold/parallax/ParallaxChunkMeta.java +++ b/src/main/java/com/volmit/iris/v2/scaffold/parallax/ParallaxChunkMeta.java @@ -17,6 +17,7 @@ public class ParallaxChunkMeta { public static final Function> adapter = (c) -> new PaletteHunkIOAdapter() { @Override public void write(ParallaxChunkMeta parallaxChunkMeta, DataOutputStream dos) throws IOException { + dos.writeBoolean(parallaxChunkMeta.isUpdates()); dos.writeBoolean(parallaxChunkMeta.isGenerated()); dos.writeBoolean(parallaxChunkMeta.isParallaxGenerated()); dos.writeBoolean(parallaxChunkMeta.isObjects()); @@ -30,15 +31,17 @@ public class ParallaxChunkMeta { @Override public ParallaxChunkMeta read(DataInputStream din) throws IOException { + boolean bb = din.readBoolean(); boolean g = din.readBoolean(); boolean p = din.readBoolean(); boolean o = din.readBoolean(); int min = o ? din.readByte() - Byte.MIN_VALUE : -1; int max = o ? din.readByte() - Byte.MIN_VALUE : -1; - return new ParallaxChunkMeta(g, p, o, min, max); + return new ParallaxChunkMeta(bb, g, p, o, min, max); } }; + private boolean updates; private boolean generated; private boolean parallaxGenerated; private boolean objects; @@ -47,6 +50,6 @@ public class ParallaxChunkMeta { public ParallaxChunkMeta() { - this(false, false, false, -1, -1); + this(false, false, false, false, -1, -1); } }