package com.aaaaahhhhhhh.bananapuncher714.minietest; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; 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.stream.Collectors; import java.util.Set; import org.apache.commons.lang.SystemUtils; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.data.BlockData; import org.bukkit.craftbukkit.v1_21_R4.CraftWorld; import org.bukkit.craftbukkit.v1_21_R4.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.Listener; 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.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.Mesh; import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionRuleWinding; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple.GluWindingRule; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.SubCommand; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor.CommandExecutableMessage; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.InputValidatorInt; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender.SenderValidatorPlayer; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkLocation; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Facet; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.MeshBuilder; import com.aaaaahhhhhhh.bananapuncher714.minietest.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.Plane; 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.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 >(); @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(); space = new PhysicsSpace( BroadphaseType.DBVT ); PlaneCollisionShape plane = new PlaneCollisionShape( new 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..." ); // scanAndGenerate( event.getChunk() ); // saveChunk( event.getChunk() ); } } @EventHandler private void onChunkUnload( ChunkUnloadEvent event ) { if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { ChunkLocation loc = new ChunkLocation( event.getChunk() ); if ( chunks.containsKey( loc ) ) { space.removeCollisionObject( chunks.get( loc ) ); chunks.remove( loc ); } } } }, this ); // getLogger().info( "There are " + Bukkit.getWorld( "world" ).getLoadedChunks().length + " chunks" ); // getLogger().info( "Converted " + saveAllChunks( 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 ); } private void registerCommands() { new SubCommand( "minie" ) .addSenderValidator( new SenderValidatorPlayer() ) .add( new SubCommand( "spawn" ) .defaultTo( ( sender, args, params ) -> { Player player = ( Player ) sender; Location loc = player.getLocation(); 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; } 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 ) ); space.addCollisionObject( rigid ); // Create the display BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { d.setBlock( displayData ); } ); linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ) ); if ( displayData.getMaterial() == Material.TNT ) { isTnt.add( rigid ); } player.sendMessage( "Created display" ); } ) ) .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( 5, 2, 5 ); 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 ); } ); 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..." ); scanAndGenerate( player.getLocation().getChunk() ); } ) ) .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 int saveAllChunks( World world ) { int total = 0; for ( Chunk chunk : world.getLoadedChunks() ) { total += saveChunk( chunk ); } return total; } 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 scanAndGenerate( World world ) { for ( Chunk chunk : world.getLoadedChunks() ) { scanAndGenerate( chunk ); } } private void scanAndGenerate( final Chunk chunk ) { final long start = System.currentTimeMillis(); final World world = chunk.getWorld(); final int worldMinHeight = world.getMinHeight(); final int worldMaxHeight = world.getMaxHeight(); final Collection< BoundingBox > boxes = new ArrayDeque< BoundingBox >(); getLogger().info( "Gathering bounding boxes..." ); for ( int y = worldMinHeight; y < worldMaxHeight; ++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() ) ) { 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 ) ); for ( BoundingBox box : boundingBoxes ) { boxes.add( box ); } } } } } Bukkit.getScheduler().runTaskAsynchronously( this, () -> { getLogger().info( "Building planes..." ); final MeshBuilder builder = new MeshBuilder(); for ( final BoundingBox box : boxes ) { for ( final Facet facet : generateFacetsFor( box ) ) { 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" ); } ); } ); } 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 com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.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 >(); Vector p1 = new Vector( box.getMinX(), box.getMinY(), box.getMinZ() ); Vector p2 = new Vector( box.getMinX(), box.getMinY(), box.getMaxZ() ); Vector p3 = new Vector( box.getMinX(), box.getMaxY(), box.getMinZ() ); Vector p4 = new Vector( box.getMinX(), box.getMaxY(), box.getMaxZ() ); Vector p5 = new Vector( box.getMaxX(), box.getMinY(), box.getMinZ() ); Vector p6 = new Vector( box.getMaxX(), box.getMinY(), box.getMaxZ() ); Vector p7 = new Vector( box.getMaxX(), box.getMaxY(), box.getMinZ() ); Vector p8 = new Vector( 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 Vector( -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 Vector( 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 Vector( 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 Vector( 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 Vector( 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 Vector( 0, 0, 1 ); facets.add( facet ); } return facets; } 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 } }