Implement structure storage format (to be moved to Gaea in the future)

This commit is contained in:
dfsek 2020-09-22 20:52:11 -07:00
parent 53a82470da
commit aa326d95e9
14 changed files with 400 additions and 106 deletions

View File

@ -11,6 +11,11 @@
<option name="name" value="aikar" />
<option name="url" value="https://repo.aikar.co/content/groups/aikar/" />
</remote-repository>
<remote-repository>
<option name="id" value="enginehub-maven" />
<option name="name" value="enginehub-maven" />
<option name="url" value="http://maven.enginehub.org/repo/" />
</remote-repository>
<remote-repository>
<option name="id" value="spigotmc-repo" />
<option name="name" value="spigotmc-repo" />
@ -26,6 +31,11 @@
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="enginehub" />
<option name="name" value="enginehub" />
<option name="url" value="https://maven.enginehub.org/repo/" />
</remote-repository>
<remote-repository>
<option name="id" value="minecraft-repo" />
<option name="name" value="minecraft-repo" />

10
pom.xml
View File

@ -68,6 +68,10 @@
<name>gaea</name>
<url>file:/home/dfsek/Documents/Gaea/repo</url>
</repository>
<repository>
<id>enginehub-maven</id>
<url>http://maven.enginehub.org/repo/</url>
</repository>
</repositories>
<dependencies>
@ -110,6 +114,12 @@
<artifactId>commons-imaging</artifactId>
<version>1.0-alpha2</version>
</dependency>
<dependency>
<groupId>com.sk89q.worldedit</groupId>
<artifactId>worldedit-bukkit</artifactId>
<version>7.2.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -2,23 +2,29 @@ package com.dfsek.terra;
import com.dfsek.terra.biome.TerraBiomeGrid;
import com.dfsek.terra.biome.UserDefinedBiome;
import com.dfsek.terra.config.ConfigUtil;
import com.dfsek.terra.config.WorldConfig;
import com.dfsek.terra.config.genconfig.BiomeConfig;
import com.dfsek.terra.config.ConfigUtil;
import com.dfsek.terra.config.genconfig.OreConfig;
import com.dfsek.terra.image.WorldImageGenerator;
import com.dfsek.terra.structure.GaeaStructure;
import com.sk89q.worldedit.IncompleteRegionException;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.Region;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.polydev.gaea.profiler.WorldProfiler;
import java.io.File;
import java.util.Arrays;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Random;
@ -26,90 +32,148 @@ import java.util.Random;
public class TerraCommand implements CommandExecutor, TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
switch(args[0]) {
case "reload":
ConfigUtil.loadConfig(Terra.getInstance());
sender.sendMessage("Reloaded Terra config.");
return true;
case "biome":
if(!(sender instanceof Player)) return false;
sender.sendMessage("You are in " + BiomeConfig.fromBiome((UserDefinedBiome) TerraBiomeGrid.fromWorld(((Player) sender).getWorld()).getBiome(((Player) sender).getLocation())).getFriendlyName());
return true;
case "profile":
if(! (sender instanceof Player)) {
sender.sendMessage("Command is for players only.");
try {
switch(args[0]) {
case "reload":
ConfigUtil.loadConfig(Terra.getInstance());
sender.sendMessage("Reloaded Terra config.");
return true;
}
Player p = (Player) sender;
if(p.getWorld().getGenerator() instanceof TerraChunkGenerator) {
WorldProfiler profile = TerraProfiler.fromWorld(p.getWorld());
if(args.length > 1 && "query".equals(args[1])) {
sender.sendMessage(profile.getResultsFormatted());
return true;
} else if(args.length > 1 && "reset".equals(args[1])) {
profile.reset();
sender.sendMessage("Profiler has been reset.");
return true;
} else if(args.length > 1 && "start".equals(args[1])) {
profile.setProfiling(true);
sender.sendMessage("Profiler has started.");
return true;
} else if(args.length > 1 && "stop".equals(args[1])) {
profile.setProfiling(false);
sender.sendMessage("Profiler has stopped.");
return true;
}
} else sender.sendMessage("World is not a Terra world!");
break;
case "ore":
if(! (sender instanceof Player)) {
sender.sendMessage("Command is for players only.");
case "biome":
if(! (sender instanceof Player)) return false;
sender.sendMessage("You are in " + BiomeConfig.fromBiome((UserDefinedBiome) TerraBiomeGrid.fromWorld(((Player) sender).getWorld()).getBiome(((Player) sender).getLocation())).getFriendlyName());
return true;
}
Block bl = ((Player) sender).getTargetBlockExact(25);
OreConfig ore = OreConfig.fromID(args[1]);
if(ore == null) {
sender.sendMessage("Unable to find Ore");
return true;
}
ore.doVein(bl.getLocation(), new Random());
return true;
case "image":
if("render".equals(args[1])) {
case "profile":
if(! (sender instanceof Player)) {
sender.sendMessage("Command is for players only.");
return true;
}
Player pl = (Player) sender;
if(args.length != 4) return false;
try {
WorldImageGenerator g = new WorldImageGenerator(pl.getWorld(), Integer.parseInt(args[2]), Integer.parseInt(args[3]));
g.drawWorld(pl.getLocation().getBlockX(), pl.getLocation().getBlockZ());
File file = new File(Terra.getInstance().getDataFolder() + File.separator + "map_export" + File.separator + "map_" + System.currentTimeMillis() + ".png");
file.mkdirs();
file.createNewFile();
g.save(file);
sender.sendMessage("Saved image to " + file.getPath());
return true;
} catch(Exception e) {
e.printStackTrace();
return false;
}
} else if("gui".equals(args[1])) {
Player p = (Player) sender;
if(p.getWorld().getGenerator() instanceof TerraChunkGenerator) {
WorldProfiler profile = TerraProfiler.fromWorld(p.getWorld());
if(args.length > 1 && "query".equals(args[1])) {
sender.sendMessage(profile.getResultsFormatted());
return true;
} else if(args.length > 1 && "reset".equals(args[1])) {
profile.reset();
sender.sendMessage("Profiler has been reset.");
return true;
} else if(args.length > 1 && "start".equals(args[1])) {
profile.setProfiling(true);
sender.sendMessage("Profiler has started.");
return true;
} else if(args.length > 1 && "stop".equals(args[1])) {
profile.setProfiling(false);
sender.sendMessage("Profiler has stopped.");
return true;
}
} else sender.sendMessage("World is not a Terra world!");
break;
case "ore":
if(! (sender instanceof Player)) {
sender.sendMessage("Command is for players only.");
return true;
}
Player pl = (Player) sender;
try {
if("raw".equals(args[2])) WorldConfig.fromWorld(pl.getWorld()).imageLoader.debug(false, pl.getWorld());
else if("step".equals(args[2])) WorldConfig.fromWorld(pl.getWorld()).imageLoader.debug(true, pl.getWorld());
else return false;
Block bl = ((Player) sender).getTargetBlockExact(25);
OreConfig ore = OreConfig.fromID(args[1]);
if(ore == null) {
sender.sendMessage("Unable to find Ore");
return true;
} catch(NullPointerException e) {
return false;
}
}
ore.doVein(bl.getLocation(), new Random());
return true;
case "image":
if("render".equals(args[1])) {
if(! (sender instanceof Player)) {
sender.sendMessage("Command is for players only.");
return true;
}
Player pl = (Player) sender;
if(args.length != 4) return false;
try {
WorldImageGenerator g = new WorldImageGenerator(pl.getWorld(), Integer.parseInt(args[2]), Integer.parseInt(args[3]));
g.drawWorld(pl.getLocation().getBlockX(), pl.getLocation().getBlockZ());
File file = new File(Terra.getInstance().getDataFolder() + File.separator + "export" + File.separator + "map" + File.separator + "map_" + System.currentTimeMillis() + ".png");
file.mkdirs();
file.createNewFile();
g.save(file);
sender.sendMessage("Saved image to " + file.getPath());
return true;
} catch(Exception e) {
e.printStackTrace();
return false;
}
} else if("gui".equals(args[1])) {
if(! (sender instanceof Player)) {
sender.sendMessage("Command is for players only.");
return true;
}
Player pl = (Player) sender;
try {
if("raw".equals(args[2]))
WorldConfig.fromWorld(pl.getWorld()).imageLoader.debug(false, pl.getWorld());
else if("step".equals(args[2]))
WorldConfig.fromWorld(pl.getWorld()).imageLoader.debug(true, pl.getWorld());
else return false;
return true;
} catch(NullPointerException e) {
return false;
}
}
break;
case "structure":
if(! (sender instanceof Player)) {
sender.sendMessage("Command is for players only.");
return true;
}
Player pl = (Player) sender;
if("export".equals(args[1])) {
WorldEditPlugin we = WorldEditUtil.getWorldEdit();
if(we == null) {
sender.sendMessage("WorldEdit is not installed! Please install WorldEdit before attempting to export structures.");
return true;
}
Region selection;
try {
selection = we.getSession(pl).getSelection(BukkitAdapter.adapt(pl.getWorld()));
} catch(IncompleteRegionException e) {
sender.sendMessage("Invalid/incomplete selection!");
return true;
}
BukkitAdapter.adapt(pl);
if(selection == null) {
sender.sendMessage("Please make a selection before attempting to export!");
return true;
}
BlockVector3 min = selection.getMinimumPoint();
BlockVector3 max = selection.getMaximumPoint();
Location l1 = new Location(pl.getWorld(), min.getBlockX(), min.getBlockY(), min.getBlockZ());
Location l2 = new Location(pl.getWorld(), max.getBlockX(), max.getBlockY(), max.getBlockZ());
GaeaStructure structure = new GaeaStructure(l1, l2);
try {
File file = new File(Terra.getInstance().getDataFolder() + File.separator + "export" + File.separator + "structures", args[2] + ".tstructure");
file.getParentFile().mkdirs();
file.createNewFile();
structure.save(file);
sender.sendMessage("Saved to " + file.getPath());
} catch(IOException e) {
e.printStackTrace();
}
return true;
} else if("load".equals(args[1])) {
try {
GaeaStructure struc = GaeaStructure.load(new File(Terra.getInstance().getDataFolder() + File.separator + "export" + File.separator + "structures", args[2] + ".tstructure"));
if("true".equals(args[3])) struc.paste(pl.getLocation());
else struc.paste(pl.getLocation(), pl.getLocation().getChunk());
} catch(IOException e) {
e.printStackTrace();
sender.sendMessage("Structure not found.");
}
return true;
}
}
} catch(ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
return false;
}

View File

@ -0,0 +1,14 @@
package com.dfsek.terra;
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
public class WorldEditUtil {
public static WorldEditPlugin getWorldEdit() {
Plugin p = Bukkit.getServer().getPluginManager().getPlugin("WorldEdit");
if (p instanceof WorldEditPlugin) return (WorldEditPlugin) p;
Bukkit.getLogger().severe("[Terra] a command requiring WorldEdit was executed, but WorldEdit was not detected!");
return null;
}
}

View File

@ -20,7 +20,7 @@ public class ConfigLoader {
}
public <T extends TerraConfigObject> void load(JavaPlugin main, Class<T> clazz) {
File folder = new File(main.getDataFolder() + File.separator + path);
File folder = new File(main.getDataFolder() + File.separator + "config" + File.separator + path);
folder.mkdirs();
try (Stream<Path> paths = Files.walk(folder.toPath())) {
paths

View File

@ -73,19 +73,25 @@ public class WorldConfig {
freq1 = 1f/config.getInt("frequencies.grid-1", 256);
freq2 = 1f/config.getInt("frequencies.grid-2", 512);
fromImage = config.getBoolean("image.use-image", false);
biomeXChannel = ImageLoader.Channel.valueOf(Objects.requireNonNull(config.getString("image.channels.biome-x", "red")).toUpperCase());
biomeZChannel = ImageLoader.Channel.valueOf(Objects.requireNonNull(config.getString("image.channels.biome-z", "green")).toUpperCase());
if(biomeZChannel.equals(biomeXChannel)) throw new InvalidConfigurationException("2 objects share the same image channels: biome-x and biome-z");
zoneChannel = ImageLoader.Channel.valueOf(Objects.requireNonNull(config.getString("image.channels.zone", "blue")).toUpperCase());
if(zoneChannel.equals(biomeXChannel) || zoneChannel.equals(biomeZChannel)) throw new InvalidConfigurationException("2 objects share the same image channels: zone and biome-x/z");
if(fromImage) {
try {
imageLoader = new ImageLoader(new File(Objects.requireNonNull(config.getString("image.image-location"))));
Bukkit.getLogger().info("[Terra] Loading world from image.");
} catch(IOException | NullPointerException e) {
e.printStackTrace();
fromImage = false;
try {
biomeXChannel = ImageLoader.Channel.valueOf(Objects.requireNonNull(config.getString("image.channels.biome-x", "red")).toUpperCase());
biomeZChannel = ImageLoader.Channel.valueOf(Objects.requireNonNull(config.getString("image.channels.biome-z", "green")).toUpperCase());
if(biomeZChannel.equals(biomeXChannel))
throw new InvalidConfigurationException("2 objects share the same image channels: biome-x and biome-z");
zoneChannel = ImageLoader.Channel.valueOf(Objects.requireNonNull(config.getString("image.channels.zone", "blue")).toUpperCase());
if(zoneChannel.equals(biomeXChannel) || zoneChannel.equals(biomeZChannel))
throw new InvalidConfigurationException("2 objects share the same image channels: zone and biome-x/z");
if(fromImage) {
try {
imageLoader = new ImageLoader(new File(Objects.requireNonNull(config.getString("image.image-location"))), ImageLoader.Align.valueOf(config.getString("image.align", "center").toUpperCase()));
Bukkit.getLogger().info("[Terra] Loading world from image.");
} catch(IOException | NullPointerException e) {
e.printStackTrace();
fromImage = false;
}
}
} catch(IllegalArgumentException e) {
throw new InvalidConfigurationException(e.getCause());
}

View File

@ -0,0 +1,23 @@
package com.dfsek.terra.config.genconfig;
import com.dfsek.terra.config.TerraConfigObject;
import org.bukkit.configuration.InvalidConfigurationException;
import java.io.File;
import java.io.IOException;
public class StructureConfig extends TerraConfigObject {
public StructureConfig(File file) throws IOException, InvalidConfigurationException {
super(file);
}
@Override
public void init() throws InvalidConfigurationException {
}
@Override
public String getID() {
return null;
}
}

View File

@ -1,8 +1,10 @@
package com.dfsek.terra.image;
import com.dfsek.terra.Terra;
import com.dfsek.terra.TerraChunkGenerator;
import com.dfsek.terra.biome.TerraBiomeGrid;
import com.dfsek.terra.biome.UserDefinedBiome;
import com.dfsek.terra.config.WorldConfig;
import com.dfsek.terra.config.genconfig.BiomeConfig;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@ -29,17 +31,22 @@ public class DebugFrame extends JFrame implements ActionListener {
public void paint(Graphics g) {
super.paintComponents(g);
for(Player p : Bukkit.getOnlinePlayers()) {
int xp = (int) (((double) Math.floorMod(p.getLocation().getBlockX(), x)/x)*getWidth());
int zp = (int) (((double) Math.floorMod(p.getLocation().getBlockZ(), z)/z)*getHeight());
if(! (p.getWorld().getGenerator() instanceof TerraChunkGenerator)) break;
int xp = (int) (((double) Math.floorMod(p.getLocation().getBlockX(), x) / x) * getWidth());
int zp = (int) (((double) Math.floorMod(p.getLocation().getBlockZ(), z) / z) * getHeight());
if(WorldConfig.fromWorld(p.getWorld()).imageLoader.getAlign().equals(ImageLoader.Align.CENTER)) {
xp = (int) (((double) Math.floorMod(p.getLocation().getBlockX() - (img.getWidth() / 2), x) / x) * getWidth());
zp = (int) (((double) Math.floorMod(p.getLocation().getBlockZ() - (img.getHeight() / 2), z) / z) * getHeight());
}
String str = BiomeConfig.fromBiome((UserDefinedBiome) TerraBiomeGrid.fromWorld(p.getWorld()).getBiome(p.getLocation())).getID();
g.setColor(new Color(255, 255, 255, 128));
g.fillRect(xp+13, zp-13, (int) (8 + 8.25*str.length()), 33);
g.fillRect(xp + 13, zp - 13, (int) (8 + 8.25 * str.length()), 33);
g.setColor(Color.BLACK);
g.drawString(p.getName(), xp+15, zp);
g.drawString(str, xp+15, zp+15);
g.drawString(p.getName(), xp + 15, zp);
g.drawString(str, xp + 15, zp + 15);
g.fillOval(xp, zp, 10, 10);
g.setColor(Color.RED);
g.fillOval(xp+3, zp+3, 5, 5);
g.fillOval(xp + 3, zp + 3, 5, 5);
}
}

View File

@ -18,20 +18,17 @@ import javax.imageio.ImageIO;
public class ImageLoader {
private final BufferedImage image;
private final Align align;
double inverseRoot2 = 0.7071067811865475;
public ImageLoader(File file) throws IOException {
public ImageLoader(File file, Align align) throws IOException {
image = ImageIO.read(file);
this.align = align;
}
public int getChannel(int x, int y, Channel channel) {
int rgb;
try {
rgb = image.getRGB(Math.floorMod(x, image.getWidth()), Math.floorMod(y, image.getHeight()));
} catch(ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
throw new IllegalArgumentException("Index " + x + "/" + x + "out of bounds for size " + image.getWidth() + "/" + image.getHeight());
}
rgb = align.getRGB(image, x, y);
switch(channel) {
case RED: return rgb >> 16 & 0xff;
case GREEN: return rgb >> 8 & 0xff;
@ -63,7 +60,7 @@ public class ImageLoader {
public double getNoiseVal(int x, int y, Channel channel) {
return ((double) (getChannel(x, y, channel) - 128)/128)*inverseRoot2;
}
private static BufferedImage copyImage(BufferedImage source){
private static BufferedImage copyImage(BufferedImage source) {
BufferedImage b = new BufferedImage(source.getWidth(), source.getHeight(), source.getType());
Graphics g = b.getGraphics();
g.drawImage(source, 0, 0, null);
@ -71,7 +68,29 @@ public class ImageLoader {
return b;
}
public Align getAlign() {
return align;
}
public enum Channel {
RED, GREEN, BLUE, ALPHA
}
public enum Align {
CENTER {
@Override
public int getRGB(BufferedImage image, int x, int y) {
return Align.getRGBNoAlign(image, x-(image.getWidth()/2), y-(image.getHeight()/2));
}
},
NONE {
@Override
public int getRGB(BufferedImage image, int x, int y) {
return image.getRGB(Math.floorMod(x, image.getWidth()), Math.floorMod(y, image.getHeight()));
}
};
public abstract int getRGB(BufferedImage image, int x, int y);
private static int getRGBNoAlign(BufferedImage image, int x, int y) {
return image.getRGB(Math.floorMod(x, image.getWidth()), Math.floorMod(y, image.getHeight()));
}
}
}

View File

@ -0,0 +1,90 @@
package com.dfsek.terra.structure;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Objects;
public class GaeaStructure implements Serializable {
public static final long serialVersionUID = -6664585217063842034L;
private final StructureContainedBlock[][][] structure;
public static GaeaStructure load(File f) throws IOException {
try {
return fromFile(f);
} catch(ClassNotFoundException e) {
throw new IllegalArgumentException("Provided file does not contain a GaeaStructure.");
}
}
public GaeaStructure(Location l1, Location l2) {
if(l1.getX() > l2.getX() || l1.getY() > l2.getY() || l1.getZ() > l2.getZ()) throw new IllegalArgumentException("Invalid locations provided!");
structure = new StructureContainedBlock[l2.getBlockX()-l1.getBlockX()+1][l2.getBlockY()-l1.getBlockY()+1][l2.getBlockZ()-l1.getBlockZ()+1];
for(int x = 0; x <= l2.getBlockX()-l1.getBlockX(); x++) {
for(int y = 0; y <= l2.getBlockY()-l1.getBlockY(); y++) {
for(int z = 0; z <= l2.getBlockZ()-l1.getBlockZ(); z++) {
structure[x][y][z] = new StructureContainedBlock(x, y, z, Objects.requireNonNull(l1.getWorld()).getBlockAt(l1.clone().add(x, y, z)));
}
}
}
}
public void paste(Location origin) {
for(StructureContainedBlock[][] bList2 : structure) {
for(StructureContainedBlock[] bList1 : bList2) {
for(StructureContainedBlock block : bList1) {
BlockData data = block.getBlockData();
Block worldBlock = origin.clone().add(block.getX(), block.getY(), block.getZ()).getBlock();
if(!data.getMaterial().equals(Material.STRUCTURE_VOID)) worldBlock.setBlockData(data);
}
}
}
}
public void paste(Location origin, Chunk c) {
for(StructureContainedBlock[][] bList2 : structure) {
for(StructureContainedBlock[] bList1 : bList2) {
for(StructureContainedBlock block : bList1) {
Location newLoc = origin.clone().add(block.getX(), block.getY(), block.getZ());
BlockData data = block.getBlockData();
if(newLoc.getChunk().equals(c) && !data.getMaterial().equals(Material.STRUCTURE_VOID)) newLoc.getBlock().setBlockData(block.getBlockData());
}
}
}
}
public void save(File f) throws IOException {
toFile(this, f);
}
private static GaeaStructure fromFile(File f) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
Object o = ois.readObject();
ois.close();
return (GaeaStructure) o;
}
public static GaeaStructure fromStream(InputStream f) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(f);
Object o = ois.readObject();
ois.close();
return (GaeaStructure) o;
}
private static void toFile(Serializable o, File f) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(o);
oos.close();
}
}

View File

@ -1,4 +0,0 @@
package com.dfsek.terra.structure;
public class MultiPartStructure {
}

View File

@ -0,0 +1,44 @@
package com.dfsek.terra.structure;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
import java.io.Serializable;
public class StructureContainedBlock implements Serializable {
public static final long serialVersionUID = 6143969483382710947L;
private final transient BlockData bl;
private final String dataString;
private final int x;
private final int y;
private final int z;
public StructureContainedBlock(int x, int y, int z, Block block) {
this.x = x;
this.y = y;
this.z = z;
this.bl = block.getBlockData();
dataString = bl.getAsString(false);
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public BlockData getBlockData() {
return bl == null ? Bukkit.createBlockData(dataString) : bl;
}
public String getDataAsString() {
return dataString;
}
}

View File

@ -3,6 +3,7 @@ main: com.dfsek.terra.Terra
version: 1.0.0
load: STARTUP
api-version: "1.15"
softdepend: ["WorldEdit"]
commands:
terra:
description: Terra base command

View File

@ -24,4 +24,14 @@ terra {
step;
}
}
structure {
export {
structure brigadier:string single_word;
}
load {
structure brigadier:string single_word {
respect_chunk brigadier:bool;
}
}
}
}