diff --git a/plugin/environment/dependency-reduced-pom.xml b/plugin/environment/dependency-reduced-pom.xml index dc5770b..533f9b1 100644 --- a/plugin/environment/dependency-reduced-pom.xml +++ b/plugin/environment/dependency-reduced-pom.xml @@ -1,12 +1,12 @@ - mc-mesh-master + mc-mesh-plugin com.aaaaahhhhhhh.bananapuncher714 - 1.0.0 + 0.0.1 4.0.0 - mc-mesh-plugin + mc-mesh-plugin-environment 0.0.1-SNAPSHOT @@ -29,25 +29,13 @@ 21 - - maven-jar-plugin - 3.5.0 - - - - true - com.aaaaahhhhhhh.bananapuncher714.minietest.MeshingTest6 - - - - org.spigotmc spigot - 1.21.10-R0.1-SNAPSHOT + 1.21.11-R0.1-SNAPSHOT provided @@ -67,25 +55,17 @@ org.ow2.asm - jackson-annotations - com.fasterxml.jackson.core + lz4-java + at.yawk.lz4 - jackson-core - com.fasterxml.jackson.core - - - jackson-databind - com.fasterxml.jackson.core + azure-json + com.azure oshi-core com.github.oshi - - jcip-annotations - com.github.stephenc.jcip - msal4j com.microsoft.azure @@ -110,22 +90,6 @@ logging com.mojang - - content-type - com.nimbusds - - - lang-tag - com.nimbusds - - - nimbus-jose-jwt - com.nimbusds - - - oauth2-oidc-sdk - com.nimbusds - commons-io commons-io @@ -135,7 +99,11 @@ io.netty - netty-codec + netty-codec-base + io.netty + + + netty-codec-compression io.netty @@ -162,6 +130,10 @@ netty-transport-classes-epoll io.netty + + netty-transport-classes-kqueue + io.netty + netty-transport-native-epoll io.netty @@ -170,6 +142,14 @@ netty-transport-native-epoll io.netty + + netty-transport-native-kqueue + io.netty + + + netty-transport-native-kqueue + io.netty + netty-transport-native-unix-common io.netty @@ -186,14 +166,6 @@ jna-platform net.java.dev.jna - - accessors-smart - net.minidev - - - json-smart - net.minidev - jopt-simple net.sf.jopt-simple @@ -215,8 +187,8 @@ org.apache.logging.log4j - lz4-java - org.lz4 + jspecify + org.jspecify commons-lang diff --git a/plugin/environment/pom.xml b/plugin/environment/pom.xml index f5a90c5..ea05d9b 100644 --- a/plugin/environment/pom.xml +++ b/plugin/environment/pom.xml @@ -14,7 +14,7 @@ org.spigotmc spigot - 1.21.10-R0.1-SNAPSHOT + 1.21.11-R0.1-SNAPSHOT provided diff --git a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/ChunkMeshCache.java b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/ChunkMeshCache.java deleted file mode 100644 index 2e69eee..0000000 --- a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/ChunkMeshCache.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.StandardOpenOption; - -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkLocation; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkMesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.ChunkMeshUtil; - -public class ChunkMeshCache { - File directory; - - public ChunkMeshCache( File directory ) { - this.directory = directory; - } - - public ChunkMesh load( ChunkLocation location ) { - final File file = new File( directory, location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" ); - if ( file.exists() ) { - try { - final byte[] data = Files.readAllBytes( file.toPath() ); - final ChunkMesh mesh = ChunkMeshUtil.deserialize( data ); - - return mesh; - } catch ( IOException e ) { - e.printStackTrace(); - } - } - return null; - } - - public void save( ChunkLocation location, ChunkMesh mesh ) { - final File file = new File( directory, location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" ); - file.getParentFile().mkdirs(); - - final byte[] data = ChunkMeshUtil.serialize( mesh ); - - try { - Files.write( file.toPath(), data, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE ); - } catch ( IOException e ) { - e.printStackTrace(); - } - } -} diff --git a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/McMeshEnvironmentPlugin.java b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/McMeshEnvironmentPlugin.java index 0d4db5b..485e9b3 100644 --- a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/McMeshEnvironmentPlugin.java +++ b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/McMeshEnvironmentPlugin.java @@ -1,21 +1,88 @@ package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.logging.Level; +import java.util.stream.Collectors; import org.apache.commons.lang.SystemUtils; import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPhysicsEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Vector; +import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.Mesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.region.simple.RegionRuleWinding; +import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.region.simple.RegionSimple; +import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.region.simple.RegionSimple.GluWindingRule; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.AbstractMesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataInformation; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataType; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkLocation; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkMesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.IndexedTriangle; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane.PlaneAxis; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.PlaneMesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.SparseMesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.VectorReference; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.ChunkMeshUtil; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.FileUtil; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil.BlockShapeType; import com.jme3.bullet.PhysicsSpace; import com.jme3.bullet.objects.PhysicsRigidBody; public class McMeshEnvironmentPlugin extends JavaPlugin { + private File saveDirectory; + + private Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >(); + private Map< ChunkLocation, ChunkMesh > chunkMeshes = new HashMap< ChunkLocation, ChunkMesh >(); + + private Map< ChunkLocation, ChunkTask > tasks = new HashMap< ChunkLocation, ChunkTask >(); + + private Set< EnvironmentHandler > handlers = new HashSet< EnvironmentHandler >(); + + private ReentrantLock lock = new ReentrantLock(); + @Override public void onEnable() { FileUtil.saveToFile( getResource( "native/windows/x86_64/bulletjme.dll" ), new File( getDataFolder() + "/lib", "bulletjme.dll" ), false ); - FileUtil.saveToFile( getResource( "native/osx/x86_64/libbulletjme.dylib" ), new File( getDataFolder() + "/lib", "libbulletjme.dylib" ), false ); +// FileUtil.saveToFile( getResource( "native/osx/x86_64/libbulletjme.dylib" ), new File( getDataFolder() + "/lib", "libbulletjme.dylib" ), false ); FileUtil.saveToFile( getResource( "native/linux/x86_64/libbulletjme.so" ), new File( getDataFolder() + "/lib", "libbulletjme.so" ), false ); if ( !loadNativeLibraries() ) { getLogger().severe( "Unable to load Bullet JME native libraries! Disabling plugin" ); @@ -23,15 +90,121 @@ public class McMeshEnvironmentPlugin extends JavaPlugin { return; } + saveDirectory = new File( getDataFolder(), "meshes" ); + // Don't log anything PhysicsSpace.logger.setLevel( Level.WARNING ); PhysicsSpace.loggerC.setLevel( Level.WARNING ); PhysicsRigidBody.logger2.setLevel( Level.WARNING ); + + Bukkit.getScheduler().runTaskTimer( this, () -> { + lock.lock(); + updateLocations.entrySet().stream().forEach( e -> { + final ChunkLocation location = e.getKey(); + ChunkMesh mesh = chunkMeshes.get( location ); + if ( mesh != null ) { + final Collection< Location > locs = update( mesh, e.getValue() ); + e.getValue().removeAll( locs ); + + handlers.forEach( h -> h.onMeshUpdate( location, mesh ) ); + } + } ); + + updateLocations.values().removeIf( c -> c.isEmpty() ); + lock.unlock(); + }, 1, 1 ); + + Bukkit.getPluginManager().registerEvents( new Listener() { + @EventHandler + private void onChunkLoad( ChunkLoadEvent event ) { + final ChunkLocation location = new ChunkLocation( event.getChunk() ); + + lock.lock(); + try { + if ( chunkMeshes.containsKey( location ) ) { + // Do nothing + } else { + final ChunkTask task = tasks.get( location ); + if ( task == null ) { + loadMesh( location ); + } else if ( task.type == ChunkTaskType.LOAD ) { + // Do nothing + } else if ( task.type == ChunkTaskType.UNLOAD ) { + // Do nothing + } + } + } finally { + lock.unlock(); + } + + /* + * Order of operations: + * - Check if: + * - The mesh is loaded + * - Do nothing + * - There is an unload queued + * - Do nothing + * - There is a load queued + * - Do nothing + * - Nothing is queued + * - Queue a load + */ + } + + @EventHandler + private void onChunkUnload( ChunkUnloadEvent event ) { + final ChunkLocation location = new ChunkLocation( event.getChunk() ); + + lock.lock(); + try { + if ( chunkMeshes.containsKey( location ) ) { + if ( tasks.containsKey( location ) ) { + // Wait for the chunk to finish loading + } else { + // Force all updates for this chunk + // Queue an unload + unloadMesh( location ); + } + } else { + final ChunkTask task = tasks.get( location ); + if ( task == null ) { + // Do nothing + } else if ( task.type == ChunkTaskType.LOAD ) { + // Do nothing + } else if ( task.type == ChunkTaskType.UNLOAD ) { + // Do nothing + } + } + } finally { + lock.unlock(); + } + + /* + * Order of operations: + * - Check if: + * - The mesh is unloaded + * - Do nothing + * - There is an unload queued + * - Do nothing + * - There is a load queued + * - Do nothing + * - Nothing is queued + * - Queue an unload + */ + } + + @EventHandler( priority = EventPriority.MONITOR ) + private void onBlockUpdateEvent( final BlockPhysicsEvent event ) { + final ChunkLocation chunkLocation = new ChunkLocation( event.getBlock().getChunk() ); + getLocationFor( chunkLocation ).add( event.getBlock().getLocation() ); + } + }, this ); } private final boolean loadNativeLibraries() { if ( SystemUtils.IS_OS_MAC ) { - System.load( new File( getDataFolder() + "/lib", "libbulletjme.dylib" ).getAbsolutePath() ); + getLogger().warning( "Mac not supported!" ); +// System.load( new File( getDataFolder() + "/lib", "libbulletjme.dylib" ).getAbsolutePath() ); } else if ( SystemUtils.IS_OS_LINUX ) { System.load( new File( getDataFolder() + "/lib", "libbulletjme.so" ).getAbsolutePath() ); } else if ( SystemUtils.IS_OS_WINDOWS ) { @@ -41,4 +214,867 @@ public class McMeshEnvironmentPlugin extends JavaPlugin { } return true; } + + private Collection< Location > getLocationFor( ChunkLocation location ) { + lock.lock(); + Collection< Location > locations = updateLocations.get( location ); + if ( locations == null ) { + locations = new HashSet< Location >(); + updateLocations.put( location, locations ); + } + lock.unlock(); + return locations; + } + + public void registerHandler( EnvironmentHandler handler ) { + handlers.add( handler ); + } + + public void unregisterHandler( EnvironmentHandler handler ) { + handlers.remove( handler ); + } + + private void completeTask( ChunkLocation location ) { + lock.lock(); + try { + final ChunkTask task = tasks.remove( location ); + if ( task.type == ChunkTaskType.LOAD ) { + handlers.forEach( h -> h.onMeshLoad( location, chunkMeshes.get( location ) ) ); + if ( !location.isLoaded() ) { + // Update all and unload + unloadMesh( location ); + } else { + // Chunk is still loaded, do nothing + } + } else if ( task.type == ChunkTaskType.UNLOAD ) { + if ( !location.isLoaded() ) { + // Check if there are any updates that we missed + // If so, then queue another unload with the newest mesh + // Otherwise, remove the location from our list of locations + if ( updateLocations.containsKey( location ) ) { + unloadMesh( location ); + } else { + final ChunkMesh mesh = chunkMeshes.remove( location ); + + handlers.forEach( h -> h.onMeshUnload( location, mesh ) ); + } + } else { + // Chunk is loaded, but we saved. Do nothing + } + } + } finally { + lock.unlock(); + } + } + + private void loadMesh( ChunkLocation location ) { + loadMesh( location, false ); + } + + private void loadMesh( ChunkLocation location, boolean force ) { + ChunkTask task = new ChunkTask(); + lock.lock(); + try { + // TODO Make sure tasks does not contain anything beforehand + if ( tasks.containsKey( location ) ) { + return; + } + tasks.put( location, task ); + + task.type = ChunkTaskType.LOAD; + task.task = Bukkit.getScheduler().runTaskAsynchronously( this, () -> { + final ChunkMesh mesh = loadFromFile( location ); + if ( mesh != null ) { + lock.lock(); + try { + // Save the mesh + if ( !chunkMeshes.containsKey( location ) || force ) { + chunkMeshes.put( location, mesh ); + } + + completeTask( location ); + } finally { + lock.unlock(); + } + } else { + // Process and generate the mesh + Bukkit.getScheduler().runTask( this, () -> { + generateMesh( this, location.getWorld(), location.getChunk(), m -> { + lock.lock(); + try { + chunkMeshes.put( location, m ); + + completeTask( location ); + } finally { + lock.unlock(); + } + } ); + } ); + } + } ); + } finally { + lock.unlock(); + } + } + + private void unloadMesh( ChunkLocation location ) { + ChunkTask task = new ChunkTask(); + lock.lock(); + try { + tasks.put( location, task ); + + if ( !applyUpdatesToMesh( location ) ) { + // Not good! Piston issue somewhere + } + + task.type = ChunkTaskType.UNLOAD; + task.task = Bukkit.getScheduler().runTaskAsynchronously( this, () -> { + lock.lock(); + try { + ChunkMesh mesh = chunkMeshes.get( location ); + if ( mesh != null ) { + // Save the mesh + saveToFile( location, mesh ); + } else { + // Not really good + } + + completeTask( location ); + } finally { + lock.unlock(); + } + } ); + } finally { + lock.unlock(); + } + } + + private boolean applyUpdatesToMesh( ChunkLocation location ) { + lock.lock(); + try { + final Collection< Location > locations = updateLocations.get( location ); + final ChunkMesh mesh = chunkMeshes.get( location ); + + if ( locations != null && !locations.isEmpty() && mesh != null ) { + final Collection< Location > removed = update( mesh, locations ); + locations.removeAll( removed ); + + handlers.forEach( h -> h.onMeshUpdate( location, mesh ) ); + + return locations.isEmpty(); + } + } finally { + lock.unlock(); + } + + return true; + } + + public static void generateSpareMesh( JavaPlugin plugin, World world, Collection< Vector > locations, Consumer< SparseMesh > callback ) { + if ( locations.isEmpty() ) { + return; + } + + boolean set = false; + Vector min = new Vector(); + Vector max = new Vector(); + + for ( Vector v : locations ) { + if ( !set ) { + set = true; + + min = v.toBlockVector(); + max = v.toBlockVector(); + } else { + min = Vector.getMinimum( min, v.toBlockVector() ); + max = Vector.getMaximum( max, v.toBlockVector() ); + } + } + + final SparseMesh sparseMesh = new SparseMesh( min, max ); + final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); + + { + final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { + Collection< Polygon > polygonCollection = polygons.get( p ); + if ( polygonCollection == null ) { + polygonCollection = new ArrayDeque< Polygon >(); + polygons.put( p, polygonCollection ); + } + return polygonCollection; + }; + + for ( final Vector location : locations ) { + final Block block = world.getBlockAt( location.getBlockX(), location.getBlockY(), location.getBlockZ() ); + + if ( !( block.isEmpty() && block.isLiquid() ) ) { + final BlockData data = block.getBlockData(); + final Location loc = block.getLocation(); + + // Start the box at 0, 0, 0 + final BoundingBox[] boundingBoxes = NmsUtil.getShape( data, loc, BlockShapeType.COLLISION_SHAPE ).e().stream().map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).toArray( BoundingBox[]::new ); + + final BlockDataInformation information = new BlockDataInformation( data ); + + if ( boundingBoxes.length == 0 ) { + // No bounding box + information.type = BlockDataType.NONE; + } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { + // Solid + information.type = BlockDataType.SOLID; + } else { + information.type = BlockDataType.COMPLEX; + // Complex bounding shape + + // Add the voxel shape to the chunk mesh + for ( final BoundingBox aabb : boundingBoxes ) { + final double minX = aabb.getMinX() + loc.getBlockX() - min.getBlockX(); + final double minY = aabb.getMinY() + loc.getBlockY() - min.getBlockY(); + final double minZ = aabb.getMinZ() + loc.getBlockZ() - min.getBlockZ(); + final double maxX = aabb.getMaxX() + loc.getBlockX() - min.getBlockX(); + final double maxY = aabb.getMaxY() + loc.getBlockY() - min.getBlockY(); + final double maxZ = aabb.getMaxZ() + loc.getBlockZ() - min.getBlockZ(); + + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( minY, minZ ), + new Point( minY, maxZ ), + new Point( maxY, maxZ ), + new Point( maxY, minZ ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( minX, minZ ), + new Point( minX, maxZ ), + new Point( maxX, maxZ ), + new Point( maxX, minZ ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( minX, minY ), + new Point( minX, maxY ), + new Point( maxX, maxY ), + new Point( maxX, minY ) + ) ); + + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); + } + + information.boundingBoxes = boundingBoxes; + sparseMesh.setBlockDataInformationFor( information, loc.toVector().subtract( min ) ); + } + } + } + + // Now generate facets for each solid block based on if the side they are on is visible + for ( final Vector location : locations ) { + final Vector blockVector = location.toBlockVector().subtract( min ); + final BlockDataInformation info = sparseMesh.getBlockDataInformationFor( blockVector ); + if ( info != null ) { + if ( info.type == BlockDataType.SOLID ) { + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( blockVector.getY(), blockVector.getZ() ), + new Point( blockVector.getY(), blockVector.getZ() + 1 ), + new Point( blockVector.getY() + 1, blockVector.getZ() + 1 ), + new Point( blockVector.getY() + 1, blockVector.getZ() ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( blockVector.getX(), blockVector.getZ() ), + new Point( blockVector.getX(), blockVector.getZ() + 1 ), + new Point( blockVector.getX() + 1, blockVector.getZ() + 1 ), + new Point( blockVector.getX() + 1, blockVector.getZ() ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( blockVector.getX(), blockVector.getY() ), + new Point( blockVector.getX(), blockVector.getY() + 1 ), + new Point( blockVector.getX() + 1, blockVector.getY() + 1 ), + new Point( blockVector.getX() + 1, blockVector.getY() ) + ) ); + + BlockDataInformation adjacent; + if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().subtract( new Vector( 0, 1, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, blockVector.getY() ) ).add( yPoly ); + } + + if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().add( new Vector( 0, 1, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, blockVector.getY() + 1 ) ).add( yPoly ); + } + + if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().subtract( new Vector( 0, 0, 1 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, blockVector.getZ() ) ).add( zPoly ); + } + + if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().add( new Vector( 0, 0, 1 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, blockVector.getZ() + 1 ) ).add( zPoly ); + } + + if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().subtract( new Vector( 1, 0, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, blockVector.getX() ) ).add( xPoly ); + } + + if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().add( new Vector( 1, 0, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, blockVector.getX() + 1 ) ).add( xPoly ); + } + } + } else { + // Shouldn't be null btw + } + } + } + + Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> { + // Mesh the data and get the output + final MeshResult meshResult = generateMeshFrom( polygons ); + + sparseMesh.planes = meshResult.meshes; + sparseMesh.pointReferences = meshResult.points; + + callback.accept( sparseMesh ); + } ); + } + + // TODO Use ChunkSnapshot in the future + public static void generateMesh( JavaPlugin plugin, World world, final Chunk chunk, Consumer< ChunkMesh > callback ) { + final int worldMinHeight = world.getMinHeight(); + final int worldMaxHeight = world.getMaxHeight(); + final int worldHeight = worldMaxHeight - worldMinHeight; + + final int cx = chunk.getX(); + final int cz = chunk.getZ(); + + final ChunkMesh chunkMesh = new ChunkMesh( new BoundingBox( cx << 4, worldMinHeight, cz << 4, ( cx << 4 ) + 16, worldMaxHeight, ( cz << 4 ) + 16 ) ); + final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); + + { + final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { + Collection< Polygon > polygonCollection = polygons.get( p ); + if ( polygonCollection == null ) { + polygonCollection = new ArrayDeque< Polygon >(); + polygons.put( p, polygonCollection ); + } + return polygonCollection; + }; + + // When we would have inserted each facet, instead, find or create the + // corresponding plane, and add directly. + // Also, create a chunk mesh and populate the structs + for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { + final int relY = y - worldMinHeight; + final int yIndex = relY << 8; + for ( int z = 0; z < 16; ++z ) { + final int zIndex = yIndex + ( z << 4 ); + for ( int x = 0; x < 16; ++x ) { + final int index = zIndex + x; + final Block block = chunk.getBlock( x, y, z ); + if ( !( block.isEmpty() && block.isLiquid() ) ) { + final BlockData data = block.getBlockData(); + final Location loc = block.getLocation(); + + // Start the box at 0, 0, 0 + final BoundingBox[] boundingBoxes = NmsUtil.getShape( data, loc, BlockShapeType.COLLISION_SHAPE ).e().stream().map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).toArray( BoundingBox[]::new ); + + if ( boundingBoxes.length == 0 ) { + // No bounding box + chunkMesh.setType( index, BlockDataType.NONE ); + } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { + // Solid + chunkMesh.setType( index, BlockDataType.SOLID ); + } else { + chunkMesh.setType( index, BlockDataType.COMPLEX ); + // Complex bounding shape + + // Only store the old block data if we know it is not empty or solid!! + chunkMesh.setData( index, data ); + + // Add the voxel shape to the chunk mesh + for ( final BoundingBox aabb : boundingBoxes ) { + final double minX = aabb.getMinX() + x; + final double minY = aabb.getMinY() + relY; + final double minZ = aabb.getMinZ() + z; + final double maxX = aabb.getMaxX() + x; + final double maxY = aabb.getMaxY() + relY; + final double maxZ = aabb.getMaxZ() + z; + + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( minY, minZ ), + new Point( minY, maxZ ), + new Point( maxY, maxZ ), + new Point( maxY, minZ ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( minX, minZ ), + new Point( minX, maxZ ), + new Point( maxX, maxZ ), + new Point( maxX, minZ ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( minX, minY ), + new Point( minX, maxY ), + new Point( maxX, maxY ), + new Point( maxX, minY ) + ) ); + + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); + } + + chunkMesh.setBoxes( index, boundingBoxes ); + } + } else { + chunkMesh.setType( index, BlockDataType.NONE ); + } + } + } + } + + // Now generate facets for each solid block based on if the side they are on is visible + for ( int y = 0; y < worldHeight; ++y ) { + final int yIndex = y << 8; + for ( int z = 0; z < 16; ++z ) { + final int zIndex = z << 4; + for ( int x = 0; x < 16; ++x ) { + final int index = yIndex + zIndex + x; + if ( chunkMesh.getType( index ) == BlockDataType.SOLID ) { + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( y, z ), + new Point( y, z + 1 ), + new Point( y + 1, z + 1 ), + new Point( y + 1, z ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( x, z ), + new Point( x, z + 1 ), + new Point( x + 1, z + 1 ), + new Point( x + 1, z ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( x, y ), + new Point( x, y + 1 ), + new Point( x + 1, y + 1 ), + new Point( x + 1, y ) + ) ); + + // Check each face + if ( y == 0 || chunkMesh.getType( index - 256 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, y ) ).add( yPoly ); + } + + if ( y == worldHeight - 1 || chunkMesh.getType( index + 256 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, y + 1 ) ).add( yPoly ); + } + + if ( z == 0 || chunkMesh.getType( index - 16 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, z ) ).add( zPoly ); + } + + if ( z == 15 || chunkMesh.getType( index + 16 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, z + 1 ) ).add( zPoly ); + } + + if ( x == 0 || chunkMesh.getType( index - 1 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, x ) ).add( xPoly ); + } + + if ( x == 15 || chunkMesh.getType( index + 1 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, x + 1 ) ).add( xPoly ); + } + } + } + } + } + } + + Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> { + // Mesh the data and get the output + final MeshResult meshResult = generateMeshFrom( polygons ); + + chunkMesh.planes = meshResult.meshes; + chunkMesh.pointReferences = meshResult.points; + + callback.accept( chunkMesh ); + } ); + } + + public static Collection< Location > update( final AbstractMesh mesh, final Collection< Location > locations ) { + if ( locations.isEmpty() ) { + return Collections.emptySet(); + } + + final Collection< Location > canRemove = new ArrayDeque< Location >(); + // Check each location + final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); + final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { + Collection< Polygon > polygonCollection = polygons.get( p ); + if ( polygonCollection == null ) { + polygonCollection = new ArrayDeque< Polygon >(); + polygons.put( p, polygonCollection ); + } + return polygonCollection; + }; + + final BiFunction< List< BoundingBox >, BoundingBox[], Boolean > isEqual = ( a, b ) -> { + if ( a.size() == b.length ) { + for ( int i = 0; i < b.length; ++i ) { + final BoundingBox b1 = a.get( i ); + final BoundingBox b2 = b[ i ]; + + if ( !b1.equals( b2 ) ) { + return false; + } + } + } + + return true; + }; + + for ( final Location location : locations ) { + final BlockData data = location.getBlock().getBlockData(); + + // Defer moving piston updates until another tick when it's done moving + if ( data.getMaterial() == Material.MOVING_PISTON ) { + continue; + } + + final Vector position = location.toVector().subtract( mesh.getMinimumCorner() ); + final BlockDataInformation information = mesh.getBlockDataInformationFor( position ); + if ( information != null ) { + canRemove.add( location ); + if ( information.isBlockData( data ) ) { + // Ignore if the block data has not changed + continue; + } else { + final List< BoundingBox > boxes = NmsUtil.getShape( data, location, BlockShapeType.COLLISION_SHAPE ).e().stream() + .map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).collect( Collectors.toList() ); + final BlockDataType oldType = information.type; + final BoundingBox[] otherBoxes = information.boundingBoxes; + if ( boxes.size() == 0 ) { + if ( oldType == BlockDataType.NONE ) { + continue; + } + + BlockDataInformation newInfo = new BlockDataInformation( data ); + newInfo.type = BlockDataType.NONE; + newInfo.boundingBoxes = null; + + mesh.setBlockDataInformationFor( newInfo, position ); + } else if ( boxes.size() == 1 && boxes.get( 0 ).getVolume() == 1 ) { + if ( oldType == BlockDataType.SOLID ) { + continue; + } + + BlockDataInformation newInfo = new BlockDataInformation( data ); + newInfo.type = BlockDataType.SOLID; + newInfo.boundingBoxes = null; + + mesh.setBlockDataInformationFor( newInfo, position ); + } else { + if ( otherBoxes != null && isEqual.apply( boxes, otherBoxes ) ) { + continue; + } + + BlockDataInformation newInfo = new BlockDataInformation( data ); + newInfo.type = BlockDataType.COMPLEX; + newInfo.boundingBoxes = boxes.toArray( BoundingBox[]::new ); + + mesh.setBlockDataInformationFor( newInfo, position ); + } + + if ( oldType == BlockDataType.SOLID ) { + boxes.add( new BoundingBox( 0, 0, 0, 1, 1, 1 ) ); + } else if ( oldType == BlockDataType.COMPLEX ) { + if ( otherBoxes == null || otherBoxes.length == 0 ) { + throw new IllegalStateException( "Complex shape but no bounding boxes!" ); + } + + for ( final BoundingBox box : otherBoxes ) { + boxes.add( box ); + } + } + + // Add the voxel shape to the chunk mesh + for ( final BoundingBox aabb : boxes ) { + final double minX = aabb.getMinX() + position.getX(); + final double minY = aabb.getMinY() + position.getY(); + final double minZ = aabb.getMinZ() + position.getZ(); + final double maxX = aabb.getMaxX() + position.getX(); + final double maxY = aabb.getMaxY() + position.getY(); + final double maxZ = aabb.getMaxZ() + position.getZ(); + + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( minY, minZ ), + new Point( minY, maxZ ), + new Point( maxY, maxZ ), + new Point( maxY, minZ ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( minX, minZ ), + new Point( minX, maxZ ), + new Point( maxX, maxZ ), + new Point( maxX, minZ ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( minX, minY ), + new Point( minX, maxY ), + new Point( maxX, maxY ), + new Point( maxX, minY ) + ) ); + + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); + } + } + } + } + + // No polygons found... which means nothing changed + if ( polygons.isEmpty() ) { + return canRemove; + } + + final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >(); + + // Now mesh the blocks asynchronously and combine, then do the bare minimum to regenerate the chunk mesh + for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final PlaneMesh planeMesh = mesh.planes.remove( plane ); + final Mesh< RegionSimple > mesher = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); + + if ( planeMesh != null ) { + // Reset all indexed polygons(triangles) for this plane + for ( final IndexedTriangle poly : planeMesh.completedPolygons ) { + poly.getPoints().parallelStream().forEach( p -> --p.referenceCount ); + } + + // Add all polygons that were previously generated + for ( final Polygon polygon : planeMesh.generatedRegions ) { + mesher.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); + } + } + for ( final Polygon polygon : entry.getValue() ) { + mesher.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); + } + + meshMap.put( plane, mesher ); + } + + // Mesh each plane + final Map< MinecraftPlane, CompleteMesh > triangles = meshMap.entrySet().parallelStream().collect( Collectors.toConcurrentMap( e -> e.getKey(), e -> new CompleteMesh( e.getKey(), e.getValue() ) ) ); + + for ( final Entry< MinecraftPlane, CompleteMesh > entry : triangles.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final CompleteMesh completeMesh = entry.getValue(); + final Collection< Polygon > tris = completeMesh.triangles; + final Mesh< RegionSimple > mesher = completeMesh.mesh; + + // Ignore any planes that are empty + if ( !tris.isEmpty() ) { + final PlaneMesh newMesh = new PlaneMesh(); + newMesh.completedPolygons = new ArrayList< IndexedTriangle >(); + for ( final Polygon tri : tris ) { + final IndexedTriangle newPoly = new IndexedTriangle(); + for ( int i = 0; i < tri.getPoints().size() && i < 3; ++i ) { + final Point point = tri.getPoints().get( i ); + final Vector vert = plane.convert( point ); + + // Find an existing vertex reference if one exists + final VectorReference tempRef = new VectorReference( vert ); + + VectorReference ref = null; + final VectorReference upperRef = mesh.pointReferences.ceiling( tempRef ); + if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = upperRef; + } else { + final VectorReference lowerRef = mesh.pointReferences.floor( tempRef ); + if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = lowerRef; + } + } + + if ( ref == null ) { + ref = tempRef; + ref.referenceCount = 1; + mesh.pointReferences.add( ref ); + } else { + ++ref.referenceCount; + } + + switch ( i ) { + case 0: + newPoly.setPoint1( ref ); + break; + case 1: + newPoly.setPoint2( ref ); + break; + case 2: + newPoly.setPoint3( ref ); + break; + default: + // Should never be here! + } + } + newMesh.completedPolygons.add( newPoly ); + } + + // Save the generated regions as an intermediary step so we can re-use them later + newMesh.generatedRegions = mesher.getPolygons(); + + mesh.planes.put( plane, newMesh ); + } + } + + // Remove all vector references that have a reference count of 0 + mesh.pointReferences.removeIf( v -> v.referenceCount <= 0 ); + + return canRemove; + } + + public static MeshResult generateMeshFrom( final Map< MinecraftPlane, Collection< Polygon > > polygons ) { + final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >(); + for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); + for ( final Polygon polygon : entry.getValue() ) { + mesh.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); + } + meshMap.put( plane, mesh ); + } + + // Mesh each plane + final Map< MinecraftPlane, CompleteMesh > triangles = meshMap.entrySet().parallelStream().collect( Collectors.toConcurrentMap( e -> e.getKey(), e -> new CompleteMesh( e.getKey(), e.getValue() ) ) ); + + // Construct a series of indexed polygons + final TreeSet< VectorReference > vertices = new TreeSet< VectorReference >(); + + final MeshResult result = new MeshResult(); + for ( final Entry< MinecraftPlane, CompleteMesh > entry : triangles.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final CompleteMesh completeMesh = entry.getValue(); + final Collection< Polygon > tris = completeMesh.triangles; + final Mesh< RegionSimple > mesh = completeMesh.mesh; + + // Ignore any planes that are empty + if ( !tris.isEmpty() ) { + final PlaneMesh newMesh = new PlaneMesh(); + newMesh.completedPolygons = new ArrayList< IndexedTriangle >(); + for ( final Polygon tri : tris ) { + final IndexedTriangle newPoly = new IndexedTriangle(); + for ( int i = 0; i < tri.getPoints().size() && i < 3; ++i ) { + final Point point = tri.getPoints().get( i ); + final Vector vert = plane.convert( point ); + + final VectorReference tempRef = new VectorReference( vert ); + // Find an existing vertex reference if one exists + VectorReference ref = null; + if ( ref == null ) { + final VectorReference upperRef = vertices.ceiling( tempRef ); + if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = upperRef; + } else { + final VectorReference lowerRef = vertices.floor( tempRef ); + if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = lowerRef; + } + } + } + + if ( ref == null ) { + ref = tempRef; + ref.referenceCount = 1; + vertices.add( ref ); + } else { + ++ref.referenceCount; + } + + switch ( i ) { + case 0: + newPoly.setPoint1( ref ); + break; + case 1: + newPoly.setPoint2( ref ); + break; + case 2: + newPoly.setPoint3( ref ); + break; + default: + // Should never be here! + } + + } + newMesh.completedPolygons.add( newPoly ); + } + + // Save the generated regions as an intermediary step so we can re-use them later + newMesh.generatedRegions = mesh.getPolygons(); + + result.meshes.put( plane, newMesh ); + } + } + + // Add all vector references + result.points = vertices; + + return result; + } + + private ChunkMesh loadFromFile( ChunkLocation location ) { + final File file = new File( saveDirectory, location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" ); + if ( file.exists() ) { + try { + final byte[] data = Files.readAllBytes( file.toPath() ); + final ChunkMesh mesh = ChunkMeshUtil.deserialize( data ); + + return mesh; + } catch ( IOException e ) { + e.printStackTrace(); + } + } + return null; + } + + private void saveToFile( ChunkLocation location, ChunkMesh mesh ) { + final File file = new File( saveDirectory, location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" ); + file.getParentFile().mkdirs(); + + final byte[] data = ChunkMeshUtil.serialize( mesh ); + + try { + Files.write( file.toPath(), data, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE ); + } catch ( IOException e ) { + e.printStackTrace(); + } + } + + private static class CompleteMesh { + Mesh< RegionSimple > mesh; + Collection< Polygon > triangles; + + CompleteMesh( final MinecraftPlane plane, final Mesh< RegionSimple > mesh ) { + this.mesh = mesh; + triangles = mesh.meshify(); + } + } + + public static class MeshResult { + Map< MinecraftPlane, PlaneMesh > meshes = new HashMap< MinecraftPlane, PlaneMesh >(); + TreeSet< VectorReference > points = new TreeSet< VectorReference >(); + } + + private enum ChunkTaskType { + LOAD, UNLOAD + } + + private class ChunkTask { + BukkitTask task; + ChunkTaskType type; + } } \ No newline at end of file diff --git a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/MeshEnvironment.java b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/MeshEnvironment.java deleted file mode 100644 index 1895605..0000000 --- a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/MeshEnvironment.java +++ /dev/null @@ -1,1021 +0,0 @@ -package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; - -import java.io.File; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.data.BlockData; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.HandlerList; -import org.bukkit.event.Listener; -import org.bukkit.event.block.BlockPhysicsEvent; -import org.bukkit.event.world.ChunkLoadEvent; -import org.bukkit.event.world.ChunkUnloadEvent; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitTask; -import org.bukkit.util.BoundingBox; -import org.bukkit.util.Vector; - -import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.Mesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.Point; -import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.Polygon; -import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.region.simple.RegionRuleWinding; -import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.region.simple.RegionSimple; -import com.aaaaahhhhhhh.bananapuncher714.mesh.algorithm.region.simple.RegionSimple.GluWindingRule; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.AbstractMesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataInformation; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataType; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkLocation; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkMesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.IndexedTriangle; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane.PlaneAxis; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.PlaneMesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.SparseMesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.VectorReference; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil.BlockShapeType; - -public class MeshEnvironment { - private JavaPlugin plugin; - private File cacheDirectory; - - private Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >(); - private Map< ChunkLocation, ChunkMesh > chunkMeshes = new HashMap< ChunkLocation, ChunkMesh >(); - - private Map< ChunkLocation, ChunkTask > tasks = new HashMap< ChunkLocation, ChunkTask >(); - - private ChunkMeshCache cache; - - private EnvironmentHandler handler; - - private ReentrantLock lock = new ReentrantLock(); - - final BukkitTask task = Bukkit.getScheduler().runTaskTimer( plugin, () -> { - lock.lock(); - updateLocations.entrySet().stream().forEach( e -> { - ChunkMesh mesh = chunkMeshes.get( e.getKey() ); - if ( mesh != null ) { - final Collection< Location > locs = update( mesh, e.getValue() ); - e.getValue().removeAll( locs ); - - if ( handler != null && !locs.isEmpty() ) { - handler.onMeshUpdate( e.getKey(), mesh ); - } - } - } ); - - updateLocations.values().removeIf( c -> c.isEmpty() ); - lock.unlock(); - }, 1, 1 ); - - final Function< ChunkLocation, Collection< Location > > getLocationFor = c -> { - lock.lock(); - Collection< Location > locations = updateLocations.get( c ); - if ( locations == null ) { - locations = new HashSet< Location >(); - updateLocations.put( c, locations ); - } - lock.unlock(); - return locations; - }; - - private Listener eventListener = new Listener() { - @EventHandler - private void onChunkLoad( ChunkLoadEvent event ) { - final ChunkLocation location = new ChunkLocation( event.getChunk() ); - - lock.lock(); - try { - if ( chunkMeshes.containsKey( location ) ) { - // Do nothing - } else { - final ChunkTask task = tasks.get( location ); - if ( task == null ) { - loadMesh( location ); - } else if ( task.type == ChunkTaskType.LOAD ) { - // Do nothing - } else if ( task.type == ChunkTaskType.UNLOAD ) { - // Do nothing - } - } - } finally { - lock.unlock(); - } - - /* - * Order of operations: - * - Check if: - * - The mesh is loaded - * - Do nothing - * - There is an unload queued - * - Do nothing - * - There is a load queued - * - Do nothing - * - Nothing is queued - * - Queue a load - */ - } - - @EventHandler - private void onChunkUnload( ChunkUnloadEvent event ) { - final ChunkLocation location = new ChunkLocation( event.getChunk() ); - - lock.lock(); - try { - if ( chunkMeshes.containsKey( location ) ) { - if ( tasks.containsKey( location ) ) { - // Wait for the chunk to finish loading - } else { - // Force all updates for this chunk - // Queue an unload - unloadMesh( location ); - } - } else { - final ChunkTask task = tasks.get( location ); - if ( task == null ) { - // Do nothing - } else if ( task.type == ChunkTaskType.LOAD ) { - // Do nothing - } else if ( task.type == ChunkTaskType.UNLOAD ) { - // Do nothing - } - } - } finally { - lock.unlock(); - } - - /* - * Order of operations: - * - Check if: - * - The mesh is unloaded - * - Do nothing - * - There is an unload queued - * - Do nothing - * - There is a load queued - * - Do nothing - * - Nothing is queued - * - Queue an unload - */ - } - - @EventHandler( priority = EventPriority.MONITOR ) - private void onBlockUpdateEvent( final BlockPhysicsEvent event ) { - final ChunkLocation chunkLocation = new ChunkLocation( event.getBlock().getChunk() ); - getLocationFor.apply( chunkLocation ).add( event.getBlock().getLocation() ); - } - }; - - public MeshEnvironment( JavaPlugin plugin, File cacheDirectory ) { - this.plugin = plugin; - this.cacheDirectory = cacheDirectory; - cache = new ChunkMeshCache( new File( cacheDirectory, "meshes" ) ); - } - - public void activate() { - Bukkit.getPluginManager().registerEvents( eventListener, plugin ); - } - - public void deactivate() { - HandlerList.unregisterAll( eventListener ); - } - - public void load( ChunkLocation location ) { - loadMesh( location ); - } - - public void setHandler( EnvironmentHandler handler ) { - this.handler = handler; - } - - private void completeTask( ChunkLocation location ) { - lock.lock(); - try { - final ChunkTask task = tasks.remove( location ); - if ( task.type == ChunkTaskType.LOAD ) { - handler.onMeshLoad( location, chunkMeshes.get( location ) ); - if ( !location.isLoaded() ) { - // Update all and unload - unloadMesh( location ); - } else { - // Chunk is still loaded, do nothing - } - } else if ( task.type == ChunkTaskType.UNLOAD ) { - if ( !location.isLoaded() ) { - // Check if there are any updates that we missed - // If so, then queue another unload with the newest mesh - // Otherwise, remove the location from our list of locations - if ( updateLocations.containsKey( location ) ) { - unloadMesh( location ); - } else { - final ChunkMesh mesh = chunkMeshes.remove( location ); - - if ( handler != null ) { - handler.onMeshUnload( location, mesh ); - } - } - } else { - // Chunk is loaded, but we saved. Do nothing - } - } - } finally { - lock.unlock(); - } - } - - private void loadMesh( ChunkLocation location ) { - ChunkTask task = new ChunkTask(); - lock.lock(); - try { - tasks.put( location, task ); - - task.type = ChunkTaskType.LOAD; - task.task = Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> { - final ChunkMesh mesh = cache.load( location ); - if ( mesh != null ) { - lock.lock(); - try { - // Save the mesh - if ( !chunkMeshes.containsKey( location ) ) { - chunkMeshes.put( location, mesh ); - } - - completeTask( location ); - } finally { - lock.unlock(); - } - } else { - // Process and generate the mesh - Bukkit.getScheduler().runTask( plugin, () -> { - generateMesh( plugin, location.getWorld(), location.getChunk(), m -> { - lock.lock(); - try { - chunkMeshes.put( location, m ); - - completeTask( location ); - } finally { - lock.unlock(); - } - } ); - } ); - } - } ); - } finally { - lock.unlock(); - } - } - - private void unloadMesh( ChunkLocation location ) { - ChunkTask task = new ChunkTask(); - lock.lock(); - try { - tasks.put( location, task ); - - if ( !applyUpdatesToMesh( location ) ) { - // Not good! Piston issue somewhere - } - - task.type = ChunkTaskType.UNLOAD; - task.task = Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> { - lock.lock(); - try { - ChunkMesh mesh = chunkMeshes.get( location ); - if ( mesh != null ) { - // Save the mesh - cache.save( location, mesh ); - } else { - // Not really good - } - - completeTask( location ); - } finally { - lock.unlock(); - } - } ); - } finally { - lock.unlock(); - } - } - - private boolean applyUpdatesToMesh( ChunkLocation location ) { - lock.lock(); - try { - final Collection< Location > locations = updateLocations.get( location ); - final ChunkMesh mesh = chunkMeshes.get( location ); - - if ( locations != null && !locations.isEmpty() && mesh != null ) { - final Collection< Location > removed = update( mesh, locations ); - locations.removeAll( removed ); - - if ( handler != null && !removed.isEmpty() ) { - handler.onMeshUpdate( location, mesh ); - } - - return locations.isEmpty(); - } - } finally { - lock.unlock(); - } - - return true; - } - - public static void generateSpareMesh( JavaPlugin plugin, World world, Collection< Vector > locations, Consumer< SparseMesh > callback ) { - if ( locations.isEmpty() ) { - return; - } - - boolean set = false; - Vector min = new Vector(); - Vector max = new Vector(); - - for ( Vector v : locations ) { - if ( !set ) { - set = true; - - min = v.toBlockVector(); - max = v.toBlockVector(); - } else { - min = Vector.getMinimum( min, v.toBlockVector() ); - max = Vector.getMaximum( max, v.toBlockVector() ); - } - } - - final SparseMesh sparseMesh = new SparseMesh( min, max ); - final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); - - { - final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { - Collection< Polygon > polygonCollection = polygons.get( p ); - if ( polygonCollection == null ) { - polygonCollection = new ArrayDeque< Polygon >(); - polygons.put( p, polygonCollection ); - } - return polygonCollection; - }; - - for ( final Vector location : locations ) { - final Block block = world.getBlockAt( location.getBlockX(), location.getBlockY(), location.getBlockZ() ); - - if ( !( block.isEmpty() && block.isLiquid() ) ) { - final BlockData data = block.getBlockData(); - final Location loc = block.getLocation(); - - // Start the box at 0, 0, 0 - final BoundingBox[] boundingBoxes = NmsUtil.getShape( data, loc, BlockShapeType.COLLISION_SHAPE ).e().stream().map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).toArray( BoundingBox[]::new ); - - final BlockDataInformation information = new BlockDataInformation( data ); - - if ( boundingBoxes.length == 0 ) { - // No bounding box - information.type = BlockDataType.NONE; - } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { - // Solid - information.type = BlockDataType.SOLID; - } else { - information.type = BlockDataType.COMPLEX; - // Complex bounding shape - - // Add the voxel shape to the chunk mesh - for ( final BoundingBox aabb : boundingBoxes ) { - final double minX = aabb.getMinX() + loc.getBlockX() - min.getBlockX(); - final double minY = aabb.getMinY() + loc.getBlockY() - min.getBlockY(); - final double minZ = aabb.getMinZ() + loc.getBlockZ() - min.getBlockZ(); - final double maxX = aabb.getMaxX() + loc.getBlockX() - min.getBlockX(); - final double maxY = aabb.getMaxY() + loc.getBlockY() - min.getBlockY(); - final double maxZ = aabb.getMaxZ() + loc.getBlockZ() - min.getBlockZ(); - - final Polygon xPoly = new Polygon( Arrays.asList( - new Point( minY, minZ ), - new Point( minY, maxZ ), - new Point( maxY, maxZ ), - new Point( maxY, minZ ) - ) ); - final Polygon yPoly = new Polygon( Arrays.asList( - new Point( minX, minZ ), - new Point( minX, maxZ ), - new Point( maxX, maxZ ), - new Point( maxX, minZ ) - ) ); - final Polygon zPoly = new Polygon( Arrays.asList( - new Point( minX, minY ), - new Point( minX, maxY ), - new Point( maxX, maxY ), - new Point( maxX, minY ) - ) ); - - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); - } - - information.boundingBoxes = boundingBoxes; - sparseMesh.setBlockDataInformationFor( information, loc.toVector().subtract( min ) ); - } - } - } - - // Now generate facets for each solid block based on if the side they are on is visible - for ( final Vector location : locations ) { - final Vector blockVector = location.toBlockVector().subtract( min ); - final BlockDataInformation info = sparseMesh.getBlockDataInformationFor( blockVector ); - if ( info != null ) { - if ( info.type == BlockDataType.SOLID ) { - final Polygon xPoly = new Polygon( Arrays.asList( - new Point( blockVector.getY(), blockVector.getZ() ), - new Point( blockVector.getY(), blockVector.getZ() + 1 ), - new Point( blockVector.getY() + 1, blockVector.getZ() + 1 ), - new Point( blockVector.getY() + 1, blockVector.getZ() ) - ) ); - final Polygon yPoly = new Polygon( Arrays.asList( - new Point( blockVector.getX(), blockVector.getZ() ), - new Point( blockVector.getX(), blockVector.getZ() + 1 ), - new Point( blockVector.getX() + 1, blockVector.getZ() + 1 ), - new Point( blockVector.getX() + 1, blockVector.getZ() ) - ) ); - final Polygon zPoly = new Polygon( Arrays.asList( - new Point( blockVector.getX(), blockVector.getY() ), - new Point( blockVector.getX(), blockVector.getY() + 1 ), - new Point( blockVector.getX() + 1, blockVector.getY() + 1 ), - new Point( blockVector.getX() + 1, blockVector.getY() ) - ) ); - - BlockDataInformation adjacent; - if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().subtract( new Vector( 0, 1, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, blockVector.getY() ) ).add( yPoly ); - } - - if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().add( new Vector( 0, 1, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, blockVector.getY() + 1 ) ).add( yPoly ); - } - - if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().subtract( new Vector( 0, 0, 1 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, blockVector.getZ() ) ).add( zPoly ); - } - - if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().add( new Vector( 0, 0, 1 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, blockVector.getZ() + 1 ) ).add( zPoly ); - } - - if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().subtract( new Vector( 1, 0, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, blockVector.getX() ) ).add( xPoly ); - } - - if ( ( adjacent = sparseMesh.getBlockDataInformationFor( blockVector.clone().add( new Vector( 1, 0, 0 ) ) ) ) == null || adjacent.type != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, blockVector.getX() + 1 ) ).add( xPoly ); - } - } - } else { - // Shouldn't be null btw - } - } - } - - Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> { - // Mesh the data and get the output - final MeshResult meshResult = generateMeshFrom( polygons ); - - sparseMesh.planes = meshResult.meshes; - sparseMesh.pointReferences = meshResult.points; - - callback.accept( sparseMesh ); - } ); - } - - // TODO Use ChunkSnapshot in the future - public static void generateMesh( JavaPlugin plugin, World world, final Chunk chunk, Consumer< ChunkMesh > callback ) { - final int worldMinHeight = world.getMinHeight(); - final int worldMaxHeight = world.getMaxHeight(); - final int worldHeight = worldMaxHeight - worldMinHeight; - - final ChunkMesh chunkMesh = new ChunkMesh( new BoundingBox( 0, 0, 0, 16, worldHeight, 16 ) ); - final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); - - { - final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { - Collection< Polygon > polygonCollection = polygons.get( p ); - if ( polygonCollection == null ) { - polygonCollection = new ArrayDeque< Polygon >(); - polygons.put( p, polygonCollection ); - } - return polygonCollection; - }; - - // When we would have inserted each facet, instead, find or create the - // corresponding plane, and add directly. - // Also, create a chunk mesh and populate the structs - for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { - final int relY = y - worldMinHeight; - final int yIndex = relY << 8; - for ( int z = 0; z < 16; ++z ) { - final int zIndex = yIndex + ( z << 4 ); - for ( int x = 0; x < 16; ++x ) { - final int index = zIndex + x; - final Block block = chunk.getBlock( x, y, z ); - if ( !( block.isEmpty() && block.isLiquid() ) ) { - final BlockData data = block.getBlockData(); - final Location loc = block.getLocation(); - - // Start the box at 0, 0, 0 - final BoundingBox[] boundingBoxes = NmsUtil.getShape( data, loc, BlockShapeType.COLLISION_SHAPE ).e().stream().map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).toArray( BoundingBox[]::new ); - - if ( boundingBoxes.length == 0 ) { - // No bounding box - chunkMesh.setType( index, BlockDataType.NONE ); - } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { - // Solid - chunkMesh.setType( index, BlockDataType.SOLID ); - } else { - chunkMesh.setType( index, BlockDataType.COMPLEX ); - // Complex bounding shape - - // Only store the old block data if we know it is not empty or solid!! - chunkMesh.setData( index, data ); - - // Add the voxel shape to the chunk mesh - for ( final BoundingBox aabb : boundingBoxes ) { - final double minX = aabb.getMinX() + x; - final double minY = aabb.getMinY() + relY; - final double minZ = aabb.getMinZ() + z; - final double maxX = aabb.getMaxX() + x; - final double maxY = aabb.getMaxY() + relY; - final double maxZ = aabb.getMaxZ() + z; - - final Polygon xPoly = new Polygon( Arrays.asList( - new Point( minY, minZ ), - new Point( minY, maxZ ), - new Point( maxY, maxZ ), - new Point( maxY, minZ ) - ) ); - final Polygon yPoly = new Polygon( Arrays.asList( - new Point( minX, minZ ), - new Point( minX, maxZ ), - new Point( maxX, maxZ ), - new Point( maxX, minZ ) - ) ); - final Polygon zPoly = new Polygon( Arrays.asList( - new Point( minX, minY ), - new Point( minX, maxY ), - new Point( maxX, maxY ), - new Point( maxX, minY ) - ) ); - - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); - } - - chunkMesh.setBoxes( index, boundingBoxes ); - } - } else { - chunkMesh.setType( index, BlockDataType.NONE ); - } - } - } - } - - // Now generate facets for each solid block based on if the side they are on is visible - for ( int y = 0; y < worldHeight; ++y ) { - final int yIndex = y << 8; - for ( int z = 0; z < 16; ++z ) { - final int zIndex = z << 4; - for ( int x = 0; x < 16; ++x ) { - final int index = yIndex + zIndex + x; - if ( chunkMesh.getType( index ) == BlockDataType.SOLID ) { - final Polygon xPoly = new Polygon( Arrays.asList( - new Point( y, z ), - new Point( y, z + 1 ), - new Point( y + 1, z + 1 ), - new Point( y + 1, z ) - ) ); - final Polygon yPoly = new Polygon( Arrays.asList( - new Point( x, z ), - new Point( x, z + 1 ), - new Point( x + 1, z + 1 ), - new Point( x + 1, z ) - ) ); - final Polygon zPoly = new Polygon( Arrays.asList( - new Point( x, y ), - new Point( x, y + 1 ), - new Point( x + 1, y + 1 ), - new Point( x + 1, y ) - ) ); - - // Check each face - if ( y == 0 || chunkMesh.getType( index - 256 ) != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, y ) ).add( yPoly ); - } - - if ( y == worldHeight - 1 || chunkMesh.getType( index + 256 ) != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, y + 1 ) ).add( yPoly ); - } - - if ( z == 0 || chunkMesh.getType( index - 16 ) != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, z ) ).add( zPoly ); - } - - if ( z == 15 || chunkMesh.getType( index + 16 ) != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, z + 1 ) ).add( zPoly ); - } - - if ( x == 0 || chunkMesh.getType( index - 1 ) != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, x ) ).add( xPoly ); - } - - if ( x == 15 || chunkMesh.getType( index + 1 ) != BlockDataType.SOLID ) { - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, x + 1 ) ).add( xPoly ); - } - } - } - } - } - } - - Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> { - // Mesh the data and get the output - final MeshResult meshResult = generateMeshFrom( polygons ); - - chunkMesh.planes = meshResult.meshes; - chunkMesh.pointReferences = meshResult.points; - - callback.accept( chunkMesh ); - } ); - } - - public static Collection< Location > update( final AbstractMesh mesh, final Collection< Location > locations ) { - if ( locations.isEmpty() ) { - return Collections.emptySet(); - } - - final Collection< Location > canRemove = new ArrayDeque< Location >(); - // Check each location - final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); - final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { - Collection< Polygon > polygonCollection = polygons.get( p ); - if ( polygonCollection == null ) { - polygonCollection = new ArrayDeque< Polygon >(); - polygons.put( p, polygonCollection ); - } - return polygonCollection; - }; - - final BiFunction< List< BoundingBox >, BoundingBox[], Boolean > isEqual = ( a, b ) -> { - if ( a.size() == b.length ) { - for ( int i = 0; i < b.length; ++i ) { - final BoundingBox b1 = a.get( i ); - final BoundingBox b2 = b[ i ]; - - if ( !b1.equals( b2 ) ) { - return false; - } - } - } - - return true; - }; - - for ( final Location location : locations ) { - final BlockData data = location.getBlock().getBlockData(); - - // Defer moving piston updates until another tick when it's done moving - if ( data.getMaterial() == Material.MOVING_PISTON ) { - continue; - } - - final Vector position = location.toVector().subtract( mesh.getMinimumCorner() ); - final BlockDataInformation information = mesh.getBlockDataInformationFor( position ); - if ( information != null ) { - canRemove.add( location ); - if ( information.isBlockData( data ) ) { - // Ignore if the block data has not changed - continue; - } else { - final List< BoundingBox > boxes = NmsUtil.getShape( data, location, BlockShapeType.COLLISION_SHAPE ).e().stream() - .map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).collect( Collectors.toList() ); - final BlockDataType oldType = information.type; - final BoundingBox[] otherBoxes = information.boundingBoxes; - if ( boxes.size() == 0 ) { - if ( oldType == BlockDataType.NONE ) { - continue; - } - - BlockDataInformation newInfo = new BlockDataInformation( data ); - newInfo.type = BlockDataType.NONE; - newInfo.boundingBoxes = null; - - mesh.setBlockDataInformationFor( newInfo, position ); - } else if ( boxes.size() == 1 && boxes.get( 0 ).getVolume() == 1 ) { - if ( oldType == BlockDataType.SOLID ) { - continue; - } - - BlockDataInformation newInfo = new BlockDataInformation( data ); - newInfo.type = BlockDataType.SOLID; - newInfo.boundingBoxes = null; - - mesh.setBlockDataInformationFor( newInfo, position ); - } else { - if ( otherBoxes != null && isEqual.apply( boxes, otherBoxes ) ) { - continue; - } - - BlockDataInformation newInfo = new BlockDataInformation( data ); - newInfo.type = BlockDataType.COMPLEX; - newInfo.boundingBoxes = boxes.toArray( BoundingBox[]::new ); - - mesh.setBlockDataInformationFor( newInfo, position ); - } - - if ( oldType == BlockDataType.SOLID ) { - boxes.add( new BoundingBox( 0, 0, 0, 1, 1, 1 ) ); - } else if ( oldType == BlockDataType.COMPLEX ) { - if ( otherBoxes == null || otherBoxes.length == 0 ) { - throw new IllegalStateException( "Complex shape but no bounding boxes!" ); - } - - for ( final BoundingBox box : otherBoxes ) { - boxes.add( box ); - } - } - - // Add the voxel shape to the chunk mesh - for ( final BoundingBox aabb : boxes ) { - final double minX = aabb.getMinX() + position.getX(); - final double minY = aabb.getMinY() + position.getY(); - final double minZ = aabb.getMinZ() + position.getZ(); - final double maxX = aabb.getMaxX() + position.getX(); - final double maxY = aabb.getMaxY() + position.getY(); - final double maxZ = aabb.getMaxZ() + position.getZ(); - - final Polygon xPoly = new Polygon( Arrays.asList( - new Point( minY, minZ ), - new Point( minY, maxZ ), - new Point( maxY, maxZ ), - new Point( maxY, minZ ) - ) ); - final Polygon yPoly = new Polygon( Arrays.asList( - new Point( minX, minZ ), - new Point( minX, maxZ ), - new Point( maxX, maxZ ), - new Point( maxX, minZ ) - ) ); - final Polygon zPoly = new Polygon( Arrays.asList( - new Point( minX, minY ), - new Point( minX, maxY ), - new Point( maxX, maxY ), - new Point( maxX, minY ) - ) ); - - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); - getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); - } - } - } - } - - // No polygons found... which means nothing changed - if ( polygons.isEmpty() ) { - return canRemove; - } - - final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >(); - - // Now mesh the blocks asynchronously and combine, then do the bare minimum to regenerate the chunk mesh - for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) { - final MinecraftPlane plane = entry.getKey(); - final PlaneMesh planeMesh = mesh.planes.remove( plane ); - final Mesh< RegionSimple > mesher = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); - - if ( planeMesh != null ) { - // Reset all indexed polygons(triangles) for this plane - for ( final IndexedTriangle poly : planeMesh.completedPolygons ) { - poly.getPoints().parallelStream().forEach( p -> --p.referenceCount ); - } - - // Add all polygons that were previously generated - for ( final Polygon polygon : planeMesh.generatedRegions ) { - mesher.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); - } - } - for ( final Polygon polygon : entry.getValue() ) { - mesher.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); - } - - meshMap.put( plane, mesher ); - } - - // Mesh each plane - final Map< MinecraftPlane, CompleteMesh > triangles = meshMap.entrySet().parallelStream().collect( Collectors.toConcurrentMap( e -> e.getKey(), e -> new CompleteMesh( e.getKey(), e.getValue() ) ) ); - - for ( final Entry< MinecraftPlane, CompleteMesh > entry : triangles.entrySet() ) { - final MinecraftPlane plane = entry.getKey(); - final CompleteMesh completeMesh = entry.getValue(); - final Collection< Polygon > tris = completeMesh.triangles; - final Mesh< RegionSimple > mesher = completeMesh.mesh; - - // Ignore any planes that are empty - if ( !tris.isEmpty() ) { - final PlaneMesh newMesh = new PlaneMesh(); - newMesh.completedPolygons = new ArrayList< IndexedTriangle >(); - for ( final Polygon tri : tris ) { - final IndexedTriangle newPoly = new IndexedTriangle(); - for ( int i = 0; i < tri.getPoints().size() && i < 3; ++i ) { - final Point point = tri.getPoints().get( i ); - final Vector vert = plane.convert( point ); - - // Find an existing vertex reference if one exists - final VectorReference tempRef = new VectorReference( vert ); - - VectorReference ref = null; - final VectorReference upperRef = mesh.pointReferences.ceiling( tempRef ); - if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { - ref = upperRef; - } else { - final VectorReference lowerRef = mesh.pointReferences.floor( tempRef ); - if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { - ref = lowerRef; - } - } - - if ( ref == null ) { - ref = tempRef; - ref.referenceCount = 1; - mesh.pointReferences.add( ref ); - } else { - ++ref.referenceCount; - } - - switch ( i ) { - case 0: - newPoly.setPoint1( ref ); - break; - case 1: - newPoly.setPoint2( ref ); - break; - case 2: - newPoly.setPoint3( ref ); - break; - default: - // Should never be here! - } - } - newMesh.completedPolygons.add( newPoly ); - } - - // Save the generated regions as an intermediary step so we can re-use them later - newMesh.generatedRegions = mesher.getPolygons(); - - mesh.planes.put( plane, newMesh ); - } - } - - // Remove all vector references that have a reference count of 0 - mesh.pointReferences.removeIf( v -> v.referenceCount <= 0 ); - - return canRemove; - } - - public static MeshResult generateMeshFrom( final Map< MinecraftPlane, Collection< Polygon > > polygons ) { - final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >(); - for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) { - final MinecraftPlane plane = entry.getKey(); - final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); - for ( final Polygon polygon : entry.getValue() ) { - mesh.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); - } - meshMap.put( plane, mesh ); - } - - // Mesh each plane - final Map< MinecraftPlane, CompleteMesh > triangles = meshMap.entrySet().parallelStream().collect( Collectors.toConcurrentMap( e -> e.getKey(), e -> new CompleteMesh( e.getKey(), e.getValue() ) ) ); - - // Construct a series of indexed polygons - final TreeSet< VectorReference > vertices = new TreeSet< VectorReference >(); - - final MeshResult result = new MeshResult(); - for ( final Entry< MinecraftPlane, CompleteMesh > entry : triangles.entrySet() ) { - final MinecraftPlane plane = entry.getKey(); - final CompleteMesh completeMesh = entry.getValue(); - final Collection< Polygon > tris = completeMesh.triangles; - final Mesh< RegionSimple > mesh = completeMesh.mesh; - - // Ignore any planes that are empty - if ( !tris.isEmpty() ) { - final PlaneMesh newMesh = new PlaneMesh(); - newMesh.completedPolygons = new ArrayList< IndexedTriangle >(); - for ( final Polygon tri : tris ) { - final IndexedTriangle newPoly = new IndexedTriangle(); - for ( int i = 0; i < tri.getPoints().size() && i < 3; ++i ) { - final Point point = tri.getPoints().get( i ); - final Vector vert = plane.convert( point ); - - final VectorReference tempRef = new VectorReference( vert ); - // Find an existing vertex reference if one exists - VectorReference ref = null; - if ( ref == null ) { - final VectorReference upperRef = vertices.ceiling( tempRef ); - if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { - ref = upperRef; - } else { - final VectorReference lowerRef = vertices.floor( tempRef ); - if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { - ref = lowerRef; - } - } - } - - if ( ref == null ) { - ref = tempRef; - ref.referenceCount = 1; - vertices.add( ref ); - } else { - ++ref.referenceCount; - } - - switch ( i ) { - case 0: - newPoly.setPoint1( ref ); - break; - case 1: - newPoly.setPoint2( ref ); - break; - case 2: - newPoly.setPoint3( ref ); - break; - default: - // Should never be here! - } - - } - newMesh.completedPolygons.add( newPoly ); - } - - // Save the generated regions as an intermediary step so we can re-use them later - newMesh.generatedRegions = mesh.getPolygons(); - - result.meshes.put( plane, newMesh ); - } - } - - // Add all vector references - result.points = vertices; - - return result; - } - - private static class CompleteMesh { - Mesh< RegionSimple > mesh; - Collection< Polygon > triangles; - - CompleteMesh( final MinecraftPlane plane, final Mesh< RegionSimple > mesh ) { - this.mesh = mesh; - triangles = mesh.meshify(); - } - } - - public static class MeshResult { - Map< MinecraftPlane, PlaneMesh > meshes; - TreeSet< VectorReference > points; - } - - private enum ChunkTaskType { - LOAD, UNLOAD - } - - private class ChunkTask { - BukkitTask task; - ChunkTaskType type; - } -} diff --git a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/util/NmsUtil.java b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/util/NmsUtil.java index b0bea4b..a772dcb 100644 --- a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/util/NmsUtil.java +++ b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/util/NmsUtil.java @@ -2,8 +2,8 @@ package com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util; import org.bukkit.Location; import org.bukkit.block.data.BlockData; -import org.bukkit.craftbukkit.v1_21_R6.CraftWorld; -import org.bukkit.craftbukkit.v1_21_R6.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_21_R7.CraftWorld; +import org.bukkit.craftbukkit.v1_21_R7.block.data.CraftBlockData; import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; diff --git a/plugin/environment/src/main/resources/plugin.yml b/plugin/environment/src/main/resources/plugin.yml index b5fa074..2af919c 100644 --- a/plugin/environment/src/main/resources/plugin.yml +++ b/plugin/environment/src/main/resources/plugin.yml @@ -1,6 +1,7 @@ -name: MC-Mesh-Environment +name: Mc-Mesh-Environment main: com.aaaaahhhhhhh.bananapuncher714.mesh.environment.McMeshEnvironmentPlugin version: 0.0.1 description: BulletJME and MC-Mesh integration author: BananaPuncher714 -api-version: 1.13 \ No newline at end of file +api-version: 1.13 +load: STARTUP \ No newline at end of file diff --git a/plugin/physics/pom.xml b/plugin/physics/pom.xml index 1ba62d7..6d9646e 100644 --- a/plugin/physics/pom.xml +++ b/plugin/physics/pom.xml @@ -14,7 +14,7 @@ org.spigotmc spigot - 1.21.10-R0.1-SNAPSHOT + 1.21.11-R0.1-SNAPSHOT provided @@ -27,19 +27,6 @@ - - org.apache.maven.plugins - maven-shade-plugin - 3.1.0 - - - package - - shade - - - - org.apache.maven.plugins maven-compiler-plugin diff --git a/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/McMeshPhysicsPlugin.java b/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/McMeshPhysicsPlugin.java index 693fbb3..6702aaf 100644 --- a/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/McMeshPhysicsPlugin.java +++ b/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/McMeshPhysicsPlugin.java @@ -1,6 +1,5 @@ package com.aaaaahhhhhhh.bananapuncher714.mesh.physics; -import java.io.File; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -30,7 +29,7 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.InputValidatorDo import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.InputValidatorInt; import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.sender.SenderValidatorPlayer; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.EnvironmentHandler; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.MeshEnvironment; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.McMeshEnvironmentPlugin; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkLocation; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkMesh; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil; @@ -52,15 +51,12 @@ import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; public class McMeshPhysicsPlugin extends JavaPlugin { - private static final int TIME_STEPS = 5; + private static final int TIME_STEPS = 10; private static final float SIMULATION_SPEED = 1f; - private MeshEnvironment environment; - private PhysicsSpace space; private boolean paused = false; private boolean teleportOnDeactivate = false; - private float tick = 0; private Map< PhysicsCollisionObject, AuxiliaryDisplayStruct > linkedDisplays = new HashMap< PhysicsCollisionObject, AuxiliaryDisplayStruct >(); private Set< PhysicsCollisionObject > active = new HashSet< PhysicsCollisionObject >(); private Map< ChunkLocation, PhysicsRigidBody > chunks = new HashMap< ChunkLocation, PhysicsRigidBody >(); @@ -72,10 +68,13 @@ public class McMeshPhysicsPlugin extends JavaPlugin { space = new PhysicsSpace( BroadphaseType.DBVT ); space.setAccuracy( 1f / ( TIME_STEPS * 20f ) ); - environment = new MeshEnvironment( this, new File( getDataFolder(), "environment" ) ); - environment.setHandler( new EnvironmentHandler() { + JavaPlugin.getPlugin( McMeshEnvironmentPlugin.class ).registerHandler( new EnvironmentHandler() { @Override public void onMeshLoad( ChunkLocation location, ChunkMesh mesh ) { + if ( !location.getWorld().getName().equalsIgnoreCase( "world" ) ) { + return; + } + // Now, create a physics object from the chunk mesh final IndexedMesh nativeMesh = mesh.toIndexedMesh(); // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... @@ -99,12 +98,20 @@ public class McMeshPhysicsPlugin extends JavaPlugin { @Override public void onMeshUnload( ChunkLocation location, ChunkMesh mesh ) { - space.removeCollisionObject( chunks.get( location ) ); + if ( !location.getWorld().getName().equalsIgnoreCase( "world" ) ) { + return; + } + + space.removeCollisionObject( chunks.remove( location ) ); } @Override public void onMeshUpdate( ChunkLocation location, ChunkMesh mesh ) { - // Now, create a physics object from the chunk mesh + if ( !location.getWorld().getName().equalsIgnoreCase( "world" ) ) { + return; + } + + // Now, create a physics object from the chunk mesh final IndexedMesh nativeMesh = mesh.toIndexedMesh(); // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... final MeshCollisionShape meshCollision = new MeshCollisionShape( true, nativeMesh ); @@ -132,7 +139,7 @@ public class McMeshPhysicsPlugin extends JavaPlugin { if ( !paused ) { // Update the real world with the physics space objects space.distributeEvents(); - space.update( tick, TIME_STEPS ); + space.update( SIMULATION_SPEED / 20f, TIME_STEPS ); for ( Iterator< Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > > it = linkedDisplays.entrySet().iterator(); it.hasNext(); ) { Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > entry = it.next(); @@ -187,8 +194,6 @@ public class McMeshPhysicsPlugin extends JavaPlugin { disp.display.setInterpolationDuration( 2 ); disp.display.setTransformationMatrix( transMat.mul( transformRot ) ); } - - tick += SIMULATION_SPEED / 20f; } }, 0, 1 ); } diff --git a/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/MiniePlugin.java b/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/MiniePlugin.java index 55b4d0a..bd75787 100644 --- a/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/MiniePlugin.java +++ b/plugin/physics/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/physics/MiniePlugin.java @@ -35,8 +35,8 @@ import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.data.BlockData; import org.bukkit.command.CommandSender; -import org.bukkit.craftbukkit.v1_21_R6.CraftWorld; -import org.bukkit.craftbukkit.v1_21_R6.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_21_R7.CraftWorld; +import org.bukkit.craftbukkit.v1_21_R7.block.data.CraftBlockData; import org.bukkit.entity.BlockDisplay; import org.bukkit.entity.Display; import org.bukkit.entity.Player; diff --git a/plugin/physics/src/main/resources/plugin.yml b/plugin/physics/src/main/resources/plugin.yml index 255eac4..fb2dad5 100644 --- a/plugin/physics/src/main/resources/plugin.yml +++ b/plugin/physics/src/main/resources/plugin.yml @@ -5,6 +5,7 @@ description: BulletJME and MC-Mesh integration author: BananaPuncher714 api-version: 1.13 depend: [ "Mc-Mesh-Environment" ] +load: STARTUP #libraries: #- com.github.stephengold:Minie:7.5.0