diff --git a/.gitignore b/.gitignore index d5c8323..34d0030 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .project target .settings -data \ No newline at end of file +data +bin \ No newline at end of file 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 index 3dc561d..2e69eee 100644 --- 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 @@ -1,5 +1,46 @@ package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; -public class ChunkMeshCache { +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/MeshEnvironment.java b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/MeshEnvironment.java index 8bb0c90..6bf6c31 100644 --- 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 @@ -1,9 +1,6 @@ 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; @@ -16,7 +13,9 @@ 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; @@ -50,78 +49,139 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataInfor 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.IndexedPolygon; 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.NmsUtil; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil.BlockShapeType; -import com.jme3.bullet.collision.shapes.MeshCollisionShape; -import com.jme3.bullet.collision.shapes.infos.IndexedMesh; -import com.jme3.bullet.objects.PhysicsBody; -import com.jme3.bullet.objects.PhysicsRigidBody; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; public class MeshEnvironment { private JavaPlugin plugin; private File cacheDirectory; - private Map< ChunkLocation, ChunkMesh > chunkMeshes; + 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 ReentrantLock lock = new ReentrantLock(); + + final BukkitTask task = Bukkit.getScheduler().runTaskTimer( plugin, () -> { +// final Collection< ChunkLocation > locations = new ArrayDeque< ChunkLocation >(); + 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 ( !locs.isEmpty() ) { +// locations.add( e.getKey() ); +// } + } + } ); + +// if ( !locations.isEmpty() ) { +// space.activateAll( true ); +// } + + 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 ) { - // TODO Queue chunk load - loadOrGenerateMesh( event.getChunk() ); + 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 ) { - // TODO Queue chunk unload - if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { - final ChunkLocation loc = new ChunkLocation( event.getChunk() ); - if ( chunks.containsKey( loc ) ) { - space.removeCollisionObject( chunks.get( loc ) ); - chunks.remove( loc ); + 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 + } } - - final ChunkMesh mesh = chunkMeshes.remove( loc ); - if ( mesh != null ) { - saveMesh( loc, mesh ); - } - } - } - - final Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >(); - final BukkitTask task = Bukkit.getScheduler().runTaskTimer( plugin, () -> { - final Collection< ChunkLocation > locations = new ArrayDeque< ChunkLocation >(); - updateLocations.entrySet().stream().forEach( e -> { - final Collection< Location > locs = update( e.getValue() ); - e.getValue().removeAll( locs ); - if ( !locs.isEmpty() ) { - locations.add( e.getKey() ); - } - } ); - - if ( !locations.isEmpty() ) { - space.activateAll( true ); + } finally { + lock.unlock(); } - updateLocations.values().removeIf( c -> c.isEmpty() ); - }, 1, 1 ); - final Function< ChunkLocation, Collection< Location > > getLocationFor = c -> { - Collection< Location > locations = updateLocations.get( c ); - if ( locations == null ) { - locations = new HashSet< Location >(); - updateLocations.put( c, locations ); - } - return locations; - }; + /* + * 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 ) { @@ -133,6 +193,7 @@ public class MeshEnvironment { public MeshEnvironment( JavaPlugin plugin, File cacheDirectory ) { this.plugin = plugin; this.cacheDirectory = cacheDirectory; + cache = new ChunkMeshCache( new File( cacheDirectory, "meshes" ) ); } public void activate() { @@ -143,52 +204,136 @@ public class MeshEnvironment { HandlerList.unregisterAll( eventListener ); } - private void loadOrGenerateMesh( final Chunk chunk ) { - final ChunkLocation location = new ChunkLocation( chunk ); - - final ChunkMesh mesh = loadMesh( location ); - if ( mesh == null ) { - generateMesh( chunk ); - } else { - // Save our chunk mesh - chunkMeshes.put( location, mesh ); - } - } - - private ChunkMesh loadMesh( final ChunkLocation location ) { - final File file = new File( cacheDirectory, "meshes/" + 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(); + private void completeTask( ChunkLocation location ) { + lock.lock(); + try { + final ChunkTask task = tasks.remove( location ); + if ( task.type == ChunkTaskType.LOAD ) { + 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 { + chunkMeshes.remove( location ); + } + } 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(); } - return null; - } - - private void saveMesh( final ChunkLocation location, final ChunkMesh mesh ) { - final File file = new File( cacheDirectory, "meshes/" + 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 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 ) { + locations.removeAll( update( mesh, locations ) ); + + return locations.isEmpty(); + } + } finally { + lock.unlock(); + } + + return true; + } + + private static void generateSpareMesh( JavaPlugin plugin, World world, Collection< Vector > locations, Consumer< SparseMesh > callback ) { + if ( locations.isEmpty() ) { + return; } - } - - private SparseMesh generateSpareMesh( World world, Collection< Vector > locations ) { - final long start = System.currentTimeMillis(); - + boolean set = false; - Vector min; - Vector max; + Vector min = new Vector(); + Vector max = new Vector(); for ( Vector v : locations ) { if ( !set ) { @@ -335,22 +480,19 @@ public class MeshEnvironment { } } - // Mesh the data and get the output - final MeshResult meshResult = generateMeshFrom( polygons ); - - sparseMesh.planes = meshResult.meshes; - sparseMesh.pointReferences = meshResult.points; - - // DONE - - return sparseMesh; + 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 ); + } ); } - private void generateMesh( final Chunk chunk ) { - final long start = System.currentTimeMillis(); - final World world = chunk.getWorld(); - - final ChunkLocation chunkLocation = new ChunkLocation( chunk ); + // TODO Use ChunkSnapshot in the future + private 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; @@ -508,44 +650,15 @@ public class MeshEnvironment { chunkMesh.planes = meshResult.meshes; chunkMesh.pointReferences = meshResult.points; - // DONE - - // Now, create a physics object from the chunk mesh - final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh(); - // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... - final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); - - // Create a rigid body - PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); - rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, world.getMinHeight(), chunk.getZ() << 4 ) ); - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - rigid.setKinematic( false ); - - Bukkit.getScheduler().runTask( plugin, () -> { - // Save our chunk mesh - chunkMeshes.put( chunkLocation, chunkMesh ); - - space.addCollisionObject( rigid ); - - PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); - if ( old != null ) { - getLogger().warning( "Old rigid body found!" ); - space.remove( old ); - } - - final long time = System.currentTimeMillis() - start; -// getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles" ); - } ); + callback.accept( chunkMesh ); } ); } - private Collection< Location > update( final AbstractMesh mesh, final Collection< Location > locations ) { + private static Collection< Location > update( final AbstractMesh mesh, final Collection< Location > locations ) { if ( locations.isEmpty() ) { return Collections.emptySet(); } - final World world = locations.iterator().next().getWorld(); - final Collection< Location > canRemove = new ArrayDeque< Location >(); // Check each location final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); @@ -775,23 +888,10 @@ public class MeshEnvironment { // Remove all vector references that have a reference count of 0 mesh.pointReferences.removeIf( v -> v.referenceCount <= 0 ); - // DONE - - // Now, create a physics object from the chunk mesh - final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh(); - // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... - final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); - - // Create a rigid body - PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); - rigid.setPhysicsLocation( new Vector3f( chunkLocation.getX() << 4, world.getMinHeight(), chunkLocation.getZ() << 4 ) ); - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - rigid.setKinematic( false ); - return canRemove; } - private MeshResult generateMeshFrom( final Map< MinecraftPlane, Collection< Polygon > > polygons ) { + private 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(); @@ -875,9 +975,11 @@ public class MeshEnvironment { // Add all vector references result.points = vertices; + + return result; } - private class CompleteMesh { + private static class CompleteMesh { Mesh< RegionSimple > mesh; Collection< Polygon > triangles; @@ -887,8 +989,17 @@ public class MeshEnvironment { } } - private class MeshResult { + private 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/MiniePlugin.java b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/MiniePlugin.java index c4a7b38..9f6a4a6 100644 --- a/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/MiniePlugin.java +++ b/plugin/environment/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/environment/MiniePlugin.java @@ -1,2045 +1,2045 @@ -package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; - -import java.io.File; -import java.io.FileWriter; -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.Iterator; -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.function.BiFunction; -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.ChunkSnapshot; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -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.entity.BlockDisplay; -import org.bukkit.entity.Display; -import org.bukkit.entity.Player; -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.inventory.ItemStack; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitTask; -import org.bukkit.util.BoundingBox; -import org.bukkit.util.Transformation; -import org.bukkit.util.Vector; -import org.joml.Matrix4f; -import org.joml.Quaternionf; - -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.base.Facet; -import com.aaaaahhhhhhh.bananapuncher714.mesh.base.MeshBuilder; -import com.aaaaahhhhhhh.bananapuncher714.mesh.base.Plane; -import com.aaaaahhhhhhh.bananapuncher714.mesh.base.Vector3d; -import com.aaaaahhhhhhh.bananapuncher714.mesh.command.CommandParameters; -import com.aaaaahhhhhhh.bananapuncher714.mesh.command.SubCommand; -import com.aaaaahhhhhhh.bananapuncher714.mesh.command.executor.CommandExecutableMessage; -import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.InputValidatorDouble; -import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.InputValidatorInt; -import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.sender.SenderValidatorPlayer; -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.IndexedPolygon; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.PlaneMesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.VectorReference; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane.PlaneAxis; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.ChunkMeshUtil; -import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.FileUtil; -import com.jme3.bullet.PhysicsSpace; -import com.jme3.bullet.PhysicsSpace.BroadphaseType; -import com.jme3.bullet.RotationOrder; -import com.jme3.bullet.collision.ContactListener; -import com.jme3.bullet.collision.PhysicsCollisionEvent; -import com.jme3.bullet.collision.PhysicsCollisionListener; -import com.jme3.bullet.collision.PhysicsCollisionObject; -import com.jme3.bullet.collision.shapes.BoxCollisionShape; -import com.jme3.bullet.collision.shapes.CollisionShape; -import com.jme3.bullet.collision.shapes.CompoundCollisionShape; -import com.jme3.bullet.collision.shapes.MeshCollisionShape; -import com.jme3.bullet.collision.shapes.PlaneCollisionShape; -import com.jme3.bullet.collision.shapes.SphereCollisionShape; -import com.jme3.bullet.collision.shapes.infos.IndexedMesh; -import com.jme3.bullet.joints.New6Dof; -import com.jme3.bullet.joints.PhysicsJoint; -import com.jme3.bullet.joints.motors.MotorParam; -import com.jme3.bullet.objects.PhysicsBody; -import com.jme3.bullet.objects.PhysicsRigidBody; -import com.jme3.math.Matrix3f; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; - -import net.minecraft.core.BlockPosition; -import net.minecraft.server.level.WorldServer; -import net.minecraft.world.phys.AxisAlignedBB; -import net.minecraft.world.phys.shapes.VoxelShape; -import net.minecraft.world.phys.shapes.VoxelShapeCollision; - -public class MiniePlugin extends JavaPlugin { - private static final int TIME_STEPS = 5; - private static final float SIMULATION_SPEED = 1f; - - 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 PhysicsRigidBody planeBody; - - private Set< PhysicsCollisionObject > active = new HashSet< PhysicsCollisionObject >(); - private Set< PhysicsCollisionObject > isTnt = new HashSet< PhysicsCollisionObject >(); - - private Map< PhysicsJoint, JointStruct > joints = new HashMap< PhysicsJoint, JointStruct >(); - - private Map< ChunkLocation, PhysicsRigidBody > chunks = new HashMap< ChunkLocation, PhysicsRigidBody >(); - - private Map< Vector3f, CollisionShape > collisionShapes = new HashMap< Vector3f, CollisionShape >(); - - private Map< ChunkLocation, ChunkMesh > chunkMeshes = new HashMap< ChunkLocation, ChunkMesh >(); - - @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/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" ); - Bukkit.getPluginManager().disablePlugin( this ); - return; - } - - registerCommands(); - - // Don't log anything - PhysicsSpace.logger.setLevel( Level.WARNING ); - PhysicsSpace.loggerC.setLevel( Level.WARNING ); - PhysicsRigidBody.logger2.setLevel( Level.WARNING ); - - space = new PhysicsSpace( BroadphaseType.DBVT ); - PlaneCollisionShape plane = new PlaneCollisionShape( new com.jme3.math.Plane( new Vector3f( 0, 1, 0 ), 0 ) ); - planeBody = new PhysicsRigidBody( plane, PhysicsBody.massForStatic ); - planeBody.setPhysicsLocation( new Vector3f( 0, 128, 0 ) ); -// space.addCollisionObject( planeBody ); - space.setAccuracy( 1f / ( TIME_STEPS * 20f ) ); - - space.addContactListener( new ContactListener() { - @Override - public void onContactEnded( long manifoldId ) { - - } - - @Override - public void onContactProcessed( PhysicsCollisionObject objA, PhysicsCollisionObject objB, long manifoldPointId ) { - if ( isTnt.remove( objA ) ) { - impulse( objA.getPhysicsLocation(), 30f ); - if ( linkedDisplays.containsKey( objA ) ) { - linkedDisplays.get( objA ).display.remove(); - } - } - - if ( isTnt.remove( objB ) ) { - impulse( objB.getPhysicsLocation(), 30f ); - if ( linkedDisplays.containsKey( objB ) ) { - linkedDisplays.get( objB ).display.remove(); - } - } - } - - @Override - public void onContactStarted( long manifoldId ) { - - } - } ); - - Bukkit.getPluginManager().registerEvents( new Listener() { - @EventHandler - private void onChunkLoad( ChunkLoadEvent event ) { - if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { -// getLogger().info( "Loading chunk " + event.getChunk().getX() + ", " + event.getChunk().getZ() ); -// generateMesh( event.getChunk() ); -// scanAndGenerate( event.getChunk() ); -// saveFacets( event.getChunk() ); - loadOrGenerateMesh( event.getChunk() ); - } - } - - @EventHandler - private void onChunkUnload( ChunkUnloadEvent event ) { - if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { - final ChunkLocation loc = new ChunkLocation( event.getChunk() ); - if ( chunks.containsKey( loc ) ) { - space.removeCollisionObject( chunks.get( loc ) ); - chunks.remove( loc ); - } - - final ChunkMesh mesh = chunkMeshes.remove( loc ); - if ( mesh != null ) { - saveMesh( loc, mesh ); - } - } - } - - final Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >(); - final BukkitTask task = Bukkit.getScheduler().runTaskTimer( MiniePlugin.this, () -> { - final Collection< ChunkLocation > locations = new ArrayDeque< ChunkLocation >(); - updateLocations.entrySet().stream().forEach( e -> { - final Collection< Location > locs = update( e.getValue() ); - e.getValue().removeAll( locs ); - if ( !locs.isEmpty() ) { - locations.add( e.getKey() ); - } - } ); - - if ( !locations.isEmpty() ) { - space.activateAll( true ); - } - - updateLocations.values().removeIf( c -> c.isEmpty() ); - }, 1, 1 ); - final Function< ChunkLocation, Collection< Location > > getLocationFor = c -> { - Collection< Location > locations = updateLocations.get( c ); - if ( locations == null ) { - locations = new HashSet< Location >(); - updateLocations.put( c, locations ); - } - return locations; - }; - - @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() ); - } - }, this ); - - - getLogger().info( "There are " + Bukkit.getWorld( "world" ).getLoadedChunks().length + " chunks" ); -// getLogger().info( "Converted " + saveAllChunks( Bukkit.getWorld( "world" ) ) ); - - loadOrGenerateWorld( Bukkit.getWorld( "world" ) ); -// scanAndGenerate( Bukkit.getWorld( "world" ) ); -// saveAllFacets( Bukkit.getWorld( "world" ) ); - - Bukkit.getScheduler().runTaskTimer( this, () -> { - if ( !paused ) { - // Update the real world with the physics space objects - space.distributeEvents(); - space.update( tick, TIME_STEPS ); - - for ( Iterator< Entry< PhysicsJoint, JointStruct > > it = joints.entrySet().iterator(); it.hasNext(); ) { - Entry< PhysicsJoint, JointStruct > entry = it.next(); - PhysicsJoint joint = entry.getKey(); - JointStruct struct = entry.getValue(); - BlockState state = struct.state; - - if ( state.getBlock().getType() != state.getType() || !state.getChunk().isLoaded() ) { - space.removeJoint( joint ); - space.remove( struct.object ); - it.remove(); - } else if ( !joint.isEnabled() ) { - Location loc = state.getLocation(); - BlockData displayData = state.getBlockData(); - - // Create the display - BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { - d.setBlock( displayData ); - } ); - - linkedDisplays.put( struct.object, new AuxiliaryDisplayStruct( display, struct.offset ) ); - - state.getBlock().setType( Material.AIR ); - - struct.object.setProtectGravity( false ); - Vector3f grav = new Vector3f(); - space.setGravity( grav ); - struct.object.setGravity( grav ); - - space.removeJoint( joint ); - it.remove(); - } - } - - for ( Iterator< Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > > it = linkedDisplays.entrySet().iterator(); it.hasNext(); ) { - Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > entry = it.next(); - PhysicsCollisionObject obj = entry.getKey(); - AuxiliaryDisplayStruct disp = entry.getValue(); - - if ( !disp.display.isValid() ) { - it.remove(); - space.removeCollisionObject( obj ); - active.remove( obj ); - isTnt.remove( obj ); - - continue; - } - - Location displayLocation = disp.display.getLocation(); - - // Do not calculate for this object if it is inactive - if ( !obj.isActive() ) { - if ( teleportOnDeactivate && active.remove( obj ) ) { - // Newly deactivated object, teleport the display to the actual position to prevent the translation from getting too large - - Transformation currentTransformation = disp.display.getTransformation(); - org.joml.Vector3f trans = currentTransformation.getTranslation(); - - displayLocation.add( trans.x(), trans.y(), trans.z() ); - disp.display.teleport( displayLocation ); - - Transformation newTrans = new Transformation( new org.joml.Vector3f( 0, 0, 0 ), currentTransformation.getLeftRotation(), currentTransformation.getScale(), currentTransformation.getRightRotation() ); - disp.display.setInterpolationDelay( 0 ); - disp.display.setInterpolationDuration( 0 ); - disp.display.setTransformation( newTrans ); - } - - continue; - } else { - active.add( obj ); - } - - Vector3f location = obj.getPhysicsLocation(); - Quaternion rotation = new Quaternion(); - obj.getPhysicsRotation( rotation ); - - Vector offsetVec = disp.offset; - Vector scale = disp.scale; - Matrix4f transformRot = new Matrix4f(); - transformRot.set( new Quaternionf( rotation.getX(), rotation.getY(), rotation.getZ(), rotation.getW() ) ); - - transformRot.mul( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, - ( float ) offsetVec.getX(), - ( float ) offsetVec.getY(), - ( float ) offsetVec.getZ(), 1 ); - transformRot.mul( ( float ) scale.getX(), 0, 0, 0, 0, ( float ) scale.getY(), 0, 0, 0, 0, ( float ) scale.getZ(), 0, 0, 0, 0, 1 ); - Matrix4f transMat = new Matrix4f( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ( float ) ( location.getX() - displayLocation.getX() ), ( float ) ( location.getY() - displayLocation.getY() ), ( float ) ( location.getZ() - displayLocation.getZ() ), 1 ); - - disp.display.setInterpolationDelay( 0 ); - disp.display.setInterpolationDuration( 2 ); - disp.display.setTransformationMatrix( transMat.mul( transformRot ) ); - } - - tick += SIMULATION_SPEED / 20f; - } - }, 0, 1 ); - } - - @Override - public void onDisable() { - for ( final Entry< ChunkLocation, ChunkMesh > entry : chunkMeshes.entrySet() ) { - saveMesh( entry.getKey(), entry.getValue() ); - } - } - - private void registerCommands() { - new SubCommand( "minie" ) - .addSenderValidator( new SenderValidatorPlayer() ) - .add( new SubCommand( "spawn" ) - .add( new SubCommand( new InputValidatorDouble( 0.01, 100 ) ) - .defaultTo( this::spawnBlock ) ) - .defaultTo( this::spawnBlock ) ) - .add( new SubCommand( "constraint" ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - Location loc = player.getLocation(); - loc.setPitch( 0 ); - loc.setDirection( new Vector( 0, 0, 0 ) ); - - BlockData displayData = Material.IRON_TRAPDOOR.createBlockData(); - - final Vector scale = new Vector( 10, 3, 10 ); - - BoundingBox[] boxes = convertFrom( getShape( displayData, loc, BlockShapeType.VISUAL_SHAPE ) ); - for ( BoundingBox box : boxes ) { - Vector min = box.getMin().multiply( scale ); - Vector max = box.getMax().multiply( scale ); - box.resize( min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ() ); - } - Vector blockCenter = calculateCenter( boxes ); - - CollisionShape box; - if ( boxes.length == 0 ) { - player.sendMessage( "No bounding box detected! Wrong method?" ); - return; - } - - if ( boxes.length == 1 ) { - Vector min = boxes[ 0 ].getMin(); - Vector center = boxes[ 0 ].getCenter().subtract( min ); - box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); - } else { - CompoundCollisionShape compound = new CompoundCollisionShape(); - for ( BoundingBox aabb : boxes ) { - Vector min = aabb.getMin(); - Vector center = aabb.getCenter(); - Vector half = center.clone().subtract( min ); - center.subtract( blockCenter ); - CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); - compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); - } - box = compound; - } - - PhysicsRigidBody rigid = new PhysicsRigidBody( box, 5f ); - - // Since the center of the rigid body and the box shape are different, we need to offset the location - rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); - - // Convert the display direction vector to a quaternion - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - - space.addCollisionObject( rigid ); - - New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ ); - - constraint.set( MotorParam.UpperLimit, 3, ( float ) Math.PI / 6 ); - constraint.set( MotorParam.LowerLimit, 3, ( float ) - Math.PI / 6 ); - constraint.set( MotorParam.UpperLimit, 4, 0 ); - constraint.set( MotorParam.LowerLimit, 4, 0 ); - constraint.set( MotorParam.UpperLimit, 5, 0 ); - constraint.set( MotorParam.LowerLimit, 5, 0 ); - - space.addJoint( constraint ); - - player.sendMessage( "Location: " + rigid.getPhysicsLocation() ); - - // Create the display - BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { - d.setBlock( displayData ); - d.setViewRange( 1000 ); - } ); - - linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ).setScale( scale ) ); - - player.sendMessage( "Created constraint" ); - }) ) - .add( new SubCommand( "fixed" ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - Block block = player.getTargetBlockExact( 20 ); - convert( block ); - - player.sendMessage( "Fixed" ); - } ) ) - .add( new SubCommand( "chunk" ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - - player.sendMessage( "Registering chunk..." ); - generateMesh( player.getLocation().getChunk() ); - } ) ) - .add( new SubCommand( "old_chunk" ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - - player.sendMessage( "Registering chunk..." ); - scanAndGenerate( player.getLocation().getChunk() ); - } ) ) - .add( new SubCommand( "area" ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - - player.sendMessage( "Registering chunk..." ); - final World world = player.getWorld(); - final Chunk chunk = player.getLocation().getChunk(); - final int x = chunk.getX(); - final int z = chunk.getZ(); - - int radius = 3; - for ( int i = -radius; i < radius + 1; ++i ) { - for ( int j = -radius; j < radius + 1; ++j ) { - getLogger().info( "Queueing chunk scan at (" + ( x + i ) + ", " + ( z + j ) + ")" ); -// scanAndGenerate( world.getChunkAt( x + i, z + j ) ); - generateMesh( world.getChunkAt( x + i, z + j ) ); - } - } - } ) ) - .add( new SubCommand( "plane" ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - - if ( planeBody != null ) { - if ( planeBody.getCollisionGroup() == PhysicsCollisionObject.COLLISION_GROUP_01 ) { - player.sendMessage( "Disabling plane" ); - planeBody.setCollisionGroup( PhysicsCollisionObject.COLLISION_GROUP_02 ); - } else { - player.sendMessage( "Enabling plane" ); - planeBody.setCollisionGroup( PhysicsCollisionObject.COLLISION_GROUP_01 ); - } - } else { - player.sendMessage( "Plane does not exist!" ); - } - } ) ) - .add( new SubCommand( "save" ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - - player.sendMessage( "Saving chunk..." ); - player.sendMessage( "Saved " + saveChunk( player.getLocation().getChunk() ) + " boxes" ); - } ) ) - .add( new SubCommand( "toggle" ) - .defaultTo( ( sender, args, params ) -> { - paused = !paused; - } ) ) - .add( new SubCommand( "teleport" ) - .defaultTo( ( sender, args, params ) -> { - teleportOnDeactivate = !teleportOnDeactivate; - } ) ) - .add( new SubCommand( "impulse" ) - .add( new SubCommand( new InputValidatorInt() ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - Location loc = player.getLocation(); - - impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), params.getLast( int.class ) ); - } ) ) - .defaultTo( ( sender, args, params ) -> { - Player player = ( Player ) sender; - Location loc = player.getLocation(); - - impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), 15f ); - } ) ) - .defaultTo( new CommandExecutableMessage( "An argument must be provided" ) ) - .whenUnknown( new CommandExecutableMessage( "Unknown argument" ) ) - .applyTo( getCommand( "minie" ) ); - } - - private void impulse( Vector3f location, float power ) { - PhysicsRigidBody rigid = new PhysicsRigidBody( new SphereCollisionShape( 20 ), 1f ); - rigid.setPhysicsLocation( location ); - space.contactTest( rigid, new PhysicsCollisionListener() { - @Override - public void collision( PhysicsCollisionEvent event ) { - if ( event.getObjectB() instanceof PhysicsRigidBody ) { - PhysicsRigidBody objB = ( PhysicsRigidBody ) event.getObjectB(); - - Vector3f rLoc = objB.getPhysicsLocation(); - Vector3f to = rLoc.subtract( ( float ) location.getX(), ( float ) location.getY(), ( float ) location.getZ() ); - - float distSq = to.lengthSquared(); - - if ( distSq != 0 ) { - if ( power / distSq > 1 ) { - objB.applyCentralImpulse( to.normalize().mult( power / distSq ) ); - } - } - } - - } - } ); - } - - private void spawnBlock( final CommandSender sender, final String[] args, final CommandParameters params ) { - Player player = ( Player ) sender; - Location loc = player.getLocation(); - - // Optional scale - double scale = 1; - if ( params.size() > 2 ) { - scale = params.getFirst( double.class ); - } - - loc.setPitch( 0 ); - loc.setDirection( new Vector( 0, 0, 0 ) ); - - BlockData displayData; - ItemStack item = player.getInventory().getItemInMainHand(); - if ( item != null && item.getType() != Material.AIR && item.getType().isBlock() ) { - displayData = item.getType().createBlockData(); - } else { - displayData = Material.TNT.createBlockData(); - } - - BoundingBox[] boxes = convertFrom( getShape( displayData, null, BlockShapeType.SHAPE ) ); - Vector blockCenter = calculateCenter( boxes ); - - CollisionShape box; - if ( boxes.length == 0 ) { - player.sendMessage( "No bounding box detected! Wrong method?" ); - return; - } - - if ( boxes.length == 1 ) { - Vector min = boxes[ 0 ].getMin(); - Vector center = boxes[ 0 ].getCenter().subtract( min ); - box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); - } else { - CompoundCollisionShape compound = new CompoundCollisionShape(); - for ( BoundingBox aabb : boxes ) { - Vector min = aabb.getMin(); - Vector center = aabb.getCenter(); - Vector half = center.clone().subtract( min ); - center.subtract( blockCenter ); - CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); - compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); - } - box = compound; - } - - box.setScale( ( float ) scale ); - - PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f ); - - // Since the center of the rigid body and the box shape are different, we need to offset the location - rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); - - // Convert the display direction vector to a quaternion - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - - rigid.setMass( rigid.getMass() * ( float ) ( scale * scale ) ); - - space.addCollisionObject( rigid ); - - // Create the display - BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { - d.setBlock( displayData ); - d.setViewRange( 1000 ); - } ); - - linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter.multiply( scale ) ).setScale( new Vector( scale, scale, scale ) ) ); - - if ( displayData.getMaterial() == Material.TNT ) { - isTnt.add( rigid ); - } - - player.sendMessage( "Created display" ); - } - - /* - * Replaced with saveAllFacets - */ - @Deprecated - private int saveAllChunks( World world ) { - int total = 0; - for ( Chunk chunk : world.getLoadedChunks() ) { - total += saveChunk( chunk ); - } - return total; - } - - /* - * Replaced with saveFacets - */ - @Deprecated - private int saveChunk( Chunk chunk ) { - File saveFile = new File( getDataFolder() + "/chunks", chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ() ); - saveFile.getParentFile().mkdirs(); - saveFile.delete(); - - int count = 0; - try ( FileWriter writer = new FileWriter( saveFile ) ) { - World world = chunk.getWorld(); - for ( int y = world.getMinHeight(); y < world.getMaxHeight(); ++y ) { - for ( int z = 0; z < 16; ++z ) { - for ( int x = 0; x < 16; ++x ) { - Block block = chunk.getBlock( x, y, z ); - if ( !( block.isEmpty() && block.isLiquid() ) ) { - BlockState state = block.getState(); - BlockData data = state.getBlockData(); - Location loc = block.getLocation(); - - BoundingBox[] boxes = convertFrom( getShape( data, loc, BlockShapeType.VISUAL_SHAPE ) ); - - if ( boxes.length > 0 ) { - // Save this block - for ( BoundingBox box : boxes ) { - writer.write( ( box.getMinX() + x ) + "," ); - writer.write( ( box.getMinY() + y ) + "," ); - writer.write( ( box.getMinZ() + z ) + "," ); - writer.write( ( box.getMaxX() + x ) + "," ); - writer.write( ( box.getMaxY() + y ) + "," ); - writer.write( ( box.getMaxZ() + z ) + "\n" ); - count++; - } - } - } - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } - return count; - } - - private void saveAllFacets( final World world ) { - for ( final Chunk chunk : world.getLoadedChunks() ) { - saveFacets( chunk ); - } - } - - private void saveFacets( final Chunk chunk ) { - final Collection< Facet > facets = getFacetsConservative( chunk ); - saveFacets( new File( getDataFolder() + "/facets", chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ() + ".facet" ), facets ); - } - - private void loadOrGenerateWorld( final World world ) { - for ( final Chunk chunk : world.getLoadedChunks() ) { - loadOrGenerateMesh( chunk ); - } - } - - private void loadOrGenerateMesh( final Chunk chunk ) { - final ChunkLocation location = new ChunkLocation( chunk ); - - final ChunkMesh mesh = loadMesh( location ); - if ( mesh == null ) { - generateMesh( chunk ); - } else { - // 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 meshShape = new MeshCollisionShape( true, nativeMesh ); - - // Create a rigid body - PhysicsRigidBody rigid = new PhysicsRigidBody( meshShape, PhysicsBody.massForStatic ); - rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, chunk.getWorld().getMinHeight(), chunk.getZ() << 4 ) ); - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - rigid.setKinematic( false ); - - // Save our chunk mesh - chunkMeshes.put( location, mesh ); - - space.addCollisionObject( rigid ); - - PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); - if ( old != null ) { - getLogger().warning( "Old rigid body found!" ); - space.remove( old ); - } - } - } - - private ChunkMesh loadMesh( final ChunkLocation location ) { - final File file = new File( getDataFolder(), "meshes/" + 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 saveMesh( final ChunkLocation location, final ChunkMesh mesh ) { - final File file = new File( getDataFolder(), "meshes/" + 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 Collection< Facet > getFacetsConservative( Chunk chunk ) { - final World world = chunk.getWorld(); - final int worldMinHeight = world.getMinHeight(); - final int worldMaxHeight = world.getMaxHeight(); - final int worldHeight = worldMaxHeight - worldMinHeight; - - final Collection< Facet > facets = new HashSet< Facet >(); - - BlockVoxelType[] types = new BlockVoxelType[ worldHeight << 8 ]; - - for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { - final int yIndex = ( y - worldMinHeight ) << 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 = convertFrom( getShape( data, loc, BlockShapeType.COLLISION_SHAPE ), new Vector( x, y - worldMinHeight, z ) ); - - if ( boundingBoxes.length == 0 ) { - // No bounding box - types[ index ] = BlockVoxelType.NONE; - } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { - // Solid - types[ index ] = BlockVoxelType.SOLID; - } else { - types[ index ] = BlockVoxelType.TRANSPARENT; - // Complex bounding shape, and transparent - - // Convert the bounding box to facets - for ( final BoundingBox box : boundingBoxes ) { - facets.addAll( generateFacetsFor( box ) ); - } - } - } else { - types[ index ] = BlockVoxelType.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 ( types[ index ] == BlockVoxelType.SOLID ) { - // Check each face - if ( y == 0 || types[ index - 256 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z ) ); - facet.points.add( new Vector3d( x, y, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y, z ) ); - facet.normal = new Vector3d( 0, -1, 0 ); - facets.add( facet ); - } - - if ( y == worldHeight - 1 || types[ index + 256 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y + 1, z ) ); - facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z ) ); - facet.normal = new Vector3d( 0, 1, 0 ); - facets.add( facet ); - } - - if ( z == 0 || types[ index - 16 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z ) ); - facet.points.add( new Vector3d( x, y + 1, z ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z ) ); - facet.points.add( new Vector3d( x + 1, y, z ) ); - facet.normal = new Vector3d( 0, 0, -1 ); - facets.add( facet ); - } - - if ( z == 15 || types[ index + 16 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z + 1 ) ); - facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); - facet.normal = new Vector3d( 0, 0, 1 ); - facets.add( facet ); - } - - if ( x == 0 || types[ index - 1 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z ) ); - facet.points.add( new Vector3d( x, y, z + 1 ) ); - facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x, y + 1, z ) ); - facet.normal = new Vector3d( -1, 0, 0 ); - facets.add( facet ); - } - - if ( x == 15 || types[ index + 1 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x + 1, y, z ) ); - facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z ) ); - facet.normal = new Vector3d( 1, 0, 0 ); - facets.add( facet ); - } - } - } - } - } - - return facets; - } - - private Collection< Facet > getFacetsConservative( final ChunkSnapshot chunk, final int worldMinHeight, final int worldMaxHeight ) { - final int worldHeight = worldMaxHeight - worldMinHeight; - - final Collection< Facet > facets = new HashSet< Facet >(); - - BlockVoxelType[] types = new BlockVoxelType[ worldHeight << 8 ]; - - for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { - final int yIndex = ( y - worldMinHeight ) << 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 BlockData data= chunk.getBlockData( x, y, z ); - if ( !data.getMaterial().isAir() ) { - // Start the box at 0, 0, 0 - final BoundingBox[] boundingBoxes = convertFrom( getShape( data, null, BlockShapeType.COLLISION_SHAPE ), new Vector( x, y - worldMinHeight, z ) ); - - if ( boundingBoxes.length == 0 ) { - // No bounding box - types[ index ] = BlockVoxelType.NONE; - } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { - // Solid - types[ index ] = BlockVoxelType.SOLID; - } else { - types[ index ] = BlockVoxelType.TRANSPARENT; - // Complex bounding shape, and transparent - - // Convert the bounding box to facets - for ( final BoundingBox box : boundingBoxes ) { - facets.addAll( generateFacetsFor( box ) ); - } - } - } else { - types[ index ] = BlockVoxelType.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 ( types[ index ] == BlockVoxelType.SOLID ) { - // Check each face - if ( y == 0 || types[ index - 256 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z ) ); - facet.points.add( new Vector3d( x, y, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y, z ) ); - facet.normal = new Vector3d( 0, -1, 0 ); - facets.add( facet ); - } - - if ( y == worldHeight - 1 || types[ index + 256 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y + 1, z ) ); - facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z ) ); - facet.normal = new Vector3d( 0, 1, 0 ); - facets.add( facet ); - } - - if ( z == 0 || types[ index - 16 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z ) ); - facet.points.add( new Vector3d( x, y + 1, z ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z ) ); - facet.points.add( new Vector3d( x + 1, y, z ) ); - facet.normal = new Vector3d( 0, 0, -1 ); - facets.add( facet ); - } - - if ( z == 15 || types[ index + 16 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z + 1 ) ); - facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); - facet.normal = new Vector3d( 0, 0, 1 ); - facets.add( facet ); - } - - if ( x == 0 || types[ index - 1 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x, y, z ) ); - facet.points.add( new Vector3d( x, y, z + 1 ) ); - facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x, y + 1, z ) ); - facet.normal = new Vector3d( -1, 0, 0 ); - facets.add( facet ); - } - - if ( x == 15 || types[ index + 1 ] != BlockVoxelType.SOLID ) { - Facet facet = new Facet(); - facet.points.add( new Vector3d( x + 1, y, z ) ); - facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); - facet.points.add( new Vector3d( x + 1, y + 1, z ) ); - facet.normal = new Vector3d( 1, 0, 0 ); - facets.add( facet ); - } - } - } - } - } - - return facets; - } - - private void scanAndGenerate( World world ) { - for ( Chunk chunk : world.getLoadedChunks() ) { - generateMesh( chunk ); - } - } - - private void scanAndGenerate( final ChunkSnapshot chunk, final int worldMinHeight, final int worldMaxHeight ) { - final long start = System.currentTimeMillis(); - - final Collection< Facet > originalFacets = getFacetsConservative( chunk, worldMinHeight, worldMaxHeight ); - - final MeshBuilder builder = new MeshBuilder(); - for ( final Facet facet : originalFacets ) { - builder.addFacet( facet ); - } - - Collection< Facet > facets = builder.planes.parallelStream().flatMap( p -> process( p ).parallelStream().map( p::convert ) ).collect( Collectors.toSet() ); - - final class VectorRef { - int index; - } - - final class Triangle { - List< VectorRef > refs = new ArrayList< VectorRef >(); - } - - final Map< Vector, VectorRef > vertices = new HashMap< Vector, VectorRef >(); - final List< Triangle > triangles = new ArrayList< Triangle >(); - - // Convert each facet to a triangle - for ( final Facet facet : facets ) { - if ( facet.points.size() != 3 ) { - System.out.println( "Warning: Facet is not a triangle!" ); - } - - final Triangle triangle = new Triangle(); - for ( final Vector3d vec : facet.points ) { - VectorRef ref = null; - for ( final Entry< Vector, VectorRef > entry : vertices.entrySet() ) { - final Vector existingVec = entry.getKey(); - - if ( existingVec.distanceSquared( vec ) < 1e-8 ) { - ref = entry.getValue(); - break; - } - } - - if ( ref == null ) { - ref = new VectorRef(); - vertices.put( vec, ref ); - } - - triangle.refs.add( ref ); - } - triangles.add( triangle ); - } - - // Sort the vertices - final List< Entry< Vector, VectorRef > > sorted = new ArrayList< Entry< Vector, VectorRef > >( vertices.entrySet() ); - Collections.sort( sorted, ( aEntry, bEntry ) -> { - final Vector a = aEntry.getKey(); - final Vector b = bEntry.getKey(); - - final double xDiff = a.getX() - b.getX(); - if ( xDiff == 0 ) { - final double yDiff = a.getY() - b.getY(); - if ( yDiff == 0 ) { - return Double.compare( a.getZ(), b.getZ() ); - } else { - return Double.compare( yDiff, 0 ); - } - } else { - return Double.compare( xDiff, 0 ); - } - } ); - - final Vector3f[] positionArray = new Vector3f[ sorted.size() ]; - for ( int i = 0; i < sorted.size(); ++i ) { - final Entry< Vector, VectorRef > entry = sorted.get( i ); - entry.getValue().index = i; - - final Vector vector = entry.getKey(); - - positionArray[ i ] = new Vector3f( ( float ) vector.getX(), ( float ) vector.getY(), ( float ) vector.getZ() ); - } - - final int[] indexArray = new int[ triangles.size() * 3 ]; - for ( int i = 0; i < triangles.size(); ++i ) { - final Triangle triangle = triangles.get( i ); - if ( triangle.refs.size() != 3 ) { - throw new IllegalStateException( "Triangle does not have exactly 3 vertices!" ); - } - - final int indexArrayStart = i * 3; - indexArray[ indexArrayStart ] = triangle.refs.get( 0 ).index; - indexArray[ indexArrayStart + 1 ] = triangle.refs.get( 1 ).index; - indexArray[ indexArrayStart + 2 ] = triangle.refs.get( 2 ).index; - } - - final IndexedMesh nativeMesh = new IndexedMesh( positionArray, indexArray ); - // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... - final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); - - - // Create a rigid body - PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); - rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, worldMinHeight, chunk.getZ() << 4 ) ); - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - rigid.setKinematic( false ); - - Bukkit.getScheduler().runTask( this, () -> { - space.addCollisionObject( rigid ); - - PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); - if ( old != null ) { - getLogger().warning( "Old rigid body found!" ); - space.remove( old ); - } - final long time = System.currentTimeMillis() - start; - getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles from " + facets.size() + " facets" ); - } ); - } - - private void generateMesh( final Chunk chunk ) { - final long start = System.currentTimeMillis(); - final World world = chunk.getWorld(); - - final ChunkLocation chunkLocation = new ChunkLocation( chunk ); - 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 = 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( this, () -> { - 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 >(); - - 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< IndexedPolygon >(); - for ( final Polygon tri : tris ) { - final IndexedPolygon newPoly = new IndexedPolygon(); - for ( final Point point : tri.getPoints() ) { - 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; - } - - newPoly.points.add( ref ); - } - newMesh.completedPolygons.add( newPoly ); - } - - // Save the generated regions as an intermediary step so we can re-use them later - newMesh.generatedRegions = mesh.getPolygons(); - - chunkMesh.planes.put( plane, newMesh ); - } - } - - // Add all vector references - chunkMesh.pointReferences = vertices; - - // Now, create a physics object from the chunk mesh - final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh(); - // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... - final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); - - // Create a rigid body - PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); - rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, world.getMinHeight(), chunk.getZ() << 4 ) ); - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - rigid.setKinematic( false ); - - Bukkit.getScheduler().runTask( this, () -> { - // Save our chunk mesh - chunkMeshes.put( chunkLocation, chunkMesh ); - - space.addCollisionObject( rigid ); - - PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); - if ( old != null ) { - getLogger().warning( "Old rigid body found!" ); - space.remove( old ); - } - - final long time = System.currentTimeMillis() - start; -// getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles" ); - } ); - } ); - } - - private Collection< Location > update( final Collection< Location > locations ) { - if ( locations.isEmpty() ) { - return Collections.emptySet(); - } - - final Chunk chunk = locations.iterator().next().getChunk(); - final World world = chunk.getWorld(); - final ChunkLocation chunkLocation = new ChunkLocation( chunk ); - final ChunkMesh chunkMesh = chunkMeshes.get( chunkLocation ); - - final Collection< Location > canRemove = new ArrayDeque< Location >(); - if ( chunkMesh != null ) { - // 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 int x = location.getBlockX() - ( chunkLocation.getX() << 4 ); - final int z = location.getBlockZ() - ( chunkLocation.getZ() << 4 ); - final int y = location.getBlockY() - location.getWorld().getMinHeight(); - final int index = x + ( z << 4 ) + ( y << 8 ); - - canRemove.add( location ); - - if ( chunkMesh.isBlockAt( index, data ) ) { - continue; - } else { - final List< BoundingBox > boxes = 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 = chunkMesh.getType( index ); - final BoundingBox[] otherBoxes = chunkMesh.getBoxesAt( index ); - if ( boxes.size() == 0 ) { - if ( oldType == BlockDataType.NONE ) { - continue; - } - - chunkMesh.setType( index, BlockDataType.NONE ); - chunkMesh.setBoxes( index, null ); - } else if ( boxes.size() == 1 && boxes.get( 0 ).getVolume() == 1 ) { - if ( oldType == BlockDataType.SOLID ) { - continue; - } - - chunkMesh.setType( index, BlockDataType.SOLID ); - chunkMesh.setBoxes( index, null ); - } else { - if ( otherBoxes != null && isEqual.apply( boxes, otherBoxes ) ) { - continue; - } - - chunkMesh.setType( index, BlockDataType.COMPLEX ); - chunkMesh.setData( index, data ); - chunkMesh.setBoxes( index, boxes.toArray( BoundingBox[]::new ) ); - } - - 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() + x; - final double minY = aabb.getMinY() + y; - final double minZ = aabb.getMinZ() + z; - final double maxX = aabb.getMaxX() + x; - final double maxY = aabb.getMaxY() + y; - 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 ); - } - } - } - - // 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 = chunkMesh.planes.remove( plane ); - final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); - - if ( planeMesh != null ) { - // Reset all indexed polygons(triangles) for this plane - for ( final IndexedPolygon poly : planeMesh.completedPolygons ) { - poly.points.parallelStream().forEach( p -> --p.referenceCount ); - } - - // Add all polygons that were previously generated - for ( final Polygon polygon : planeMesh.generatedRegions ) { - mesh.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); - } - } - 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() ) ) ); - - 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< IndexedPolygon >(); - for ( final Polygon tri : tris ) { - final IndexedPolygon newPoly = new IndexedPolygon(); - for ( final Point point : tri.getPoints() ) { - 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 = chunkMesh.pointReferences.ceiling( tempRef ); - if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { - ref = upperRef; - } else { - final VectorReference lowerRef = chunkMesh.pointReferences.floor( tempRef ); - if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { - ref = lowerRef; - } - } - - if ( ref == null ) { - ref = tempRef; - ref.referenceCount = 1; - chunkMesh.pointReferences.add( ref ); - } else { - ++ref.referenceCount; - } - - newPoly.points.add( ref ); - } - newMesh.completedPolygons.add( newPoly ); - } - - // Save the generated regions as an intermediary step so we can re-use them later - newMesh.generatedRegions = mesh.getPolygons(); - - chunkMesh.planes.put( plane, newMesh ); - } - } - - // Remove all vector references that have a reference count of 0 - chunkMesh.pointReferences.removeIf( v -> v.referenceCount <= 0 ); - - // Now, create a physics object from the chunk mesh - final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh(); - // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... - final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); - - // Create a rigid body - PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); - rigid.setPhysicsLocation( new Vector3f( chunkLocation.getX() << 4, world.getMinHeight(), chunkLocation.getZ() << 4 ) ); - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - rigid.setKinematic( false ); - - synchronized ( space ) { - space.addCollisionObject( rigid ); - - final PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); - if ( old != null ) { - space.remove( old ); - } - } - return canRemove; - } - return canRemove; - } - - private void scanAndGenerate( final Chunk chunk ) { - final long start = System.currentTimeMillis(); - -// getLogger().info( "Gathering bounding boxes..." ); - final World world = chunk.getWorld(); - - final Collection< Facet > originalFacets = getFacetsConservative( chunk ); - - Bukkit.getScheduler().runTaskAsynchronously( this, () -> { -// getLogger().info( "Building planes..." ); - final MeshBuilder builder = new MeshBuilder(); - for ( final Facet facet : originalFacets ) { - builder.addFacet( facet ); - } - -// getLogger().info( "Meshing planes..." ); - Collection< Facet > facets = builder.planes.parallelStream().flatMap( p -> process( p ).parallelStream().map( p::convert ) ).collect( Collectors.toSet() ); - - final class VectorRef { - int index; - } - - final class Triangle { - List< VectorRef > refs = new ArrayList< VectorRef >(); - } - - final Map< Vector, VectorRef > vertices = new HashMap< Vector, VectorRef >(); - final List< Triangle > triangles = new ArrayList< Triangle >(); - -// getLogger().info( "Generating triangles..." ); - // Convert each facet to a triangle - for ( final Facet facet : facets ) { - if ( facet.points.size() != 3 ) { - System.out.println( "Warning: Facet is not a triangle!" ); - } - - final Triangle triangle = new Triangle(); - for ( final Vector vec : facet.points ) { - VectorRef ref = null; - for ( final Entry< Vector, VectorRef > entry : vertices.entrySet() ) { - final Vector existingVec = entry.getKey(); - - if ( existingVec.distanceSquared( vec ) < 1e-8 ) { - ref = entry.getValue(); - break; - } - } - - if ( ref == null ) { - ref = new VectorRef(); - vertices.put( vec, ref ); - } - - triangle.refs.add( ref ); - } - triangles.add( triangle ); - } - - // Sort the vertices - final List< Entry< Vector, VectorRef > > sorted = new ArrayList< Entry< Vector, VectorRef > >( vertices.entrySet() ); - Collections.sort( sorted, ( aEntry, bEntry ) -> { - final Vector a = aEntry.getKey(); - final Vector b = bEntry.getKey(); - - final double xDiff = a.getX() - b.getX(); - if ( xDiff == 0 ) { - final double yDiff = a.getY() - b.getY(); - if ( yDiff == 0 ) { - return Double.compare( a.getZ(), b.getZ() ); - } else { - return Double.compare( yDiff, 0 ); - } - } else { - return Double.compare( xDiff, 0 ); - } - } ); - -// getLogger().info( "Creating buffers..." ); - final Vector3f[] positionArray = new Vector3f[ sorted.size() ]; - for ( int i = 0; i < sorted.size(); ++i ) { - final Entry< Vector, VectorRef > entry = sorted.get( i ); - entry.getValue().index = i; - - final Vector vector = entry.getKey(); - - positionArray[ i ] = new Vector3f( ( float ) vector.getX(), ( float ) vector.getY(), ( float ) vector.getZ() ); - } - - final int[] indexArray = new int[ triangles.size() * 3 ]; - for ( int i = 0; i < triangles.size(); ++i ) { - final Triangle triangle = triangles.get( i ); - if ( triangle.refs.size() != 3 ) { - throw new IllegalStateException( "Triangle does not have exactly 3 vertices!" ); - } - - final int indexArrayStart = i * 3; - indexArray[ indexArrayStart ] = triangle.refs.get( 0 ).index; - indexArray[ indexArrayStart + 1 ] = triangle.refs.get( 1 ).index; - indexArray[ indexArrayStart + 2 ] = triangle.refs.get( 2 ).index; - } - -// getLogger().info( "Adding mesh collision shape..." ); - final IndexedMesh nativeMesh = new IndexedMesh( positionArray, indexArray ); - // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... - final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); - -// getLogger().info( "Mesh has " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles" ); - - // Create a rigid body - PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); - rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, world.getMinHeight(), chunk.getZ() << 4 ) ); - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - rigid.setKinematic( false ); - - Bukkit.getScheduler().runTask( this, () -> { - space.addCollisionObject( rigid ); - - PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); - if ( old != null ) { - getLogger().warning( "Old rigid body found!" ); - space.remove( old ); - } -// getLogger().info( "Done!" ); - final long time = System.currentTimeMillis() - start; - getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles from " + facets.size() + " facets" ); - } ); - } ); - } - - private void saveFacets( final File file, final Collection< Facet > facets ) { - file.getParentFile().mkdirs(); - file.delete(); - - try ( FileWriter writer = new FileWriter( file ) ) { - for ( final Facet facet : facets ) { - final Vector normal = facet.normal; - - writer.write( facet.points.size() + " " + normal.getX() + " " + normal.getY() + " " + normal.getZ() + "\n" ); - for ( final Vector point : facet.points ) { - writer.write( point.getX() + " " + point.getY() + " " + point.getZ() + "\n" ); - } - } - } catch ( IOException e ) { - e.printStackTrace(); - } - } - - private boolean convert( Block block ) { - BlockState state = block.getState(); - BlockData data = state.getBlockData(); - Location loc = block.getLocation(); - - BoundingBox[] boxes = convertFrom( getShape( data, loc, BlockShapeType.VISUAL_SHAPE ) ); - Vector blockCenter = calculateCenter( boxes ); - - CollisionShape box; - if ( boxes.length == 0 ) { - return false; - } - - if ( boxes.length == 1 ) { - Vector min = boxes[ 0 ].getMin(); - Vector center = boxes[ 0 ].getCenter().subtract( min ); - box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); - } else { - CompoundCollisionShape compound = new CompoundCollisionShape(); - for ( BoundingBox aabb : boxes ) { - Vector min = aabb.getMin(); - Vector center = aabb.getCenter(); - Vector half = center.clone().subtract( min ); - center.subtract( blockCenter ); - CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); - compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); - } - box = compound; - } - - PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f ); - rigid.setProtectGravity( true ); - rigid.setGravity( new Vector3f( 0, 0, 0 ) ); - - // Since the center of the rigid body and the box shape are different, we need to offset the location - rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); - - // Convert the display direction vector to a quaternion - rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); - - space.addCollisionObject( rigid ); - - New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ ); - - constraint.setBreakingImpulseThreshold( 5 ); - constraint.set( MotorParam.UpperLimit, 3, 0 ); - constraint.set( MotorParam.LowerLimit, 3, 0 ); - constraint.set( MotorParam.UpperLimit, 4, 0 ); - constraint.set( MotorParam.LowerLimit, 4, 0 ); - constraint.set( MotorParam.UpperLimit, 5, 0 ); - constraint.set( MotorParam.LowerLimit, 5, 0 ); - - space.addJoint( constraint ); - - joints.put( constraint, new JointStruct( state, rigid, blockCenter ) ); - - return true; - } - - private final boolean loadNativeLibraries() { - if ( SystemUtils.IS_OS_MAC ) { - 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 ) { - System.load( new File( getDataFolder() + "/lib", "bulletjme.dll" ).getAbsolutePath() ); - } else { - return false; - } - return true; - } - - private static Collection< Polygon > process( final Plane plane ) { - final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); - for ( Polygon poly : plane.polygons ) { - mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE ); - } - - return mesh.meshify(); - } - - private static Vector calculateCenter( BoundingBox[] boxes ) { - Vector center = new Vector( 0, 0, 0 ); - // TODO Throw error, probably - if ( boxes.length == 0 ) { - return center; - } - - double totalVolume = 0; - for ( BoundingBox box : boxes ) { - Vector mid = box.getCenter(); - center.add( mid.multiply( box.getVolume() ) ); - totalVolume += box.getVolume(); - } - return center.multiply( 1 / totalVolume ); - } - - // 1.21.5 - private static VoxelShape getShape( BlockData blockData, Location location, BlockShapeType type ) { - final CraftBlockData data = ( CraftBlockData ) blockData; - - WorldServer server = null; - BlockPosition pos = null; - if ( location != null ) { - server = ( ( CraftWorld ) location.getWorld() ).getHandle(); - pos = new BlockPosition( location.getBlockX(), location.getBlockY(), location.getBlockZ() ); - } - - switch ( type ) { - default: - case SHAPE: - return data.getState().f( server, pos ); - case COLLISION_SHAPE: - return data.getState().g( server, pos ); - case VISUAL_SHAPE: - return data.getState().c( server, pos, VoxelShapeCollision.a() ); - case INTERACTION_SHAPE: - return data.getState().i( server, pos ); - case BLOCK_SUPPORT_SHAPE: - return data.getState().h( server, pos ); - } - } - - private static BoundingBox[] convertFrom( final VoxelShape shape ) { - return convertFrom( shape, new Vector() ); - } - - private static BoundingBox[] convertFrom( final VoxelShape shape, final Vector offset ) { - return shape.e().stream() - .map( aabb -> { return new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ).shift( offset ); } ) - .toArray( BoundingBox[]::new ); - } - - private static List< Facet > generateFacetsFor( BoundingBox box ) { - List< Facet > facets = new ArrayList< Facet >(); - - Vector3d p1 = new Vector3d( box.getMinX(), box.getMinY(), box.getMinZ() ); - Vector3d p2 = new Vector3d( box.getMinX(), box.getMinY(), box.getMaxZ() ); - Vector3d p3 = new Vector3d( box.getMinX(), box.getMaxY(), box.getMinZ() ); - Vector3d p4 = new Vector3d( box.getMinX(), box.getMaxY(), box.getMaxZ() ); - Vector3d p5 = new Vector3d( box.getMaxX(), box.getMinY(), box.getMinZ() ); - Vector3d p6 = new Vector3d( box.getMaxX(), box.getMinY(), box.getMaxZ() ); - Vector3d p7 = new Vector3d( box.getMaxX(), box.getMaxY(), box.getMinZ() ); - Vector3d p8 = new Vector3d( box.getMaxX(), box.getMaxY(), box.getMaxZ() ); - - { - Facet facet = new Facet(); - facet.points.add( p1 ); - facet.points.add( p2 ); - facet.points.add( p4 ); - facet.points.add( p3 ); - facet.normal = new Vector3d( -1, 0, 0 ); - facets.add( facet ); - } - - { - Facet facet = new Facet(); - facet.points.add( p5 ); - facet.points.add( p6 ); - facet.points.add( p8 ); - facet.points.add( p7 ); - facet.normal = new Vector3d( 1, 0, 0 ); - facets.add( facet ); - } - - { - Facet facet = new Facet(); - facet.points.add( p1 ); - facet.points.add( p2 ); - facet.points.add( p6 ); - facet.points.add( p5 ); - facet.normal = new Vector3d( 0, -1, 0 ); - facets.add( facet ); - } - - { - Facet facet = new Facet(); - facet.points.add( p3 ); - facet.points.add( p4 ); - facet.points.add( p8 ); - facet.points.add( p7 ); - facet.normal = new Vector3d( 0, 1, 0 ); - facets.add( facet ); - } - - { - Facet facet = new Facet(); - facet.points.add( p1 ); - facet.points.add( p3 ); - facet.points.add( p7 ); - facet.points.add( p5 ); - facet.normal = new Vector3d( 0, 0, -1 ); - facets.add( facet ); - } - - { - Facet facet = new Facet(); - facet.points.add( p2 ); - facet.points.add( p4 ); - facet.points.add( p8 ); - facet.points.add( p6 ); - facet.normal = new Vector3d( 0, 0, 1 ); - facets.add( facet ); - } - - return facets; - } - - private static double getVolume( final AxisAlignedBB aabb ) { - return ( aabb.d - aabb.a ) * ( aabb.e - aabb.b ) * ( aabb.f - aabb.c ); - } - - private class CompleteMesh { - Mesh< RegionSimple > mesh; - Collection< Polygon > triangles; - - CompleteMesh( final MinecraftPlane plane, final Mesh< RegionSimple > mesh ) { - this.mesh = mesh; - triangles = mesh.meshify(); - } - } - - private class JointStruct { - BlockState state; - PhysicsRigidBody object; - Vector offset; - - JointStruct( BlockState state, PhysicsRigidBody body, Vector offset ) { - this.state = state; - this.object = body; - this.offset = offset.clone(); - } - } - - private class AuxiliaryDisplayStruct { - AuxiliaryDisplayStruct( Display display, Vector offset ) { - this.display = display; - this.offset = offset.clone(); - this.scale = new Vector( 1, 1, 1 ); - } - - AuxiliaryDisplayStruct setScale( Vector scale ) { - this.scale = scale.clone(); - - return this; - } - - Display display; - Vector offset; - Vector scale; - } - - private enum BlockShapeType { - SHAPE, - COLLISION_SHAPE, - VISUAL_SHAPE, - INTERACTION_SHAPE, - BLOCK_SUPPORT_SHAPE - } - - private enum BlockVoxelType { - NONE, - TRANSPARENT, - SOLID - } -} +package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; + +import java.io.File; +import java.io.FileWriter; +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.Iterator; +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.function.BiFunction; +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.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +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.entity.BlockDisplay; +import org.bukkit.entity.Display; +import org.bukkit.entity.Player; +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.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.Matrix4f; +import org.joml.Quaternionf; + +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.base.Facet; +import com.aaaaahhhhhhh.bananapuncher714.mesh.base.MeshBuilder; +import com.aaaaahhhhhhh.bananapuncher714.mesh.base.Plane; +import com.aaaaahhhhhhh.bananapuncher714.mesh.base.Vector3d; +import com.aaaaahhhhhhh.bananapuncher714.mesh.command.CommandParameters; +import com.aaaaahhhhhhh.bananapuncher714.mesh.command.SubCommand; +import com.aaaaahhhhhhh.bananapuncher714.mesh.command.executor.CommandExecutableMessage; +import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.InputValidatorDouble; +import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.InputValidatorInt; +import com.aaaaahhhhhhh.bananapuncher714.mesh.command.validator.sender.SenderValidatorPlayer; +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.IndexedPolygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.PlaneMesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.VectorReference; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane.PlaneAxis; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.ChunkMeshUtil; +import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.FileUtil; +import com.jme3.bullet.PhysicsSpace; +import com.jme3.bullet.PhysicsSpace.BroadphaseType; +import com.jme3.bullet.RotationOrder; +import com.jme3.bullet.collision.ContactListener; +import com.jme3.bullet.collision.PhysicsCollisionEvent; +import com.jme3.bullet.collision.PhysicsCollisionListener; +import com.jme3.bullet.collision.PhysicsCollisionObject; +import com.jme3.bullet.collision.shapes.BoxCollisionShape; +import com.jme3.bullet.collision.shapes.CollisionShape; +import com.jme3.bullet.collision.shapes.CompoundCollisionShape; +import com.jme3.bullet.collision.shapes.MeshCollisionShape; +import com.jme3.bullet.collision.shapes.PlaneCollisionShape; +import com.jme3.bullet.collision.shapes.SphereCollisionShape; +import com.jme3.bullet.collision.shapes.infos.IndexedMesh; +import com.jme3.bullet.joints.New6Dof; +import com.jme3.bullet.joints.PhysicsJoint; +import com.jme3.bullet.joints.motors.MotorParam; +import com.jme3.bullet.objects.PhysicsBody; +import com.jme3.bullet.objects.PhysicsRigidBody; +import com.jme3.math.Matrix3f; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; + +import net.minecraft.core.BlockPosition; +import net.minecraft.server.level.WorldServer; +import net.minecraft.world.phys.AxisAlignedBB; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.minecraft.world.phys.shapes.VoxelShapeCollision; + +public class MiniePlugin extends JavaPlugin { + private static final int TIME_STEPS = 5; + private static final float SIMULATION_SPEED = 1f; + + 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 PhysicsRigidBody planeBody; + + private Set< PhysicsCollisionObject > active = new HashSet< PhysicsCollisionObject >(); + private Set< PhysicsCollisionObject > isTnt = new HashSet< PhysicsCollisionObject >(); + + private Map< PhysicsJoint, JointStruct > joints = new HashMap< PhysicsJoint, JointStruct >(); + + private Map< ChunkLocation, PhysicsRigidBody > chunks = new HashMap< ChunkLocation, PhysicsRigidBody >(); + + private Map< Vector3f, CollisionShape > collisionShapes = new HashMap< Vector3f, CollisionShape >(); + + private Map< ChunkLocation, ChunkMesh > chunkMeshes = new HashMap< ChunkLocation, ChunkMesh >(); + + @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/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" ); + Bukkit.getPluginManager().disablePlugin( this ); + return; + } + + registerCommands(); + + // Don't log anything + PhysicsSpace.logger.setLevel( Level.WARNING ); + PhysicsSpace.loggerC.setLevel( Level.WARNING ); + PhysicsRigidBody.logger2.setLevel( Level.WARNING ); + + space = new PhysicsSpace( BroadphaseType.DBVT ); + PlaneCollisionShape plane = new PlaneCollisionShape( new com.jme3.math.Plane( new Vector3f( 0, 1, 0 ), 0 ) ); + planeBody = new PhysicsRigidBody( plane, PhysicsBody.massForStatic ); + planeBody.setPhysicsLocation( new Vector3f( 0, 128, 0 ) ); +// space.addCollisionObject( planeBody ); + space.setAccuracy( 1f / ( TIME_STEPS * 20f ) ); + + space.addContactListener( new ContactListener() { + @Override + public void onContactEnded( long manifoldId ) { + + } + + @Override + public void onContactProcessed( PhysicsCollisionObject objA, PhysicsCollisionObject objB, long manifoldPointId ) { + if ( isTnt.remove( objA ) ) { + impulse( objA.getPhysicsLocation(), 30f ); + if ( linkedDisplays.containsKey( objA ) ) { + linkedDisplays.get( objA ).display.remove(); + } + } + + if ( isTnt.remove( objB ) ) { + impulse( objB.getPhysicsLocation(), 30f ); + if ( linkedDisplays.containsKey( objB ) ) { + linkedDisplays.get( objB ).display.remove(); + } + } + } + + @Override + public void onContactStarted( long manifoldId ) { + + } + } ); + + Bukkit.getPluginManager().registerEvents( new Listener() { + @EventHandler + private void onChunkLoad( ChunkLoadEvent event ) { + if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { +// getLogger().info( "Loading chunk " + event.getChunk().getX() + ", " + event.getChunk().getZ() ); +// generateMesh( event.getChunk() ); +// scanAndGenerate( event.getChunk() ); +// saveFacets( event.getChunk() ); + loadOrGenerateMesh( event.getChunk() ); + } + } + + @EventHandler + private void onChunkUnload( ChunkUnloadEvent event ) { + if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { + final ChunkLocation loc = new ChunkLocation( event.getChunk() ); + if ( chunks.containsKey( loc ) ) { + space.removeCollisionObject( chunks.get( loc ) ); + chunks.remove( loc ); + } + + final ChunkMesh mesh = chunkMeshes.remove( loc ); + if ( mesh != null ) { + saveMesh( loc, mesh ); + } + } + } + + final Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >(); + final BukkitTask task = Bukkit.getScheduler().runTaskTimer( MiniePlugin.this, () -> { + final Collection< ChunkLocation > locations = new ArrayDeque< ChunkLocation >(); + updateLocations.entrySet().stream().forEach( e -> { + final Collection< Location > locs = update( e.getValue() ); + e.getValue().removeAll( locs ); + if ( !locs.isEmpty() ) { + locations.add( e.getKey() ); + } + } ); + + if ( !locations.isEmpty() ) { + space.activateAll( true ); + } + + updateLocations.values().removeIf( c -> c.isEmpty() ); + }, 1, 1 ); + final Function< ChunkLocation, Collection< Location > > getLocationFor = c -> { + Collection< Location > locations = updateLocations.get( c ); + if ( locations == null ) { + locations = new HashSet< Location >(); + updateLocations.put( c, locations ); + } + return locations; + }; + + @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() ); + } + }, this ); + + + getLogger().info( "There are " + Bukkit.getWorld( "world" ).getLoadedChunks().length + " chunks" ); +// getLogger().info( "Converted " + saveAllChunks( Bukkit.getWorld( "world" ) ) ); + + loadOrGenerateWorld( Bukkit.getWorld( "world" ) ); +// scanAndGenerate( Bukkit.getWorld( "world" ) ); +// saveAllFacets( Bukkit.getWorld( "world" ) ); + + Bukkit.getScheduler().runTaskTimer( this, () -> { + if ( !paused ) { + // Update the real world with the physics space objects + space.distributeEvents(); + space.update( tick, TIME_STEPS ); + + for ( Iterator< Entry< PhysicsJoint, JointStruct > > it = joints.entrySet().iterator(); it.hasNext(); ) { + Entry< PhysicsJoint, JointStruct > entry = it.next(); + PhysicsJoint joint = entry.getKey(); + JointStruct struct = entry.getValue(); + BlockState state = struct.state; + + if ( state.getBlock().getType() != state.getType() || !state.getChunk().isLoaded() ) { + space.removeJoint( joint ); + space.remove( struct.object ); + it.remove(); + } else if ( !joint.isEnabled() ) { + Location loc = state.getLocation(); + BlockData displayData = state.getBlockData(); + + // Create the display + BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { + d.setBlock( displayData ); + } ); + + linkedDisplays.put( struct.object, new AuxiliaryDisplayStruct( display, struct.offset ) ); + + state.getBlock().setType( Material.AIR ); + + struct.object.setProtectGravity( false ); + Vector3f grav = new Vector3f(); + space.setGravity( grav ); + struct.object.setGravity( grav ); + + space.removeJoint( joint ); + it.remove(); + } + } + + for ( Iterator< Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > > it = linkedDisplays.entrySet().iterator(); it.hasNext(); ) { + Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > entry = it.next(); + PhysicsCollisionObject obj = entry.getKey(); + AuxiliaryDisplayStruct disp = entry.getValue(); + + if ( !disp.display.isValid() ) { + it.remove(); + space.removeCollisionObject( obj ); + active.remove( obj ); + isTnt.remove( obj ); + + continue; + } + + Location displayLocation = disp.display.getLocation(); + + // Do not calculate for this object if it is inactive + if ( !obj.isActive() ) { + if ( teleportOnDeactivate && active.remove( obj ) ) { + // Newly deactivated object, teleport the display to the actual position to prevent the translation from getting too large + + Transformation currentTransformation = disp.display.getTransformation(); + org.joml.Vector3f trans = currentTransformation.getTranslation(); + + displayLocation.add( trans.x(), trans.y(), trans.z() ); + disp.display.teleport( displayLocation ); + + Transformation newTrans = new Transformation( new org.joml.Vector3f( 0, 0, 0 ), currentTransformation.getLeftRotation(), currentTransformation.getScale(), currentTransformation.getRightRotation() ); + disp.display.setInterpolationDelay( 0 ); + disp.display.setInterpolationDuration( 0 ); + disp.display.setTransformation( newTrans ); + } + + continue; + } else { + active.add( obj ); + } + + Vector3f location = obj.getPhysicsLocation(); + Quaternion rotation = new Quaternion(); + obj.getPhysicsRotation( rotation ); + + Vector offsetVec = disp.offset; + Vector scale = disp.scale; + Matrix4f transformRot = new Matrix4f(); + transformRot.set( new Quaternionf( rotation.getX(), rotation.getY(), rotation.getZ(), rotation.getW() ) ); + + transformRot.mul( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, - ( float ) offsetVec.getX(), - ( float ) offsetVec.getY(), - ( float ) offsetVec.getZ(), 1 ); + transformRot.mul( ( float ) scale.getX(), 0, 0, 0, 0, ( float ) scale.getY(), 0, 0, 0, 0, ( float ) scale.getZ(), 0, 0, 0, 0, 1 ); + Matrix4f transMat = new Matrix4f( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ( float ) ( location.getX() - displayLocation.getX() ), ( float ) ( location.getY() - displayLocation.getY() ), ( float ) ( location.getZ() - displayLocation.getZ() ), 1 ); + + disp.display.setInterpolationDelay( 0 ); + disp.display.setInterpolationDuration( 2 ); + disp.display.setTransformationMatrix( transMat.mul( transformRot ) ); + } + + tick += SIMULATION_SPEED / 20f; + } + }, 0, 1 ); + } + + @Override + public void onDisable() { + for ( final Entry< ChunkLocation, ChunkMesh > entry : chunkMeshes.entrySet() ) { + saveMesh( entry.getKey(), entry.getValue() ); + } + } + + private void registerCommands() { + new SubCommand( "minie" ) + .addSenderValidator( new SenderValidatorPlayer() ) + .add( new SubCommand( "spawn" ) + .add( new SubCommand( new InputValidatorDouble( 0.01, 100 ) ) + .defaultTo( this::spawnBlock ) ) + .defaultTo( this::spawnBlock ) ) + .add( new SubCommand( "constraint" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + loc.setPitch( 0 ); + loc.setDirection( new Vector( 0, 0, 0 ) ); + + BlockData displayData = Material.IRON_TRAPDOOR.createBlockData(); + + final Vector scale = new Vector( 10, 3, 10 ); + + BoundingBox[] boxes = convertFrom( getShape( displayData, loc, BlockShapeType.VISUAL_SHAPE ) ); + for ( BoundingBox box : boxes ) { + Vector min = box.getMin().multiply( scale ); + Vector max = box.getMax().multiply( scale ); + box.resize( min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ() ); + } + Vector blockCenter = calculateCenter( boxes ); + + CollisionShape box; + if ( boxes.length == 0 ) { + player.sendMessage( "No bounding box detected! Wrong method?" ); + return; + } + + if ( boxes.length == 1 ) { + Vector min = boxes[ 0 ].getMin(); + Vector center = boxes[ 0 ].getCenter().subtract( min ); + box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } else { + CompoundCollisionShape compound = new CompoundCollisionShape(); + for ( BoundingBox aabb : boxes ) { + Vector min = aabb.getMin(); + Vector center = aabb.getCenter(); + Vector half = center.clone().subtract( min ); + center.subtract( blockCenter ); + CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); + compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } + box = compound; + } + + PhysicsRigidBody rigid = new PhysicsRigidBody( box, 5f ); + + // Since the center of the rigid body and the box shape are different, we need to offset the location + rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); + + // Convert the display direction vector to a quaternion + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + + space.addCollisionObject( rigid ); + + New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ ); + + constraint.set( MotorParam.UpperLimit, 3, ( float ) Math.PI / 6 ); + constraint.set( MotorParam.LowerLimit, 3, ( float ) - Math.PI / 6 ); + constraint.set( MotorParam.UpperLimit, 4, 0 ); + constraint.set( MotorParam.LowerLimit, 4, 0 ); + constraint.set( MotorParam.UpperLimit, 5, 0 ); + constraint.set( MotorParam.LowerLimit, 5, 0 ); + + space.addJoint( constraint ); + + player.sendMessage( "Location: " + rigid.getPhysicsLocation() ); + + // Create the display + BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { + d.setBlock( displayData ); + d.setViewRange( 1000 ); + } ); + + linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ).setScale( scale ) ); + + player.sendMessage( "Created constraint" ); + }) ) + .add( new SubCommand( "fixed" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Block block = player.getTargetBlockExact( 20 ); + convert( block ); + + player.sendMessage( "Fixed" ); + } ) ) + .add( new SubCommand( "chunk" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + + player.sendMessage( "Registering chunk..." ); + generateMesh( player.getLocation().getChunk() ); + } ) ) + .add( new SubCommand( "old_chunk" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + + player.sendMessage( "Registering chunk..." ); + scanAndGenerate( player.getLocation().getChunk() ); + } ) ) + .add( new SubCommand( "area" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + + player.sendMessage( "Registering chunk..." ); + final World world = player.getWorld(); + final Chunk chunk = player.getLocation().getChunk(); + final int x = chunk.getX(); + final int z = chunk.getZ(); + + int radius = 3; + for ( int i = -radius; i < radius + 1; ++i ) { + for ( int j = -radius; j < radius + 1; ++j ) { + getLogger().info( "Queueing chunk scan at (" + ( x + i ) + ", " + ( z + j ) + ")" ); +// scanAndGenerate( world.getChunkAt( x + i, z + j ) ); + generateMesh( world.getChunkAt( x + i, z + j ) ); + } + } + } ) ) + .add( new SubCommand( "plane" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + + if ( planeBody != null ) { + if ( planeBody.getCollisionGroup() == PhysicsCollisionObject.COLLISION_GROUP_01 ) { + player.sendMessage( "Disabling plane" ); + planeBody.setCollisionGroup( PhysicsCollisionObject.COLLISION_GROUP_02 ); + } else { + player.sendMessage( "Enabling plane" ); + planeBody.setCollisionGroup( PhysicsCollisionObject.COLLISION_GROUP_01 ); + } + } else { + player.sendMessage( "Plane does not exist!" ); + } + } ) ) + .add( new SubCommand( "save" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + + player.sendMessage( "Saving chunk..." ); + player.sendMessage( "Saved " + saveChunk( player.getLocation().getChunk() ) + " boxes" ); + } ) ) + .add( new SubCommand( "toggle" ) + .defaultTo( ( sender, args, params ) -> { + paused = !paused; + } ) ) + .add( new SubCommand( "teleport" ) + .defaultTo( ( sender, args, params ) -> { + teleportOnDeactivate = !teleportOnDeactivate; + } ) ) + .add( new SubCommand( "impulse" ) + .add( new SubCommand( new InputValidatorInt() ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + + impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), params.getLast( int.class ) ); + } ) ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + + impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), 15f ); + } ) ) + .defaultTo( new CommandExecutableMessage( "An argument must be provided" ) ) + .whenUnknown( new CommandExecutableMessage( "Unknown argument" ) ) + .applyTo( getCommand( "minie" ) ); + } + + private void impulse( Vector3f location, float power ) { + PhysicsRigidBody rigid = new PhysicsRigidBody( new SphereCollisionShape( 20 ), 1f ); + rigid.setPhysicsLocation( location ); + space.contactTest( rigid, new PhysicsCollisionListener() { + @Override + public void collision( PhysicsCollisionEvent event ) { + if ( event.getObjectB() instanceof PhysicsRigidBody ) { + PhysicsRigidBody objB = ( PhysicsRigidBody ) event.getObjectB(); + + Vector3f rLoc = objB.getPhysicsLocation(); + Vector3f to = rLoc.subtract( ( float ) location.getX(), ( float ) location.getY(), ( float ) location.getZ() ); + + float distSq = to.lengthSquared(); + + if ( distSq != 0 ) { + if ( power / distSq > 1 ) { + objB.applyCentralImpulse( to.normalize().mult( power / distSq ) ); + } + } + } + + } + } ); + } + + private void spawnBlock( final CommandSender sender, final String[] args, final CommandParameters params ) { + Player player = ( Player ) sender; + Location loc = player.getLocation(); + + // Optional scale + double scale = 1; + if ( params.size() > 2 ) { + scale = params.getFirst( double.class ); + } + + loc.setPitch( 0 ); + loc.setDirection( new Vector( 0, 0, 0 ) ); + + BlockData displayData; + ItemStack item = player.getInventory().getItemInMainHand(); + if ( item != null && item.getType() != Material.AIR && item.getType().isBlock() ) { + displayData = item.getType().createBlockData(); + } else { + displayData = Material.TNT.createBlockData(); + } + + BoundingBox[] boxes = convertFrom( getShape( displayData, null, BlockShapeType.SHAPE ) ); + Vector blockCenter = calculateCenter( boxes ); + + CollisionShape box; + if ( boxes.length == 0 ) { + player.sendMessage( "No bounding box detected! Wrong method?" ); + return; + } + + if ( boxes.length == 1 ) { + Vector min = boxes[ 0 ].getMin(); + Vector center = boxes[ 0 ].getCenter().subtract( min ); + box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } else { + CompoundCollisionShape compound = new CompoundCollisionShape(); + for ( BoundingBox aabb : boxes ) { + Vector min = aabb.getMin(); + Vector center = aabb.getCenter(); + Vector half = center.clone().subtract( min ); + center.subtract( blockCenter ); + CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); + compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } + box = compound; + } + + box.setScale( ( float ) scale ); + + PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f ); + + // Since the center of the rigid body and the box shape are different, we need to offset the location + rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); + + // Convert the display direction vector to a quaternion + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + + rigid.setMass( rigid.getMass() * ( float ) ( scale * scale ) ); + + space.addCollisionObject( rigid ); + + // Create the display + BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { + d.setBlock( displayData ); + d.setViewRange( 1000 ); + } ); + + linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter.multiply( scale ) ).setScale( new Vector( scale, scale, scale ) ) ); + + if ( displayData.getMaterial() == Material.TNT ) { + isTnt.add( rigid ); + } + + player.sendMessage( "Created display" ); + } + + /* + * Replaced with saveAllFacets + */ + @Deprecated + private int saveAllChunks( World world ) { + int total = 0; + for ( Chunk chunk : world.getLoadedChunks() ) { + total += saveChunk( chunk ); + } + return total; + } + + /* + * Replaced with saveFacets + */ + @Deprecated + private int saveChunk( Chunk chunk ) { + File saveFile = new File( getDataFolder() + "/chunks", chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ() ); + saveFile.getParentFile().mkdirs(); + saveFile.delete(); + + int count = 0; + try ( FileWriter writer = new FileWriter( saveFile ) ) { + World world = chunk.getWorld(); + for ( int y = world.getMinHeight(); y < world.getMaxHeight(); ++y ) { + for ( int z = 0; z < 16; ++z ) { + for ( int x = 0; x < 16; ++x ) { + Block block = chunk.getBlock( x, y, z ); + if ( !( block.isEmpty() && block.isLiquid() ) ) { + BlockState state = block.getState(); + BlockData data = state.getBlockData(); + Location loc = block.getLocation(); + + BoundingBox[] boxes = convertFrom( getShape( data, loc, BlockShapeType.VISUAL_SHAPE ) ); + + if ( boxes.length > 0 ) { + // Save this block + for ( BoundingBox box : boxes ) { + writer.write( ( box.getMinX() + x ) + "," ); + writer.write( ( box.getMinY() + y ) + "," ); + writer.write( ( box.getMinZ() + z ) + "," ); + writer.write( ( box.getMaxX() + x ) + "," ); + writer.write( ( box.getMaxY() + y ) + "," ); + writer.write( ( box.getMaxZ() + z ) + "\n" ); + count++; + } + } + } + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return count; + } + + private void saveAllFacets( final World world ) { + for ( final Chunk chunk : world.getLoadedChunks() ) { + saveFacets( chunk ); + } + } + + private void saveFacets( final Chunk chunk ) { + final Collection< Facet > facets = getFacetsConservative( chunk ); + saveFacets( new File( getDataFolder() + "/facets", chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ() + ".facet" ), facets ); + } + + private void loadOrGenerateWorld( final World world ) { + for ( final Chunk chunk : world.getLoadedChunks() ) { + loadOrGenerateMesh( chunk ); + } + } + + private void loadOrGenerateMesh( final Chunk chunk ) { + final ChunkLocation location = new ChunkLocation( chunk ); + + final ChunkMesh mesh = loadMesh( location ); + if ( mesh == null ) { + generateMesh( chunk ); + } else { + // 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 meshShape = new MeshCollisionShape( true, nativeMesh ); + + // Create a rigid body + PhysicsRigidBody rigid = new PhysicsRigidBody( meshShape, PhysicsBody.massForStatic ); + rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, chunk.getWorld().getMinHeight(), chunk.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + rigid.setKinematic( false ); + + // Save our chunk mesh + chunkMeshes.put( location, mesh ); + + space.addCollisionObject( rigid ); + + PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + getLogger().warning( "Old rigid body found!" ); + space.remove( old ); + } + } + } + + private ChunkMesh loadMesh( final ChunkLocation location ) { + final File file = new File( getDataFolder(), "meshes/" + 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 saveMesh( final ChunkLocation location, final ChunkMesh mesh ) { + final File file = new File( getDataFolder(), "meshes/" + 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 Collection< Facet > getFacetsConservative( Chunk chunk ) { + final World world = chunk.getWorld(); + final int worldMinHeight = world.getMinHeight(); + final int worldMaxHeight = world.getMaxHeight(); + final int worldHeight = worldMaxHeight - worldMinHeight; + + final Collection< Facet > facets = new HashSet< Facet >(); + + BlockVoxelType[] types = new BlockVoxelType[ worldHeight << 8 ]; + + for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { + final int yIndex = ( y - worldMinHeight ) << 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 = convertFrom( getShape( data, loc, BlockShapeType.COLLISION_SHAPE ), new Vector( x, y - worldMinHeight, z ) ); + + if ( boundingBoxes.length == 0 ) { + // No bounding box + types[ index ] = BlockVoxelType.NONE; + } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { + // Solid + types[ index ] = BlockVoxelType.SOLID; + } else { + types[ index ] = BlockVoxelType.TRANSPARENT; + // Complex bounding shape, and transparent + + // Convert the bounding box to facets + for ( final BoundingBox box : boundingBoxes ) { + facets.addAll( generateFacetsFor( box ) ); + } + } + } else { + types[ index ] = BlockVoxelType.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 ( types[ index ] == BlockVoxelType.SOLID ) { + // Check each face + if ( y == 0 || types[ index - 256 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z ) ); + facet.points.add( new Vector3d( x, y, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y, z ) ); + facet.normal = new Vector3d( 0, -1, 0 ); + facets.add( facet ); + } + + if ( y == worldHeight - 1 || types[ index + 256 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y + 1, z ) ); + facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z ) ); + facet.normal = new Vector3d( 0, 1, 0 ); + facets.add( facet ); + } + + if ( z == 0 || types[ index - 16 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z ) ); + facet.points.add( new Vector3d( x, y + 1, z ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z ) ); + facet.points.add( new Vector3d( x + 1, y, z ) ); + facet.normal = new Vector3d( 0, 0, -1 ); + facets.add( facet ); + } + + if ( z == 15 || types[ index + 16 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z + 1 ) ); + facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); + facet.normal = new Vector3d( 0, 0, 1 ); + facets.add( facet ); + } + + if ( x == 0 || types[ index - 1 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z ) ); + facet.points.add( new Vector3d( x, y, z + 1 ) ); + facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x, y + 1, z ) ); + facet.normal = new Vector3d( -1, 0, 0 ); + facets.add( facet ); + } + + if ( x == 15 || types[ index + 1 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x + 1, y, z ) ); + facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z ) ); + facet.normal = new Vector3d( 1, 0, 0 ); + facets.add( facet ); + } + } + } + } + } + + return facets; + } + + private Collection< Facet > getFacetsConservative( final ChunkSnapshot chunk, final int worldMinHeight, final int worldMaxHeight ) { + final int worldHeight = worldMaxHeight - worldMinHeight; + + final Collection< Facet > facets = new HashSet< Facet >(); + + BlockVoxelType[] types = new BlockVoxelType[ worldHeight << 8 ]; + + for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { + final int yIndex = ( y - worldMinHeight ) << 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 BlockData data= chunk.getBlockData( x, y, z ); + if ( !data.getMaterial().isAir() ) { + // Start the box at 0, 0, 0 + final BoundingBox[] boundingBoxes = convertFrom( getShape( data, null, BlockShapeType.COLLISION_SHAPE ), new Vector( x, y - worldMinHeight, z ) ); + + if ( boundingBoxes.length == 0 ) { + // No bounding box + types[ index ] = BlockVoxelType.NONE; + } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { + // Solid + types[ index ] = BlockVoxelType.SOLID; + } else { + types[ index ] = BlockVoxelType.TRANSPARENT; + // Complex bounding shape, and transparent + + // Convert the bounding box to facets + for ( final BoundingBox box : boundingBoxes ) { + facets.addAll( generateFacetsFor( box ) ); + } + } + } else { + types[ index ] = BlockVoxelType.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 ( types[ index ] == BlockVoxelType.SOLID ) { + // Check each face + if ( y == 0 || types[ index - 256 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z ) ); + facet.points.add( new Vector3d( x, y, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y, z ) ); + facet.normal = new Vector3d( 0, -1, 0 ); + facets.add( facet ); + } + + if ( y == worldHeight - 1 || types[ index + 256 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y + 1, z ) ); + facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z ) ); + facet.normal = new Vector3d( 0, 1, 0 ); + facets.add( facet ); + } + + if ( z == 0 || types[ index - 16 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z ) ); + facet.points.add( new Vector3d( x, y + 1, z ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z ) ); + facet.points.add( new Vector3d( x + 1, y, z ) ); + facet.normal = new Vector3d( 0, 0, -1 ); + facets.add( facet ); + } + + if ( z == 15 || types[ index + 16 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z + 1 ) ); + facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); + facet.normal = new Vector3d( 0, 0, 1 ); + facets.add( facet ); + } + + if ( x == 0 || types[ index - 1 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x, y, z ) ); + facet.points.add( new Vector3d( x, y, z + 1 ) ); + facet.points.add( new Vector3d( x, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x, y + 1, z ) ); + facet.normal = new Vector3d( -1, 0, 0 ); + facets.add( facet ); + } + + if ( x == 15 || types[ index + 1 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector3d( x + 1, y, z ) ); + facet.points.add( new Vector3d( x + 1, y, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector3d( x + 1, y + 1, z ) ); + facet.normal = new Vector3d( 1, 0, 0 ); + facets.add( facet ); + } + } + } + } + } + + return facets; + } + + private void scanAndGenerate( World world ) { + for ( Chunk chunk : world.getLoadedChunks() ) { + generateMesh( chunk ); + } + } + + private void scanAndGenerate( final ChunkSnapshot chunk, final int worldMinHeight, final int worldMaxHeight ) { + final long start = System.currentTimeMillis(); + + final Collection< Facet > originalFacets = getFacetsConservative( chunk, worldMinHeight, worldMaxHeight ); + + final MeshBuilder builder = new MeshBuilder(); + for ( final Facet facet : originalFacets ) { + builder.addFacet( facet ); + } + + Collection< Facet > facets = builder.planes.parallelStream().flatMap( p -> process( p ).parallelStream().map( p::convert ) ).collect( Collectors.toSet() ); + + final class VectorRef { + int index; + } + + final class Triangle { + List< VectorRef > refs = new ArrayList< VectorRef >(); + } + + final Map< Vector, VectorRef > vertices = new HashMap< Vector, VectorRef >(); + final List< Triangle > triangles = new ArrayList< Triangle >(); + + // Convert each facet to a triangle + for ( final Facet facet : facets ) { + if ( facet.points.size() != 3 ) { + System.out.println( "Warning: Facet is not a triangle!" ); + } + + final Triangle triangle = new Triangle(); + for ( final Vector3d vec : facet.points ) { + VectorRef ref = null; + for ( final Entry< Vector, VectorRef > entry : vertices.entrySet() ) { + final Vector existingVec = entry.getKey(); + + if ( existingVec.distanceSquared( vec ) < 1e-8 ) { + ref = entry.getValue(); + break; + } + } + + if ( ref == null ) { + ref = new VectorRef(); + vertices.put( vec, ref ); + } + + triangle.refs.add( ref ); + } + triangles.add( triangle ); + } + + // Sort the vertices + final List< Entry< Vector, VectorRef > > sorted = new ArrayList< Entry< Vector, VectorRef > >( vertices.entrySet() ); + Collections.sort( sorted, ( aEntry, bEntry ) -> { + final Vector a = aEntry.getKey(); + final Vector b = bEntry.getKey(); + + final double xDiff = a.getX() - b.getX(); + if ( xDiff == 0 ) { + final double yDiff = a.getY() - b.getY(); + if ( yDiff == 0 ) { + return Double.compare( a.getZ(), b.getZ() ); + } else { + return Double.compare( yDiff, 0 ); + } + } else { + return Double.compare( xDiff, 0 ); + } + } ); + + final Vector3f[] positionArray = new Vector3f[ sorted.size() ]; + for ( int i = 0; i < sorted.size(); ++i ) { + final Entry< Vector, VectorRef > entry = sorted.get( i ); + entry.getValue().index = i; + + final Vector vector = entry.getKey(); + + positionArray[ i ] = new Vector3f( ( float ) vector.getX(), ( float ) vector.getY(), ( float ) vector.getZ() ); + } + + final int[] indexArray = new int[ triangles.size() * 3 ]; + for ( int i = 0; i < triangles.size(); ++i ) { + final Triangle triangle = triangles.get( i ); + if ( triangle.refs.size() != 3 ) { + throw new IllegalStateException( "Triangle does not have exactly 3 vertices!" ); + } + + final int indexArrayStart = i * 3; + indexArray[ indexArrayStart ] = triangle.refs.get( 0 ).index; + indexArray[ indexArrayStart + 1 ] = triangle.refs.get( 1 ).index; + indexArray[ indexArrayStart + 2 ] = triangle.refs.get( 2 ).index; + } + + final IndexedMesh nativeMesh = new IndexedMesh( positionArray, indexArray ); + // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... + final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); + + + // Create a rigid body + PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); + rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, worldMinHeight, chunk.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + rigid.setKinematic( false ); + + Bukkit.getScheduler().runTask( this, () -> { + space.addCollisionObject( rigid ); + + PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + getLogger().warning( "Old rigid body found!" ); + space.remove( old ); + } + final long time = System.currentTimeMillis() - start; + getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles from " + facets.size() + " facets" ); + } ); + } + + private void generateMesh( final Chunk chunk ) { + final long start = System.currentTimeMillis(); + final World world = chunk.getWorld(); + + final ChunkLocation chunkLocation = new ChunkLocation( chunk ); + 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 = 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( this, () -> { + 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 >(); + + 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< IndexedPolygon >(); + for ( final Polygon tri : tris ) { + final IndexedPolygon newPoly = new IndexedPolygon(); + for ( final Point point : tri.getPoints() ) { + 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; + } + + newPoly.points.add( ref ); + } + newMesh.completedPolygons.add( newPoly ); + } + + // Save the generated regions as an intermediary step so we can re-use them later + newMesh.generatedRegions = mesh.getPolygons(); + + chunkMesh.planes.put( plane, newMesh ); + } + } + + // Add all vector references + chunkMesh.pointReferences = vertices; + + // Now, create a physics object from the chunk mesh + final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh(); + // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... + final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); + + // Create a rigid body + PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); + rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, world.getMinHeight(), chunk.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + rigid.setKinematic( false ); + + Bukkit.getScheduler().runTask( this, () -> { + // Save our chunk mesh + chunkMeshes.put( chunkLocation, chunkMesh ); + + space.addCollisionObject( rigid ); + + PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + getLogger().warning( "Old rigid body found!" ); + space.remove( old ); + } + + final long time = System.currentTimeMillis() - start; +// getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles" ); + } ); + } ); + } + + private Collection< Location > update( final Collection< Location > locations ) { + if ( locations.isEmpty() ) { + return Collections.emptySet(); + } + + final Chunk chunk = locations.iterator().next().getChunk(); + final World world = chunk.getWorld(); + final ChunkLocation chunkLocation = new ChunkLocation( chunk ); + final ChunkMesh chunkMesh = chunkMeshes.get( chunkLocation ); + + final Collection< Location > canRemove = new ArrayDeque< Location >(); + if ( chunkMesh != null ) { + // 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 int x = location.getBlockX() - ( chunkLocation.getX() << 4 ); + final int z = location.getBlockZ() - ( chunkLocation.getZ() << 4 ); + final int y = location.getBlockY() - location.getWorld().getMinHeight(); + final int index = x + ( z << 4 ) + ( y << 8 ); + + canRemove.add( location ); + + if ( chunkMesh.isBlockAt( index, data ) ) { + continue; + } else { + final List< BoundingBox > boxes = 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 = chunkMesh.getType( index ); + final BoundingBox[] otherBoxes = chunkMesh.getBoxesAt( index ); + if ( boxes.size() == 0 ) { + if ( oldType == BlockDataType.NONE ) { + continue; + } + + chunkMesh.setType( index, BlockDataType.NONE ); + chunkMesh.setBoxes( index, null ); + } else if ( boxes.size() == 1 && boxes.get( 0 ).getVolume() == 1 ) { + if ( oldType == BlockDataType.SOLID ) { + continue; + } + + chunkMesh.setType( index, BlockDataType.SOLID ); + chunkMesh.setBoxes( index, null ); + } else { + if ( otherBoxes != null && isEqual.apply( boxes, otherBoxes ) ) { + continue; + } + + chunkMesh.setType( index, BlockDataType.COMPLEX ); + chunkMesh.setData( index, data ); + chunkMesh.setBoxes( index, boxes.toArray( BoundingBox[]::new ) ); + } + + 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() + x; + final double minY = aabb.getMinY() + y; + final double minZ = aabb.getMinZ() + z; + final double maxX = aabb.getMaxX() + x; + final double maxY = aabb.getMaxY() + y; + 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 ); + } + } + } + + // 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 = chunkMesh.planes.remove( plane ); + final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); + + if ( planeMesh != null ) { + // Reset all indexed polygons(triangles) for this plane + for ( final IndexedPolygon poly : planeMesh.completedPolygons ) { + poly.points.parallelStream().forEach( p -> --p.referenceCount ); + } + + // Add all polygons that were previously generated + for ( final Polygon polygon : planeMesh.generatedRegions ) { + mesh.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); + } + } + 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() ) ) ); + + 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< IndexedPolygon >(); + for ( final Polygon tri : tris ) { + final IndexedPolygon newPoly = new IndexedPolygon(); + for ( final Point point : tri.getPoints() ) { + 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 = chunkMesh.pointReferences.ceiling( tempRef ); + if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = upperRef; + } else { + final VectorReference lowerRef = chunkMesh.pointReferences.floor( tempRef ); + if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = lowerRef; + } + } + + if ( ref == null ) { + ref = tempRef; + ref.referenceCount = 1; + chunkMesh.pointReferences.add( ref ); + } else { + ++ref.referenceCount; + } + + newPoly.points.add( ref ); + } + newMesh.completedPolygons.add( newPoly ); + } + + // Save the generated regions as an intermediary step so we can re-use them later + newMesh.generatedRegions = mesh.getPolygons(); + + chunkMesh.planes.put( plane, newMesh ); + } + } + + // Remove all vector references that have a reference count of 0 + chunkMesh.pointReferences.removeIf( v -> v.referenceCount <= 0 ); + + // Now, create a physics object from the chunk mesh + final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh(); + // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... + final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); + + // Create a rigid body + PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); + rigid.setPhysicsLocation( new Vector3f( chunkLocation.getX() << 4, world.getMinHeight(), chunkLocation.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + rigid.setKinematic( false ); + + synchronized ( space ) { + space.addCollisionObject( rigid ); + + final PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + space.remove( old ); + } + } + return canRemove; + } + return canRemove; + } + + private void scanAndGenerate( final Chunk chunk ) { + final long start = System.currentTimeMillis(); + +// getLogger().info( "Gathering bounding boxes..." ); + final World world = chunk.getWorld(); + + final Collection< Facet > originalFacets = getFacetsConservative( chunk ); + + Bukkit.getScheduler().runTaskAsynchronously( this, () -> { +// getLogger().info( "Building planes..." ); + final MeshBuilder builder = new MeshBuilder(); + for ( final Facet facet : originalFacets ) { + builder.addFacet( facet ); + } + +// getLogger().info( "Meshing planes..." ); + Collection< Facet > facets = builder.planes.parallelStream().flatMap( p -> process( p ).parallelStream().map( p::convert ) ).collect( Collectors.toSet() ); + + final class VectorRef { + int index; + } + + final class Triangle { + List< VectorRef > refs = new ArrayList< VectorRef >(); + } + + final Map< Vector, VectorRef > vertices = new HashMap< Vector, VectorRef >(); + final List< Triangle > triangles = new ArrayList< Triangle >(); + +// getLogger().info( "Generating triangles..." ); + // Convert each facet to a triangle + for ( final Facet facet : facets ) { + if ( facet.points.size() != 3 ) { + System.out.println( "Warning: Facet is not a triangle!" ); + } + + final Triangle triangle = new Triangle(); + for ( final Vector vec : facet.points ) { + VectorRef ref = null; + for ( final Entry< Vector, VectorRef > entry : vertices.entrySet() ) { + final Vector existingVec = entry.getKey(); + + if ( existingVec.distanceSquared( vec ) < 1e-8 ) { + ref = entry.getValue(); + break; + } + } + + if ( ref == null ) { + ref = new VectorRef(); + vertices.put( vec, ref ); + } + + triangle.refs.add( ref ); + } + triangles.add( triangle ); + } + + // Sort the vertices + final List< Entry< Vector, VectorRef > > sorted = new ArrayList< Entry< Vector, VectorRef > >( vertices.entrySet() ); + Collections.sort( sorted, ( aEntry, bEntry ) -> { + final Vector a = aEntry.getKey(); + final Vector b = bEntry.getKey(); + + final double xDiff = a.getX() - b.getX(); + if ( xDiff == 0 ) { + final double yDiff = a.getY() - b.getY(); + if ( yDiff == 0 ) { + return Double.compare( a.getZ(), b.getZ() ); + } else { + return Double.compare( yDiff, 0 ); + } + } else { + return Double.compare( xDiff, 0 ); + } + } ); + +// getLogger().info( "Creating buffers..." ); + final Vector3f[] positionArray = new Vector3f[ sorted.size() ]; + for ( int i = 0; i < sorted.size(); ++i ) { + final Entry< Vector, VectorRef > entry = sorted.get( i ); + entry.getValue().index = i; + + final Vector vector = entry.getKey(); + + positionArray[ i ] = new Vector3f( ( float ) vector.getX(), ( float ) vector.getY(), ( float ) vector.getZ() ); + } + + final int[] indexArray = new int[ triangles.size() * 3 ]; + for ( int i = 0; i < triangles.size(); ++i ) { + final Triangle triangle = triangles.get( i ); + if ( triangle.refs.size() != 3 ) { + throw new IllegalStateException( "Triangle does not have exactly 3 vertices!" ); + } + + final int indexArrayStart = i * 3; + indexArray[ indexArrayStart ] = triangle.refs.get( 0 ).index; + indexArray[ indexArrayStart + 1 ] = triangle.refs.get( 1 ).index; + indexArray[ indexArrayStart + 2 ] = triangle.refs.get( 2 ).index; + } + +// getLogger().info( "Adding mesh collision shape..." ); + final IndexedMesh nativeMesh = new IndexedMesh( positionArray, indexArray ); + // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... + final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh ); + +// getLogger().info( "Mesh has " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles" ); + + // Create a rigid body + PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic ); + rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, world.getMinHeight(), chunk.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + rigid.setKinematic( false ); + + Bukkit.getScheduler().runTask( this, () -> { + space.addCollisionObject( rigid ); + + PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + getLogger().warning( "Old rigid body found!" ); + space.remove( old ); + } +// getLogger().info( "Done!" ); + final long time = System.currentTimeMillis() - start; + getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles from " + facets.size() + " facets" ); + } ); + } ); + } + + private void saveFacets( final File file, final Collection< Facet > facets ) { + file.getParentFile().mkdirs(); + file.delete(); + + try ( FileWriter writer = new FileWriter( file ) ) { + for ( final Facet facet : facets ) { + final Vector normal = facet.normal; + + writer.write( facet.points.size() + " " + normal.getX() + " " + normal.getY() + " " + normal.getZ() + "\n" ); + for ( final Vector point : facet.points ) { + writer.write( point.getX() + " " + point.getY() + " " + point.getZ() + "\n" ); + } + } + } catch ( IOException e ) { + e.printStackTrace(); + } + } + + private boolean convert( Block block ) { + BlockState state = block.getState(); + BlockData data = state.getBlockData(); + Location loc = block.getLocation(); + + BoundingBox[] boxes = convertFrom( getShape( data, loc, BlockShapeType.VISUAL_SHAPE ) ); + Vector blockCenter = calculateCenter( boxes ); + + CollisionShape box; + if ( boxes.length == 0 ) { + return false; + } + + if ( boxes.length == 1 ) { + Vector min = boxes[ 0 ].getMin(); + Vector center = boxes[ 0 ].getCenter().subtract( min ); + box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } else { + CompoundCollisionShape compound = new CompoundCollisionShape(); + for ( BoundingBox aabb : boxes ) { + Vector min = aabb.getMin(); + Vector center = aabb.getCenter(); + Vector half = center.clone().subtract( min ); + center.subtract( blockCenter ); + CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() ); + compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() ); + } + box = compound; + } + + PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f ); + rigid.setProtectGravity( true ); + rigid.setGravity( new Vector3f( 0, 0, 0 ) ); + + // Since the center of the rigid body and the box shape are different, we need to offset the location + rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) ); + + // Convert the display direction vector to a quaternion + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + + space.addCollisionObject( rigid ); + + New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ ); + + constraint.setBreakingImpulseThreshold( 5 ); + constraint.set( MotorParam.UpperLimit, 3, 0 ); + constraint.set( MotorParam.LowerLimit, 3, 0 ); + constraint.set( MotorParam.UpperLimit, 4, 0 ); + constraint.set( MotorParam.LowerLimit, 4, 0 ); + constraint.set( MotorParam.UpperLimit, 5, 0 ); + constraint.set( MotorParam.LowerLimit, 5, 0 ); + + space.addJoint( constraint ); + + joints.put( constraint, new JointStruct( state, rigid, blockCenter ) ); + + return true; + } + + private final boolean loadNativeLibraries() { + if ( SystemUtils.IS_OS_MAC ) { + 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 ) { + System.load( new File( getDataFolder() + "/lib", "bulletjme.dll" ).getAbsolutePath() ); + } else { + return false; + } + return true; + } + + private static Collection< Polygon > process( final Plane plane ) { + final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + for ( Polygon poly : plane.polygons ) { + mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE ); + } + + return mesh.meshify(); + } + + private static Vector calculateCenter( BoundingBox[] boxes ) { + Vector center = new Vector( 0, 0, 0 ); + // TODO Throw error, probably + if ( boxes.length == 0 ) { + return center; + } + + double totalVolume = 0; + for ( BoundingBox box : boxes ) { + Vector mid = box.getCenter(); + center.add( mid.multiply( box.getVolume() ) ); + totalVolume += box.getVolume(); + } + return center.multiply( 1 / totalVolume ); + } + + // 1.21.5 + private static VoxelShape getShape( BlockData blockData, Location location, BlockShapeType type ) { + final CraftBlockData data = ( CraftBlockData ) blockData; + + WorldServer server = null; + BlockPosition pos = null; + if ( location != null ) { + server = ( ( CraftWorld ) location.getWorld() ).getHandle(); + pos = new BlockPosition( location.getBlockX(), location.getBlockY(), location.getBlockZ() ); + } + + switch ( type ) { + default: + case SHAPE: + return data.getState().f( server, pos ); + case COLLISION_SHAPE: + return data.getState().g( server, pos ); + case VISUAL_SHAPE: + return data.getState().c( server, pos, VoxelShapeCollision.a() ); + case INTERACTION_SHAPE: + return data.getState().i( server, pos ); + case BLOCK_SUPPORT_SHAPE: + return data.getState().h( server, pos ); + } + } + + private static BoundingBox[] convertFrom( final VoxelShape shape ) { + return convertFrom( shape, new Vector() ); + } + + private static BoundingBox[] convertFrom( final VoxelShape shape, final Vector offset ) { + return shape.e().stream() + .map( aabb -> { return new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ).shift( offset ); } ) + .toArray( BoundingBox[]::new ); + } + + private static List< Facet > generateFacetsFor( BoundingBox box ) { + List< Facet > facets = new ArrayList< Facet >(); + + Vector3d p1 = new Vector3d( box.getMinX(), box.getMinY(), box.getMinZ() ); + Vector3d p2 = new Vector3d( box.getMinX(), box.getMinY(), box.getMaxZ() ); + Vector3d p3 = new Vector3d( box.getMinX(), box.getMaxY(), box.getMinZ() ); + Vector3d p4 = new Vector3d( box.getMinX(), box.getMaxY(), box.getMaxZ() ); + Vector3d p5 = new Vector3d( box.getMaxX(), box.getMinY(), box.getMinZ() ); + Vector3d p6 = new Vector3d( box.getMaxX(), box.getMinY(), box.getMaxZ() ); + Vector3d p7 = new Vector3d( box.getMaxX(), box.getMaxY(), box.getMinZ() ); + Vector3d p8 = new Vector3d( box.getMaxX(), box.getMaxY(), box.getMaxZ() ); + + { + Facet facet = new Facet(); + facet.points.add( p1 ); + facet.points.add( p2 ); + facet.points.add( p4 ); + facet.points.add( p3 ); + facet.normal = new Vector3d( -1, 0, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p5 ); + facet.points.add( p6 ); + facet.points.add( p8 ); + facet.points.add( p7 ); + facet.normal = new Vector3d( 1, 0, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p1 ); + facet.points.add( p2 ); + facet.points.add( p6 ); + facet.points.add( p5 ); + facet.normal = new Vector3d( 0, -1, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p3 ); + facet.points.add( p4 ); + facet.points.add( p8 ); + facet.points.add( p7 ); + facet.normal = new Vector3d( 0, 1, 0 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p1 ); + facet.points.add( p3 ); + facet.points.add( p7 ); + facet.points.add( p5 ); + facet.normal = new Vector3d( 0, 0, -1 ); + facets.add( facet ); + } + + { + Facet facet = new Facet(); + facet.points.add( p2 ); + facet.points.add( p4 ); + facet.points.add( p8 ); + facet.points.add( p6 ); + facet.normal = new Vector3d( 0, 0, 1 ); + facets.add( facet ); + } + + return facets; + } + + private static double getVolume( final AxisAlignedBB aabb ) { + return ( aabb.d - aabb.a ) * ( aabb.e - aabb.b ) * ( aabb.f - aabb.c ); + } + + private class CompleteMesh { + Mesh< RegionSimple > mesh; + Collection< Polygon > triangles; + + CompleteMesh( final MinecraftPlane plane, final Mesh< RegionSimple > mesh ) { + this.mesh = mesh; + triangles = mesh.meshify(); + } + } + + private class JointStruct { + BlockState state; + PhysicsRigidBody object; + Vector offset; + + JointStruct( BlockState state, PhysicsRigidBody body, Vector offset ) { + this.state = state; + this.object = body; + this.offset = offset.clone(); + } + } + + private class AuxiliaryDisplayStruct { + AuxiliaryDisplayStruct( Display display, Vector offset ) { + this.display = display; + this.offset = offset.clone(); + this.scale = new Vector( 1, 1, 1 ); + } + + AuxiliaryDisplayStruct setScale( Vector scale ) { + this.scale = scale.clone(); + + return this; + } + + Display display; + Vector offset; + Vector scale; + } + + private enum BlockShapeType { + SHAPE, + COLLISION_SHAPE, + VISUAL_SHAPE, + INTERACTION_SHAPE, + BLOCK_SUPPORT_SHAPE + } + + private enum BlockVoxelType { + NONE, + TRANSPARENT, + SOLID + } +}