diff --git a/paper-api/src/main/java/org/bukkit/block/BlockState.java b/paper-api/src/main/java/org/bukkit/block/BlockState.java
index 631cbf2be..7c5438fa9 100644
--- a/paper-api/src/main/java/org/bukkit/block/BlockState.java
+++ b/paper-api/src/main/java/org/bukkit/block/BlockState.java
@@ -7,6 +7,7 @@ import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.material.MaterialData;
import org.bukkit.metadata.Metadatable;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -47,6 +48,15 @@ public interface BlockState extends Metadatable {
@NotNull
BlockData getBlockData();
+ /**
+ * Returns a copy of this BlockState as an unplaced BlockState.
+ *
+ * @return a copy of the block state
+ */
+ @NotNull
+ @ApiStatus.Experimental
+ BlockState copy();
+
/**
* Gets the type of this block state.
*
diff --git a/paper-api/src/main/java/org/bukkit/event/world/AsyncStructureGenerateEvent.java b/paper-api/src/main/java/org/bukkit/event/world/AsyncStructureGenerateEvent.java
new file mode 100644
index 000000000..91a137379
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/event/world/AsyncStructureGenerateEvent.java
@@ -0,0 +1,229 @@
+package org.bukkit.event.world;
+
+import com.google.common.base.Preconditions;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.bukkit.NamespacedKey;
+import org.bukkit.World;
+import org.bukkit.event.HandlerList;
+import org.bukkit.generator.structure.Structure;
+import org.bukkit.util.BlockTransformer;
+import org.bukkit.util.BoundingBox;
+import org.bukkit.util.EntityTransformer;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This event will sometimes fire synchronously, depending on how it was
+ * triggered.
+ *
+ * The constructor provides a boolean to indicate if the event was fired
+ * synchronously or asynchronously. When asynchronous, this event can be called
+ * from any thread, sans the main thread, and has limited access to the API.
+ *
+ * If a {@link Structure} is naturally placed in a chunk of the world, this
+ * event will be asynchronous. If a player executes the '/place structure'
+ * command, this event will be synchronous.
+ *
+ * Allows to register transformers that can modify the blocks placed and
+ * entities spawned by the structure.
+ *
+ * Care should be taken to check {@link #isAsynchronous()} and treat the event
+ * appropriately.
+ *
+ */
+@ApiStatus.Experimental
+public class AsyncStructureGenerateEvent extends WorldEvent {
+
+ public static enum Cause {
+ COMMAND,
+ WORLD_GENERATION,
+ CUSTOM;
+ }
+
+ private static final HandlerList handlers = new HandlerList();
+
+ private final Cause cause;
+
+ private final Structure structure;
+ private final BoundingBox boundingBox;
+
+ private final int chunkX, chunkZ;
+
+ private final Map blockTransformers = new LinkedHashMap<>();
+ private final Map entityTransformers = new LinkedHashMap<>();
+
+ public AsyncStructureGenerateEvent(@NotNull World world, boolean async, @NotNull Cause cause, @NotNull Structure structure, @NotNull BoundingBox boundingBox, int chunkX, int chunkZ) {
+ super(world, async);
+ this.structure = structure;
+ this.boundingBox = boundingBox;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.cause = cause;
+ }
+
+ /**
+ * Gets the event cause.
+ *
+ * @return the event cause
+ */
+ @NotNull
+ public Cause getCause() {
+ return cause;
+ }
+
+ /**
+ * Gets a block transformer by key.
+ *
+ * @param key the key of the block transformer
+ *
+ * @return the block transformer or null
+ */
+ @Nullable
+ public BlockTransformer getBlockTransformer(@NotNull NamespacedKey key) {
+ Preconditions.checkNotNull(key, "NamespacedKey cannot be null");
+ return blockTransformers.get(key);
+ }
+
+ /**
+ * Sets a block transformer to a key.
+ *
+ * @param key the key
+ * @param transformer the block transformer
+ */
+ public void setBlockTransformer(@NotNull NamespacedKey key, @NotNull BlockTransformer transformer) {
+ Preconditions.checkNotNull(key, "NamespacedKey cannot be null");
+ Preconditions.checkNotNull(transformer, "BlockTransformer cannot be null");
+ blockTransformers.put(key, transformer);
+ }
+
+ /**
+ * Removes a block transformer.
+ *
+ * @param key the key of the block transformer
+ */
+ public void removeBlockTransformer(@NotNull NamespacedKey key) {
+ Preconditions.checkNotNull(key, "NamespacedKey cannot be null");
+ blockTransformers.remove(key);
+ }
+
+ /**
+ * Removes all block transformers.
+ */
+ public void clearBlockTransformers() {
+ blockTransformers.clear();
+ }
+
+ /**
+ * Gets all block transformers in a unmodifiable map.
+ *
+ * @return the block transformers in a map
+ */
+ @NotNull
+ public Map getBlockTransformers() {
+ return Collections.unmodifiableMap(blockTransformers);
+ }
+
+ /**
+ * Gets a entity transformer by key.
+ *
+ * @param key the key of the entity transformer
+ *
+ * @return the entity transformer or null
+ */
+ @Nullable
+ public EntityTransformer getEntityTransformer(@NotNull NamespacedKey key) {
+ Preconditions.checkNotNull(key, "NamespacedKey cannot be null");
+ return entityTransformers.get(key);
+ }
+
+ /**
+ * Sets a entity transformer to a key.
+ *
+ * @param key the key
+ * @param transformer the entity transformer
+ */
+ public void setEntityTransformer(@NotNull NamespacedKey key, @NotNull EntityTransformer transformer) {
+ Preconditions.checkNotNull(key, "NamespacedKey cannot be null");
+ Preconditions.checkNotNull(transformer, "EntityTransformer cannot be null");
+ entityTransformers.put(key, transformer);
+ }
+
+ /**
+ * Removes a entity transformer.
+ *
+ * @param key the key of the entity transformer
+ */
+ public void removeEntityTransformer(@NotNull NamespacedKey key) {
+ Preconditions.checkNotNull(key, "NamespacedKey cannot be null");
+ entityTransformers.remove(key);
+ }
+
+ /**
+ * Removes all entity transformers.
+ */
+ public void clearEntityTransformers() {
+ entityTransformers.clear();
+ }
+
+ /**
+ * Gets all entity transformers in a unmodifiable map.
+ *
+ * @return the entity transformers in a map
+ */
+ @NotNull
+ public Map getEntityTransformers() {
+ return Collections.unmodifiableMap(entityTransformers);
+ }
+
+ /**
+ * Get the structure reference that is generated.
+ *
+ * @return the structure
+ */
+ @NotNull
+ public Structure getStructure() {
+ return structure;
+ }
+
+ /**
+ * Get the bounding box of the structure.
+ *
+ * @return the bounding box
+ */
+ @NotNull
+ public BoundingBox getBoundingBox() {
+ return boundingBox.clone();
+ }
+
+ /**
+ * Get the x coordinate of the origin chunk of the structure.
+ *
+ * @return the chunk x coordinate
+ */
+ public int getChunkX() {
+ return chunkX;
+ }
+
+ /**
+ * Get the z coordinate of the origin chunk of the structure.
+ *
+ * @return the chunk z coordinate
+ */
+ public int getChunkZ() {
+ return chunkZ;
+ }
+
+ @NotNull
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ @NotNull
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/paper-api/src/main/java/org/bukkit/util/BlockTransformer.java b/paper-api/src/main/java/org/bukkit/util/BlockTransformer.java
new file mode 100644
index 000000000..7f430519c
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/util/BlockTransformer.java
@@ -0,0 +1,62 @@
+package org.bukkit.util;
+
+import org.bukkit.block.BlockState;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.generator.LimitedRegion;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A BlockTransformer is used to modify blocks that are placed by structure.
+ */
+@FunctionalInterface
+@ApiStatus.Experimental
+public interface BlockTransformer {
+
+ /**
+ * The TransformationState allows access to the original block state and the
+ * block state of the block that was at the location of the transformation
+ * in the world before the transformation started.
+ */
+ public static interface TransformationState {
+
+ /**
+ * Creates a clone of the original block state that a structure wanted
+ * to place and caches it for the current transformer.
+ *
+ * @return a clone of the original block state.
+ */
+ @NotNull
+ BlockState getOriginal();
+
+ /**
+ * Creates a clone of the block state that was at the location of the
+ * currently modified block at the start of the transformation process
+ * and caches it for the current transformer.
+ *
+ * @return a clone of the world block state.
+ */
+ @NotNull
+ BlockState getWorld();
+
+ }
+
+ /**
+ * Transforms a block in a structure.
+ *
+ * NOTE: The usage of {@link BlockData#createBlockState()} can provide even
+ * more flexibility to return the exact block state you might want to
+ * return.
+ *
+ * @param region the accessible region
+ * @param x the x position of the block
+ * @param y the y position of the block
+ * @param z the z position of the block
+ * @param current the state of the block that should be placed
+ * @param state the state of this transformation.
+ *
+ * @return the new block state
+ */
+ @NotNull
+ BlockState transform(@NotNull LimitedRegion region, int x, int y, int z, @NotNull BlockState current, @NotNull TransformationState state);
+}
diff --git a/paper-api/src/main/java/org/bukkit/util/EntityTransformer.java b/paper-api/src/main/java/org/bukkit/util/EntityTransformer.java
new file mode 100644
index 000000000..d1f44fd90
--- /dev/null
+++ b/paper-api/src/main/java/org/bukkit/util/EntityTransformer.java
@@ -0,0 +1,29 @@
+package org.bukkit.util;
+
+import org.bukkit.entity.Entity;
+import org.bukkit.generator.LimitedRegion;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A EntityTransformer is used to modify entities that are spawned by structure.
+ */
+@FunctionalInterface
+@ApiStatus.Experimental
+public interface EntityTransformer {
+
+ /**
+ * Transforms a entity in a structure.
+ *
+ * @param region the accessible region
+ * @param x the x position of the entity
+ * @param y the y position of the entity
+ * @param z the z position of the entity
+ * @param entity the entity
+ * @param allowedToSpawn if the entity is allowed to spawn
+ *
+ * @return {@code true} if the entity should be spawned otherwise
+ * {@code false}
+ */
+ boolean transform(@NotNull LimitedRegion region, int x, int y, int z, @NotNull Entity entity, boolean allowedToSpawn);
+}