diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
index ba0a66a1f41..c8142acb5fd 100644
--- a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
+++ b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
@@ -38,6 +38,7 @@
import org.bukkit.GameRule;
import org.bukkit.Location;
import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
import org.bukkit.OfflinePlayer;
import org.bukkit.Registry;
import org.bukkit.SoundCategory;
@@ -49,6 +50,8 @@
import org.bukkit.block.BlockState;
import org.bukkit.block.DoubleChest;
import org.bukkit.block.data.BlockData;
+import org.bukkit.block.structure.Mirror;
+import org.bukkit.block.structure.StructureRotation;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.enchantments.EnchantmentOffer;
@@ -77,6 +80,7 @@
import org.bukkit.metadata.Metadatable;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
+import org.bukkit.structure.Structure;
import org.bukkit.util.CachedServerIcon;
import org.bukkit.util.Vector;
@@ -102,6 +106,7 @@
import ch.njol.skript.util.BlockUtils;
import ch.njol.skript.util.PotionEffectUtils;
import ch.njol.skript.util.StringMode;
+import ch.njol.skript.util.Utils;
import ch.njol.util.StringUtils;
import ch.njol.yggdrasil.Fields;
import io.papermc.paper.world.MoonPhase;
@@ -1526,6 +1531,60 @@ public String toVariableNameString(EnchantmentOffer eo) {
.name("Transform Reason")
.description("Represents a transform reason of an entity transform event.")
.since("2.8.0"));
+
+ if (Skript.classExists("org.bukkit.structure.Structure")) {
+ Classes.registerClass(new ClassInfo<>(Structure.class, "structure")
+ .user("structures?")
+ .name("Structure")
+ .description("Represents a structure in a namespace.")
+ .defaultExpression(new EventValueExpression<>(Structure.class))
+ .requiredPlugins("Minecraft 1.17.1")
+ .since("INSERT VERSION")
+ .parser(new Parser() {
+ @Override
+ @Nullable
+ public Structure parse(String input, ParseContext context) {
+ NamespacedKey key = Utils.getNamespacedKey(input);
+ if (key == null)
+ return null;
+ return Bukkit.getStructureManager().loadStructure(key, false);
+ }
+
+ @Override
+ public boolean canParse(ParseContext context) {
+ return context != ParseContext.CONFIG;
+ }
+
+ @Override
+ public String toString(Structure structure, int flags) {
+ return Bukkit.getStructureManager().getStructures().entrySet().stream()
+ .filter(entry -> entry.getValue().equals(structure))
+ .map(entry -> "structure " + entry.getKey().asString())
+ .findFirst()
+ .orElse("structure " + structure.toString());
+ }
+
+ @Override
+ public String toVariableNameString(Structure structure) {
+ return toString(structure, 0);
+ }
+ }));
+
+ Classes.registerClass(new EnumClassInfo<>(StructureRotation.class, "structurerotation", "structure rotations")
+ .user("structure ?rotations?")
+ .name("Structure Rotations")
+ .description("Represents a rotation a structure can be rotated as when placed.")
+ .requiredPlugins("Minecraft 1.17.1")
+ .since("INSERT VERSION"));
+
+ Classes.registerClass(new EnumClassInfo<>(Mirror.class, "mirror", "mirrors")
+ .user("mirrors?")
+ .name("mirror")
+ .description("Represents a mirror setting a structure can be when placed.")
+ .requiredPlugins("Minecraft 1.17.1")
+ .since("INSERT VERSION"));
+ }
+
}
}
diff --git a/src/main/java/ch/njol/skript/conditions/CondStructureExists.java b/src/main/java/ch/njol/skript/conditions/CondStructureExists.java
new file mode 100644
index 00000000000..5cef42823d8
--- /dev/null
+++ b/src/main/java/ch/njol/skript/conditions/CondStructureExists.java
@@ -0,0 +1,80 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.conditions;
+
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.structure.StructureManager;
+import org.jetbrains.annotations.Nullable;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Condition;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.util.Utils;
+import ch.njol.util.Kleenean;
+
+@Name("Structure Exists")
+@Description("Check if structures exist.")
+@Examples("if structure named \"Example\" does exist")
+@RequiredPlugins("Minecraft 1.17.1+")
+@Since("INSERT VERSION")
+public class CondStructureExists extends Condition {
+
+ static {
+ if (Skript.classExists("org.bukkit.structure.Structure"))
+ Skript.registerCondition(CondStructureExists.class,
+ "structure[s] [named] %strings% [do[es]] exist[s]",
+ "structure[s] [named] %strings% (do[es]n't|do[es] not) exist"
+ );
+ }
+
+ private Expression names;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ names = (Expression) exprs[0];
+ setNegated(matchedPattern == 1);
+ return true;
+ }
+
+ @Override
+ public boolean check(Event event) {
+ StructureManager manager = Bukkit.getStructureManager();
+ return names.check(event, name -> {
+ NamespacedKey key = Utils.getNamespacedKey(name);
+ if (key == null)
+ return false;
+ return manager.loadStructure(key, false) != null;
+ }, isNegated());
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return "structures " + names.toString(event, debug) + (names.isSingle() ? " does " : " do ") + (isNegated() ? "not " : "") + " exist";
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/effects/EffStructureSaveUnregister.java b/src/main/java/ch/njol/skript/effects/EffStructureSaveUnregister.java
new file mode 100644
index 00000000000..70c271b3e8c
--- /dev/null
+++ b/src/main/java/ch/njol/skript/effects/EffStructureSaveUnregister.java
@@ -0,0 +1,103 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.effects;
+
+import java.io.IOException;
+
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.structure.Structure;
+import org.bukkit.structure.StructureManager;
+import org.jetbrains.annotations.Nullable;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Effect;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.util.Utils;
+import ch.njol.util.Kleenean;
+
+@Name("Structure Save/Unregister")
+@Description("Unregisters or saves a structure by it's namespace key. Unregister will unload if the structure was loaded vs Delete.")
+@Examples("unregister structure named \"Example\"")
+@RequiredPlugins("Spigot 1.17.1+")
+@Since("INSERT VERSION")
+public class EffStructureSaveUnregister extends Effect {
+
+ static {
+ if (Skript.classExists("org.bukkit.structure.Structure"))
+ Skript.registerEffect(EffStructureSaveUnregister.class, "(:unregister|delete) structure[s] [with name|named] %strings%", "save [structure] %structure% [with name|named] %string%");
+ }
+
+ private Expression structure;
+ private Expression names;
+ private boolean save, unregister;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ names = (Expression) exprs[matchedPattern];
+ unregister = parseResult.hasTag("unregister");
+ if (save = matchedPattern == 1)
+ structure = (Expression) exprs[0];
+ return true;
+ }
+
+ @Override
+ protected void execute(Event event) {
+ StructureManager manager = Bukkit.getStructureManager();
+ if (save) {
+ String name = this.names.getSingle(event);
+ if (name == null)
+ return;
+ Structure structure = this.structure.getSingle(event);
+ if (structure == null)
+ return;
+ try {
+ manager.saveStructure(Utils.getNamespacedKey(name), structure);
+ } catch (IOException e) {
+ Skript.exception(e, "Failed to save structure " + name);
+ }
+ } else {
+ for (String name : names.getArray(event)) {
+ NamespacedKey key = Utils.getNamespacedKey(name);
+ if (key == null)
+ continue;
+ try {
+ manager.deleteStructure(key, unregister);
+ } catch (IOException e) {
+ Skript.exception(e, "Failed to delete structure " + name);
+ }
+ }
+
+ }
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return (save ? "save" : "delete") + " structures " + names.toString(event, debug);
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/expressions/ExprStructure.java b/src/main/java/ch/njol/skript/expressions/ExprStructure.java
new file mode 100644
index 00000000000..dd50569111b
--- /dev/null
+++ b/src/main/java/ch/njol/skript/expressions/ExprStructure.java
@@ -0,0 +1,139 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.expressions;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.event.Event;
+import org.bukkit.structure.Structure;
+import org.bukkit.structure.StructureManager;
+import org.bukkit.util.BlockVector;
+import org.jetbrains.annotations.Nullable;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.ExpressionType;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.skript.util.BlockUtils;
+import ch.njol.skript.util.Utils;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+
+@Name("Structure Between/Name")
+@Description({
+ "A structure is a utility that allows you to save a cuboid of blocks and entities.",
+ "This syntax will return an existing structure from memory/datapacks or you can also create a structure between two locations.",
+ "The register tag adds the structure to the 'all structures' expression.",
+ "If the name contains a collon, it'll grab from the Minecraft structure space (Data packs included for namespaces).",
+})
+@Examples({
+ "set {_structure} to a new structure between {location1} and {location2} named \"Example\"",
+ "set {_structure} to structure \"Example\"",
+ "set {_structure} to structure \"minecraft:end_city\""
+})
+@RequiredPlugins("Minecraft 1.17.1+")
+@Since("INSERT VERSION")
+public class ExprStructure extends SimpleExpression {
+
+ static {
+ if (Skript.classExists("org.bukkit.structure.Structure"))
+ Skript.registerExpression(ExprStructure.class, Structure.class, ExpressionType.COMBINED,
+ "structure[s] [named] %strings% [register:and don't register]",
+ "[a] [new] structure between %location% (and|to) %location% [(including|with) entities:entities]"
+ );
+ }
+
+ @Nullable
+ private Expression location1, location2;
+
+ @Nullable
+ private Expression names;
+ private boolean entities, register = true;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ if (matchedPattern == 0) {
+ names = (Expression) exprs[0];
+ return true;
+ }
+ entities = parseResult.hasTag("entities");
+ register = !parseResult.hasTag("register");
+ location1 = (Expression) exprs[0];
+ location2 = (Expression) exprs[1];
+ return true;
+ }
+
+ @Override
+ protected Structure[] get(Event event) {
+ StructureManager manager = Bukkit.getStructureManager();
+
+ // Returning existing structures.
+ if (names != null) {
+ return names.stream(event)
+ .map(name -> Utils.getNamespacedKey(name))
+ .map(name -> name != null ? manager.loadStructure(name, register) : null)
+ .toArray(Structure[]::new);
+ }
+ Location location1 = this.location1.getSingle(event);
+ Location location2 = this.location2.getSingle(event);
+ if (location1 == null || location2 == null)
+ return new Structure[0];
+
+ World world1 = location1.getWorld();
+ World world2 = location2.getWorld();
+ if (world1 != world2)
+ return new Structure[0];
+
+ Location lowest = BlockUtils.getLowestBlockLocation(location1, location2);
+ Location highest = BlockUtils.getHighestBlockLocation(location1, location2);
+ int x = (highest.getBlockX() + 1) - lowest.getBlockX();
+ int y = (highest.getBlockY() + 1) - lowest.getBlockY();
+ int z = (highest.getBlockZ() + 1) - lowest.getBlockZ();
+
+ Structure structure = manager.createStructure();
+ structure.fill(lowest, new BlockVector(x, y, z), entities);
+ return CollectionUtils.array(structure);
+ }
+
+ @Override
+ public boolean isSingle() {
+ return names == null || names.isSingle();
+ }
+
+ @Override
+ public Class extends Structure> getReturnType() {
+ return Structure.class;
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ if (location1 == null || location2 == null)
+ return "structures " + names.toString(event, debug);
+ return "structure from " + location1.toString(event, debug) + " to " + location2.toString(event, debug);
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/expressions/ExprStructureInfo.java b/src/main/java/ch/njol/skript/expressions/ExprStructureInfo.java
new file mode 100644
index 00000000000..1931badf44b
--- /dev/null
+++ b/src/main/java/ch/njol/skript/expressions/ExprStructureInfo.java
@@ -0,0 +1,156 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.expressions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.block.BlockState;
+import org.bukkit.entity.Entity;
+import org.bukkit.event.Event;
+import org.bukkit.structure.Structure;
+import org.bukkit.util.BlockVector;
+import org.jetbrains.annotations.Nullable;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.PropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.util.BlockStateBlock;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.iterator.ArrayIterator;
+
+@Name("Structure Info")
+@Description("An expression to obtain information about a structure's entities, size, and blocks.")
+@Examples({
+ "loop all entities of structure {_structure}:",
+ "\tif loop-entity is a diamond:",
+ "\t\tmessage \"Race to the diamond at %loop-entity's location%\" to {_players::*}",
+ "\t\tstop",
+ "if the length of {_structure}'s vector is greater than 100:",
+ "\tmessage \"&a+50 coins will be granted for winning on this larger map!\""
+})
+@RequiredPlugins("Minecraft 1.17.1+")
+@Since("INSERT VERSION")
+public class ExprStructureInfo extends PropertyExpression {
+
+ static {
+ if (Skript.classExists("org.bukkit.structure.Structure"))
+ register(ExprStructureInfo.class, Object.class, "[structure] (:blocks|:entities|name:name[s]|size:[vector] size[s])", "structures");
+ }
+
+ private enum Property {
+ BLOCKS(BlockStateBlock.class),
+ SIZE(BlockVector.class),
+ ENTITIES(Entity.class),
+ NAME(String.class);
+
+ private final Class extends Object> returnType;
+
+ Property(Class extends Object> returnType) {
+ this.returnType = returnType;
+ }
+
+ public Class extends Object> getReturnType() {
+ return returnType;
+ }
+ }
+
+ @Nullable
+ private Expression name;
+ private Property property;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ property = Property.valueOf(parseResult.tags.get(0).toUpperCase(Locale.ENGLISH));
+ setExpr((Expression extends Structure>) exprs[0]);
+ return true;
+ }
+
+ @Override
+ protected Object[] get(Event event, Structure[] source) {
+ switch (property) {
+ case BLOCKS:
+ return Arrays.stream(source).flatMap(structure -> {
+ if (structure.getPaletteCount() > 0)
+ return structure.getPalettes().get(0).getBlocks().stream()
+ .map(state -> new BlockStateBlock(state, true));
+ return null;
+ })
+ .filter(Objects::nonNull)
+ .toArray(BlockStateBlock[]::new);
+ case ENTITIES:
+ return get(source, structure -> structure.getEntities().toArray(Entity[]::new));
+ case SIZE:
+ return get(source, Structure::getSize);
+ case NAME:
+ Map structures = Bukkit.getStructureManager().getStructures();
+ return get(source, structure -> structures.entrySet().stream()
+ .filter(entry -> entry.getValue().equals(structure))
+ .map(entry -> "structure " + entry.getKey().asString())
+ .findFirst()
+ .orElse("structure " + structure.toString()));
+ }
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Iterator