diff --git a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts index 60f497087b..54a8fcb10d 100644 --- a/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/buildlogic.java-conventions.gradle.kts @@ -68,7 +68,7 @@ spotless { ratchetFrom = "origin/dev" java { removeUnusedImports() - palantirJavaFormat("2.47.0").style("GOOGLE").formatJavadoc(true) + palantirJavaFormat("2.57.0").style("GOOGLE").formatJavadoc(true) } } diff --git a/core/src/main/java/tc/oc/pgm/projectile/ProjectileDefinition.java b/core/src/main/java/tc/oc/pgm/projectile/ProjectileDefinition.java index 3b4b4e8517..56fe271023 100644 --- a/core/src/main/java/tc/oc/pgm/projectile/ProjectileDefinition.java +++ b/core/src/main/java/tc/oc/pgm/projectile/ProjectileDefinition.java @@ -3,6 +3,7 @@ import java.time.Duration; import java.util.List; import org.bukkit.entity.Entity; +import org.bukkit.entity.FallingBlock; import org.bukkit.potion.PotionEffect; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.filter.Filter; @@ -15,7 +16,7 @@ public class ProjectileDefinition extends SelfIdentifyingFeatureDefinition { protected @Nullable Float power; protected double velocity; protected ClickAction clickAction; - protected Class projectile; + protected ProjectileEntity projectile; protected List potion; protected Filter destroyFilter; protected Duration coolDown; @@ -30,7 +31,7 @@ public ProjectileDefinition( @Nullable Float power, double velocity, ClickAction clickAction, - Class entity, + ProjectileEntity entity, List potion, Filter destroyFilter, Duration coolDown, @@ -52,6 +53,25 @@ public ProjectileDefinition( this.blockMaterial = blockMaterial; } + public sealed interface ProjectileEntity permits RealEntity, BlockEntityType { + boolean requiresBlockMaterial(); + } + + record RealEntity(Class entityType) implements ProjectileEntity { + @Override + public boolean requiresBlockMaterial() { + return FallingBlock.class.isAssignableFrom(entityType); + } + } + + record BlockEntityType(float size, boolean solidBlockCollision, Duration maxTravelTime) + implements ProjectileEntity { + @Override + public boolean requiresBlockMaterial() { + return true; + } + } + public @Nullable String getName() { return name; } diff --git a/core/src/main/java/tc/oc/pgm/projectile/ProjectileMatchModule.java b/core/src/main/java/tc/oc/pgm/projectile/ProjectileMatchModule.java index e080d7b749..da4ef26a38 100644 --- a/core/src/main/java/tc/oc/pgm/projectile/ProjectileMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/projectile/ProjectileMatchModule.java @@ -4,7 +4,11 @@ import com.google.common.collect.ImmutableSet; import java.util.HashMap; +import java.util.Objects; import java.util.UUID; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Entity; @@ -37,6 +41,7 @@ import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchModule; import tc.oc.pgm.api.match.MatchScope; +import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.api.player.ParticipantState; import tc.oc.pgm.events.ListenerScope; @@ -44,7 +49,10 @@ import tc.oc.pgm.filters.query.BlockQuery; import tc.oc.pgm.filters.query.PlayerBlockQuery; import tc.oc.pgm.kits.tag.ItemTags; +import tc.oc.pgm.util.MatchPlayers; +import tc.oc.pgm.util.TimeUtils; import tc.oc.pgm.util.bukkit.MetadataUtils; +import tc.oc.pgm.util.bukkit.entities.BlockEntity; import tc.oc.pgm.util.inventory.InventoryUtils; import tc.oc.pgm.util.nms.NMSHacks; @@ -78,68 +86,78 @@ public void onClickEvent(PlayerInteractEvent event) { ParticipantState playerState = match.getParticipantState(player); if (playerState == null) return; - ProjectileDefinition projectileDefinition = - this.getProjectileDefinition(player.getItemInHand()); - - if (projectileDefinition != null - && isValidProjectileAction(event.getAction(), projectileDefinition.clickAction)) { - // Prevent the original projectile from being fired - event.setCancelled(true); - - if (this.isCooldownActive(player, projectileDefinition)) return; - - boolean realProjectile = Projectile.class.isAssignableFrom(projectileDefinition.projectile); - Vector velocity = - player.getEyeLocation().getDirection().multiply(projectileDefinition.velocity); - Entity projectile; - try { - assertTrue(launchingDefinition.get() == null, "nested projectile launch"); - launchingDefinition.set(projectileDefinition); - if (realProjectile) { - projectile = player.launchProjectile( - projectileDefinition.projectile.asSubclass(Projectile.class), velocity); - if (projectile instanceof Fireball fireball && projectileDefinition.precise) { - NMSHacks.NMS_HACKS.setFireballDirection(fireball, velocity); - } - } else { - if (FallingBlock.class.isAssignableFrom(projectileDefinition.projectile)) { - projectile = - projectileDefinition.blockMaterial.spawnFallingBlock(player.getEyeLocation()); + var definition = this.getProjectileDefinition(player.getItemInHand()); + + if (definition == null || !isValidProjectileAction(event.getAction(), definition.clickAction)) + return; + + // Prevent the original projectile from being fired + event.setCancelled(true); + + if (this.isCooldownActive(player, definition)) return; + + var projType = definition.projectile; + boolean needsEvent = true; + Vector velocity = player.getEyeLocation().getDirection().multiply(definition.velocity); + Entity projectile = null; + try { + assertTrue(launchingDefinition.get() == null, "nested projectile launch"); + launchingDefinition.set(definition); + switch (projType) { + case ProjectileDefinition.RealEntity(Class entityType) -> { + if (Projectile.class.isAssignableFrom(entityType)) { + needsEvent = false; + + projectile = player.launchProjectile(entityType.asSubclass(Projectile.class), velocity); + if (projectile instanceof Fireball fireball && definition.precise) { + NMSHacks.NMS_HACKS.setFireballDirection(fireball, velocity); + } } else { - projectile = - player.getWorld().spawn(player.getEyeLocation(), projectileDefinition.projectile); + if (FallingBlock.class.isAssignableFrom(entityType)) { + projectile = definition.blockMaterial.spawnFallingBlock(player.getEyeLocation()); + } else { + projectile = player.getWorld().spawn(player.getEyeLocation(), entityType); + } + projectile.setVelocity(velocity); } - projectile.setVelocity(velocity); } - if (projectileDefinition.power != null && projectile instanceof Explosive) { - ((Explosive) projectile).setYield(projectileDefinition.power); + case ProjectileDefinition.BlockEntityType ce -> { + Location loc = player.getEyeLocation(); + var be = BlockEntity.spawnBlockEntity(loc, definition.blockMaterial, ce.size(), velocity); + new BlockRunner(definition, be, player, loc); } - projectile.setMetadata( - "projectileDefinition", new FixedMetadataValue(PGM.get(), projectileDefinition)); - } finally { - launchingDefinition.remove(); } - // If the entity implements Projectile, it will have already generated a - // ProjectileLaunchEvent. - // Otherwise, we fire our custom event. - if (!realProjectile) { - EntityLaunchEvent launchEvent = new EntityLaunchEvent(projectile, event.getPlayer()); - match.callEvent(launchEvent); - if (launchEvent.isCancelled()) { - projectile.remove(); - return; - } + if (definition.power != null && projectile instanceof Explosive) { + ((Explosive) projectile).setYield(definition.power); } - - if (projectileDefinition.throwable) { - InventoryUtils.consumeItem(event); + if (projectile != null) { + projectile.setMetadata( + "projectileDefinition", new FixedMetadataValue(PGM.get(), definition)); } + } finally { + launchingDefinition.remove(); + } - if (projectileDefinition.coolDown != null) { - startCooldown(player, projectileDefinition); + // If the entity implements Projectile, it will have already generated a + // ProjectileLaunchEvent. + // Otherwise, we fire our custom event. + if (needsEvent && projectile != null) { + EntityLaunchEvent launchEvent = new EntityLaunchEvent(projectile, event.getPlayer()); + match.callEvent(launchEvent); + if (launchEvent.isCancelled()) { + projectile.remove(); + return; } } + + if (definition.throwable) { + InventoryUtils.consumeItem(event); + } + + if (definition.coolDown != null) { + startCooldown(player, definition); + } } @EventHandler @@ -289,4 +307,106 @@ public boolean isCooldownActive(Player player, ProjectileDefinition definition) ProjectileCooldowns playerCooldowns = projectileCooldowns.get(player.getUniqueId()); return (playerCooldowns != null && playerCooldowns.isActive(definition)); } + + private class BlockRunner { + private final ProjectileDefinition definition; + private final BlockEntity blockEntity; + private final Player player; + private final Party shooterParty; + private final ProjectileDefinition.BlockEntityType ce; + private final Location currentLocation; + private final Vector increment; + private final Vector substep; + private final int substeps; + private final Future runner; + private int remainingTime; + + public BlockRunner( + ProjectileDefinition definition, + BlockEntity blockEntity, + Player player, + Location spawnLocation) { + this.definition = definition; + this.blockEntity = blockEntity; + this.player = player; + this.shooterParty = Objects.requireNonNull(match.getPlayer(player)).getParty(); + this.ce = (ProjectileDefinition.BlockEntityType) definition.projectile; + + this.currentLocation = spawnLocation.clone(); + var normalizedDirection = currentLocation.getDirection().normalize(); + this.currentLocation.setPitch(0); + this.currentLocation.setYaw(0); + + this.increment = normalizedDirection.multiply(definition.velocity); + this.substeps = Math.max(1, (int) (definition.velocity / ce.size())); + this.substep = increment.clone().divide(new Vector(substeps, substeps, substeps)); + + this.remainingTime = (int) TimeUtils.toTicks(ce.maxTravelTime()); + this.runner = match + .getExecutor(MatchScope.RUNNING) + .scheduleAtFixedRate(this::tick, 0L, 50L, TimeUnit.MILLISECONDS); + } + + public void tick() { + if (remainingTime-- <= 0) { + cancel(); + return; + } + if (definition.damage != null || ce.solidBlockCollision()) { + Location substepLoc = currentLocation.clone(); + for (int i = substeps; i > 0; i--) { + substepLoc.add(substep); + if (!blockDisplayCollision(substepLoc)) continue; + cancel(); + return; + } + } + + currentLocation.add(increment); + blockEntity.teleport(currentLocation); + } + + private void cancel() { + this.runner.cancel(true); + blockEntity.remove(); + } + + private boolean blockDisplayCollision(Location location) { + double radius = 0.5 * ce.size(); + + if (definition.damage != null) { + for (Player victim : location.getNearbyPlayers(radius)) { + var mpVictim = match.getPlayer(victim); + if (MatchPlayers.canInteract(mpVictim) && mpVictim.getParty() != shooterParty) { + victim.damage(definition.damage, player); + return true; + } + } + } + if (ce.solidBlockCollision()) { + int x1 = (int) Math.floor(location.getX() - radius); + int y1 = (int) Math.floor(location.getY() - radius); + int z1 = (int) Math.floor(location.getZ() - radius); + + int x2 = (int) Math.floor(location.getX() + radius); + int y2 = (int) Math.floor(location.getY() + radius); + int z2 = (int) Math.floor(location.getZ() + radius); + + Location loc = location.clone(); + + for (int x = x1; x <= x2; ++x) { + loc.setX(x); + for (int y = y1; y <= y2; ++y) { + loc.setY(y); + for (int z = z1; z <= z2; ++z) { + loc.setZ(z); + if (loc.getBlock().getType().isSolid()) return true; + } + } + } + } + + return false; + } + } } diff --git a/core/src/main/java/tc/oc/pgm/projectile/ProjectileModule.java b/core/src/main/java/tc/oc/pgm/projectile/ProjectileModule.java index ef5ecc5cb7..7390f5097b 100644 --- a/core/src/main/java/tc/oc/pgm/projectile/ProjectileModule.java +++ b/core/src/main/java/tc/oc/pgm/projectile/ProjectileModule.java @@ -6,11 +6,11 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.logging.Logger; import org.bukkit.entity.Arrow; import org.bukkit.entity.Entity; -import org.bukkit.entity.FallingBlock; import org.bukkit.potion.PotionEffect; import org.jdom2.Document; import org.jdom2.Element; @@ -25,6 +25,7 @@ import tc.oc.pgm.util.material.BlockMaterialData; import tc.oc.pgm.util.xml.InvalidXMLException; import tc.oc.pgm.util.xml.Node; +import tc.oc.pgm.util.xml.XMLFluentParser; import tc.oc.pgm.util.xml.XMLUtils; public class ProjectileModule implements MapModule { @@ -62,10 +63,10 @@ public ProjectileModule parse(MapFactory factory, Logger logger, Document doc) Node.fromChildOrAttr(projectileElement, "velocity"), Double.class, 1.0); ClickAction clickAction = XMLUtils.parseEnum( Node.fromAttr(projectileElement, "click"), ClickAction.class, ClickAction.BOTH); - Class entity = - XMLUtils.parseEntityTypeAttribute(projectileElement, "projectile", Arrow.class); - BlockMaterialData blockMaterial = entity.isAssignableFrom(FallingBlock.class) - ? XMLUtils.parseBlockMaterialData(Node.fromAttr(projectileElement, "material")) + ProjectileDefinition.ProjectileEntity entity = + parseProjectileEntity(projectileElement, factory.getParser()); + BlockMaterialData blockMaterial = entity.requiresBlockMaterial() + ? XMLUtils.parseBlockMaterialData(Node.fromRequiredAttr(projectileElement, "material")) : null; Float power = XMLUtils.parseNumber( Node.fromChildOrAttr(projectileElement, "power"), Float.class, (Float) null); @@ -98,5 +99,24 @@ public ProjectileModule parse(MapFactory factory, Logger logger, Document doc) return projectiles.isEmpty() ? null : new ProjectileModule(ImmutableSet.copyOf(projectiles)); } + + private static ProjectileDefinition.ProjectileEntity parseProjectileEntity( + final Element el, final XMLFluentParser parser) throws InvalidXMLException { + final String attributeName = "projectile"; + final Class def = Arrow.class; + final Node node = Node.fromAttr(el, attributeName); + if (node == null) return new ProjectileDefinition.RealEntity(def); + final String entityText = node.getValue(); + return switch (entityText.toLowerCase(Locale.ROOT)) { + case "block" -> + new ProjectileDefinition.BlockEntityType( + parser.parseFloat(el, "size").optional(1.0f), + parser.parseBool(el, "solid-block-collision").orTrue(), + parser.duration(el, "max-travel-time").optional(Duration.ofSeconds(1))); + default -> + new ProjectileDefinition.RealEntity( + XMLUtils.parseEntityTypeAttribute(el, attributeName, def)); + }; + } } } diff --git a/core/src/main/java/tc/oc/pgm/util/xml/XMLFluentParser.java b/core/src/main/java/tc/oc/pgm/util/xml/XMLFluentParser.java index d55129ca70..fdd7875b43 100644 --- a/core/src/main/java/tc/oc/pgm/util/xml/XMLFluentParser.java +++ b/core/src/main/java/tc/oc/pgm/util/xml/XMLFluentParser.java @@ -93,6 +93,10 @@ public NumberBuilder parseDouble(Element el, String... prop) { return number(Double.class, el, prop); } + public NumberBuilder parseFloat(Element el, String... prop) { + return number(Float.class, el, prop); + } + public NumberBuilder number(Class cls, Element el, String... prop) { return new NumberBuilder<>(cls, el, prop); } diff --git a/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/entities/ModernBlockEntity.java b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/entities/ModernBlockEntity.java new file mode 100644 index 0000000000..5ea9e3975b --- /dev/null +++ b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/entities/ModernBlockEntity.java @@ -0,0 +1,56 @@ +package tc.oc.pgm.platform.modern.entities; + +import static tc.oc.pgm.util.platform.Supports.Variant.PAPER; + +import org.bukkit.Location; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.Entity; +import org.bukkit.util.Vector; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import tc.oc.pgm.platform.modern.material.ModernBlockMaterialData; +import tc.oc.pgm.util.bukkit.entities.BlockEntity; +import tc.oc.pgm.util.material.BlockMaterialData; +import tc.oc.pgm.util.platform.Supports; + +@Supports(value = PAPER, minVersion = "1.21.1") +public class ModernBlockEntity implements BlockEntity.Factory { + @Override + public BlockEntity spawnBlockEntity( + Location center, BlockMaterialData blockMaterialData, float size, Vector velocity) { + Location corrected = center.clone(); + corrected.setPitch(0); + corrected.setYaw(0); + final BlockDisplay entity = center.getWorld().spawn(corrected, BlockDisplay.class); + entity.setBlock(((ModernBlockMaterialData) blockMaterialData).getBlock()); + entity.setTeleportDuration(1); + + final Matrix4f translation = + new Matrix4f().translate(new Vector3f(-0.5f * size, -0.5f * size, -0.5f * size)); + + final Matrix4f rotationMatrix = new Matrix4f(); + final Quaternionf rotation = new Quaternionf(); + rotation.rotateLocalX((float) Math.toRadians(-center.getPitch())); + rotation.rotateLocalY((float) Math.toRadians(180 - center.getYaw())); + rotation.get(rotationMatrix); + + final Matrix4f scaleMatrix = new Matrix4f().scale(size); + final Matrix4f transformationMatrix = rotationMatrix.mul(translation.mul(scaleMatrix)); + entity.setTransformationMatrix(transformationMatrix); + + return new Impl(entity); + } + + private record Impl(Entity entity) implements BlockEntity { + + public void teleport(Location center) { + entity.teleport(center); + } + + @Override + public void remove() { + entity.remove(); + } + } +} diff --git a/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/impl/ModernNMSHacks.java b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/impl/ModernNMSHacks.java index 9d4f0b1ba5..522c2a0606 100644 --- a/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/impl/ModernNMSHacks.java +++ b/platform/platform-modern/src/main/java/tc/oc/pgm/platform/modern/impl/ModernNMSHacks.java @@ -223,8 +223,8 @@ public World createWorld(WorldCreator creator) { case NORMAL -> LevelStem.OVERWORLD; case NETHER -> LevelStem.NETHER; case THE_END -> LevelStem.END; - default -> throw new IllegalArgumentException( - "Illegal dimension (" + creator.environment() + ")"); + default -> + throw new IllegalArgumentException("Illegal dimension (" + creator.environment() + ")"); }; LevelStorageSource.LevelStorageAccess worldSession; diff --git a/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/entities/SpBlockEntity.java b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/entities/SpBlockEntity.java new file mode 100644 index 0000000000..922b4a6e17 --- /dev/null +++ b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/entities/SpBlockEntity.java @@ -0,0 +1,59 @@ +package tc.oc.pgm.platform.sportpaper.entities; + +import static tc.oc.pgm.util.platform.Supports.Variant.SPORTPAPER; + +import net.minecraft.server.v1_8_R3.EntityArmorStand; +import org.bukkit.Location; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftArmorStand; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftFallingSand; +import org.bukkit.entity.ArmorStand; +import org.bukkit.util.Vector; +import tc.oc.pgm.util.bukkit.entities.BlockEntity; +import tc.oc.pgm.util.material.BlockMaterialData; +import tc.oc.pgm.util.platform.Supports; + +@Supports(SPORTPAPER) +public class SpBlockEntity implements BlockEntity.Factory { + // We use a falling sand entity, which is 1 block high, so we want to lower actual positon by half + // of that + private static final double OFFSET = 0.5d; + + @Override + public BlockEntity spawnBlockEntity( + Location center, BlockMaterialData blockMaterialData, float size, Vector velocity) { + center.setY(center.getY() - OFFSET); + var fallingBlock = blockMaterialData.spawnFallingBlock(center); + var stand = center.getWorld().spawn(center, ArmorStand.class); + stand.setCustomNameVisible(false); + stand.setVisible(false); + stand.setMarker(true); + stand.setBasePlate(false); + stand.setGravity(false); + stand.setPassenger(fallingBlock); + + // Reset the original location + center.setY(center.getY() + OFFSET); + + var nmsStand = ((CraftArmorStand) stand).getHandle(); + var nmsFallingSand = ((CraftFallingSand) fallingBlock).getHandle(); + // Noclip makes movement ignore all collisions (faster), which we don't need as we run our own + nmsStand.noclip = true; + nmsFallingSand.noclip = true; + + return new Impl(stand, nmsStand); + } + + private record Impl(ArmorStand bukkit, EntityArmorStand nms) implements BlockEntity { + @Override + public void teleport(Location l) { + // We NEED to use NMS move because bukkit's teleport does not work on entities with passengers + nms.move(l.getX() - nms.locX, l.getY() - nms.locY - OFFSET, l.getZ() - nms.locZ); + } + + @Override + public void remove() { + bukkit.getPassenger().remove(); + bukkit.remove(); + } + } +} diff --git a/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/impl/SpNMSHacks.java b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/impl/SpNMSHacks.java index a4fee8c33d..94544d7d52 100644 --- a/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/impl/SpNMSHacks.java +++ b/platform/platform-sportpaper/src/main/java/tc/oc/pgm/platform/sportpaper/impl/SpNMSHacks.java @@ -18,11 +18,7 @@ import net.minecraft.server.v1_8_R3.ServerNBTManager; import net.minecraft.server.v1_8_R3.WorldData; import net.minecraft.server.v1_8_R3.WorldServer; -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.WorldCreator; +import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.craftbukkit.v1_8_R3.CraftChunk; import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; diff --git a/util/src/main/java/tc/oc/pgm/util/bukkit/entities/BlockEntity.java b/util/src/main/java/tc/oc/pgm/util/bukkit/entities/BlockEntity.java new file mode 100644 index 0000000000..617d83d2ef --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/bukkit/entities/BlockEntity.java @@ -0,0 +1,30 @@ +package tc.oc.pgm.util.bukkit.entities; + +import org.bukkit.Location; +import org.bukkit.util.Vector; +import tc.oc.pgm.util.material.BlockMaterialData; +import tc.oc.pgm.util.platform.Platform; + +/** + * Implements a wrapper for block-like entities, used for projectiles. Modern implementations will + * use display entities, legacy (1.8) will use falling sand.
+ * + * @implNote For this class, location is actually the center of the entity + */ +public interface BlockEntity { + BlockEntity.Factory FACTORY = Platform.get(BlockEntity.Factory.class); + + static BlockEntity spawnBlockEntity( + Location center, BlockMaterialData blockMaterialData, float size, Vector velocity) { + return FACTORY.spawnBlockEntity(center, blockMaterialData, size, velocity); + } + + void teleport(Location center); + + void remove(); + + interface Factory { + BlockEntity spawnBlockEntity( + Location center, BlockMaterialData blockMaterialData, float size, Vector velocity); + } +}