├── .gitignore ├── README.md ├── pom.xml └── src └── main ├── java └── sh │ └── sagan │ └── phobos │ ├── Phobos.java │ ├── Util.java │ └── builder │ ├── SchemBuilder.java │ ├── buildpattern │ ├── BuildPattern.java │ └── impl │ │ ├── Default.java │ │ ├── EdgeToEdgeLayered.java │ │ └── Random.java │ └── placeeffect │ ├── PlaceEffect.java │ └── impl │ └── GreenSparkleEffect.java └── resources └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea 2 | **/*.iml 3 | **/target -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phobos 🏰 2 | A stylistic asynchronously processed mine craft world edit schematic builder, providing cool ways to build schematics with effects and patterns all while the sorting and processing happens on another thread 3 | 4 | If you've been here before or don't care about super-cool amazing gifs, read the wiki: [Phobos Wiki](https://github.com/sagansfault/phobos/wiki) 5 | 6 | ### Why Java 16? 7 | Do you need directions to the bingo hall too? Update your java, it's starting to smell. 8 | 9 | ## What is Phobos? 10 | Phobos aims to add functionality for developers to build world edit schematics (`.schem` files) with some style in their worlds. The following are some 11 | basic examples of some build patterns that are currently in Phobos as of `v3.0.0` 12 | 13 | | | | | 14 | |:---:|:---:|:---:| 15 | | | | | 16 | 17 | ## Installing 18 | Shade it into your project or provide it at runtime as a plugin ;) 19 |
20 | Phobos, at the time of writing this, uses **WorldEdit** `v7.2.5` 21 | ```xml 22 | 23 | sh.sagan 24 | phobos 25 | 3.0.0 26 | 27 | 28 | ``` 29 | 30 | ## Contributing 31 | Contributions are always welcome via PRs. If it's a large one, open an issue and discuss/explain 32 | what you're doing and why. 33 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | sh.sagan 8 | phobos 9 | 3.0.0 10 | 11 | 12 | 16 13 | 16 14 | 15 | 16 | 17 | 18 | papermc 19 | https://papermc.io/repo/repository/maven-public/ 20 | 21 | 22 | enginehub-maven 23 | http://maven.enginehub.org/repo/ 24 | 25 | 26 | 27 | 28 | 29 | io.papermc.paper 30 | paper-api 31 | 1.17.1-R0.1-SNAPSHOT 32 | provided 33 | 34 | 35 | com.sk89q.worldedit 36 | worldedit-bukkit 37 | 7.2.5 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/Phobos.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos; 2 | 3 | import org.bukkit.plugin.java.JavaPlugin; 4 | 5 | public class Phobos extends JavaPlugin { 6 | // :P 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/Util.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.function.Function; 7 | 8 | /** 9 | * Pretty much internal use only. Use these at your own discretion. 10 | */ 11 | public class Util { 12 | 13 | private Util() {} 14 | 15 | public static List> split(List toSplit, int elementsPerPart) { 16 | if (toSplit.isEmpty() || elementsPerPart < 1) { 17 | return new ArrayList<>(); 18 | } 19 | 20 | List mutable = new ArrayList<>(toSplit); 21 | List> returnable = new ArrayList<>(); 22 | 23 | while (!mutable.isEmpty()) { 24 | List inner = new ArrayList<>(); 25 | for (int i = 0; i < elementsPerPart; i++) { 26 | if (mutable.isEmpty()) { 27 | break; 28 | } 29 | inner.add(mutable.remove(0)); 30 | } 31 | returnable.add(inner); 32 | } 33 | 34 | return returnable; 35 | } 36 | 37 | public static List> splitOnNewValue(List sortedToSplit, Function splitOn) { 38 | if (sortedToSplit.isEmpty()) { 39 | return new ArrayList<>(); 40 | } else if (sortedToSplit.size() <= 1) { 41 | return Collections.singletonList(sortedToSplit); 42 | } 43 | 44 | List mutable = new ArrayList<>(sortedToSplit); 45 | List> returnable = new ArrayList<>(); 46 | 47 | List set = new ArrayList<>(); 48 | T currentE = mutable.remove(0); 49 | set.add(currentE); 50 | T nextE = mutable.remove(0); 51 | 52 | while (true) { 53 | int current = splitOn.apply(currentE); 54 | int next = splitOn.apply(nextE); 55 | if (next != current) { 56 | returnable.add(set); 57 | set = new ArrayList<>(); 58 | currentE = nextE; 59 | } 60 | set.add(nextE); 61 | if (mutable.isEmpty()) { 62 | returnable.add(set); 63 | break; 64 | } 65 | nextE = mutable.remove(0); 66 | } 67 | return returnable; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/builder/SchemBuilder.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos.builder; 2 | 3 | import com.sk89q.worldedit.EditSession; 4 | import com.sk89q.worldedit.MaxChangedBlocksException; 5 | import com.sk89q.worldedit.WorldEdit; 6 | import com.sk89q.worldedit.bukkit.BukkitAdapter; 7 | import com.sk89q.worldedit.extent.clipboard.Clipboard; 8 | import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; 9 | import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; 10 | import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; 11 | import com.sk89q.worldedit.math.BlockVector3; 12 | import com.sk89q.worldedit.world.World; 13 | import org.bukkit.Location; 14 | import org.bukkit.plugin.java.JavaPlugin; 15 | import org.bukkit.scheduler.BukkitRunnable; 16 | import sh.sagan.phobos.builder.buildpattern.BuildPattern; 17 | import sh.sagan.phobos.builder.buildpattern.impl.Default; 18 | import sh.sagan.phobos.builder.placeeffect.PlaceEffect; 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.IOException; 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Optional; 28 | 29 | public class SchemBuilder { 30 | 31 | private final Clipboard clipboard; 32 | private final JavaPlugin plugin; 33 | 34 | private int ticksBetweenIterations = 1; 35 | private boolean ignoreAir = false; 36 | private BuildPattern buildPattern = new Default(); 37 | private PlaceEffect placeEffect = (blockLocation, clipboard) -> {}; 38 | 39 | private SchemBuilder(JavaPlugin plugin, Clipboard clipboard) { 40 | this.plugin = plugin; 41 | this.clipboard = clipboard; 42 | } 43 | 44 | /** 45 | * Attempts to construct a new builder instance from the given main plugin and the schematic file. If a builder 46 | * could not be constructed (invalid file, loading issues), an empty optional is returned. 47 | * 48 | * @param plugin The main plugin 49 | * @param file The schematic file. ({@code Path.of("C:\\schematics\\house.schem").toFile()}) 50 | * @return An optional containing the constructed SchemBuilder is successful, empty otherwise. 51 | */ 52 | public static Optional from(JavaPlugin plugin, File file) { 53 | Clipboard clipboard; 54 | ClipboardFormat format = ClipboardFormats.findByFile(file); 55 | if (format == null) { 56 | return Optional.empty(); 57 | } 58 | 59 | try (ClipboardReader reader = format.getReader(new FileInputStream(file))) { 60 | clipboard = reader.read(); 61 | } catch (IOException e) { 62 | return Optional.empty(); 63 | } 64 | 65 | return Optional.of(new SchemBuilder(plugin, clipboard)); 66 | } 67 | 68 | /** 69 | * Sets how many ticks long the period between iterations should be. 70 | * 71 | * @param ticks The ticks to wait between iterations 72 | * @return This builder instance. 73 | */ 74 | public SchemBuilder ticksBetweenIterations(int ticks) { 75 | this.ticksBetweenIterations = Math.min(Math.max(1, ticks), 2400); 76 | return this; 77 | } 78 | 79 | /** 80 | * Specifies whether this builder should ignore air when building. When true, this will not alter blocks in the 81 | * place of air. 82 | * 83 | * @param ignoreAir Whether to ignore air blocks when building 84 | * @return This builder instance. 85 | */ 86 | public SchemBuilder ignoreAir(boolean ignoreAir) { 87 | this.ignoreAir = ignoreAir; 88 | return this; 89 | } 90 | 91 | /** 92 | * Sets the build pattern for this builder instance. 93 | * 94 | * @param buildPattern The build pattern to use for this builder 95 | * @return This builder instance. 96 | */ 97 | public SchemBuilder buildPattern(BuildPattern buildPattern) { 98 | if (buildPattern != null) { 99 | this.buildPattern = buildPattern; 100 | } 101 | return this; 102 | } 103 | 104 | /** 105 | * Sets the place effect for this builder. This effect will be run every time a *single* block is placed. 106 | * 107 | * @param placeEffect The place effect to use 108 | * @return This builder instance. 109 | */ 110 | public SchemBuilder placeEffect(PlaceEffect placeEffect) { 111 | if (placeEffect != null) { 112 | this.placeEffect = placeEffect; 113 | } 114 | return this; 115 | } 116 | 117 | /** 118 | * Gets the clipboard object used for this schematic. Use this to alter the clipboard before running the builder 119 | * 120 | * @return The clipboard for this loaded schematic. 121 | */ 122 | public Clipboard getClipboard() { 123 | return clipboard; 124 | } 125 | 126 | /** 127 | * Executes this schem builder, building the schematic at the specified location with all the prior specifications. 128 | * 129 | * @param location The location/origin to build this schematic at 130 | */ 131 | public void buildAt(Location location) { 132 | if (location.getWorld() == null) { 133 | return; 134 | } 135 | 136 | BlockVector3 buildAt = BukkitAdapter.asBlockVector(location); 137 | 138 | new BukkitRunnable(){ 139 | @Override 140 | public void run() { 141 | List toArrange = new ArrayList<>(); 142 | if (ignoreAir) { 143 | clipboard.getRegion().forEach(bv -> { 144 | if (!clipboard.getBlock(bv).getBlockType().getMaterial().isAir()) { 145 | toArrange.add(BukkitAdapter.adapt(location.getWorld(), bv)); 146 | } 147 | }); 148 | } else { 149 | clipboard.getRegion().forEach(bv -> toArrange.add(BukkitAdapter.adapt(location.getWorld(), bv))); 150 | } 151 | 152 | List> arranged = buildPattern.arrange(toArrange, clipboard); 153 | 154 | new BukkitRunnable() { 155 | @Override 156 | public void run() { 157 | if (arranged.isEmpty()) { 158 | this.cancel(); 159 | return; 160 | } 161 | 162 | List iteration = arranged.remove(0); 163 | Map toPlace = new HashMap<>(); 164 | iteration.forEach(loc -> { 165 | BlockVector3 converted = BukkitAdapter.asBlockVector(loc); 166 | BlockVector3 target = converted.add(buildAt.subtract(clipboard.getOrigin())); 167 | toPlace.put(converted, target); 168 | }); 169 | 170 | placeBlocks(toPlace, BukkitAdapter.adapt(location.getWorld())); 171 | } 172 | }.runTaskTimer(plugin, 0, ticksBetweenIterations); 173 | } 174 | }.runTaskAsynchronously(plugin); 175 | } 176 | 177 | private void placeBlocks(Map clipboardBlockLocToTarget, World world) { 178 | try (EditSession editSession = WorldEdit.getInstance().newEditSession(world)) { 179 | clipboardBlockLocToTarget.forEach((clipboardLoc, target) -> { 180 | try { 181 | editSession.setBlock(target, this.clipboard.getBlock(clipboardLoc)); 182 | placeEffect.onPlace(BukkitAdapter.adapt(BukkitAdapter.adapt(world), target), this.clipboard); 183 | } catch (MaxChangedBlocksException e) { 184 | e.printStackTrace(); 185 | } 186 | }); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/builder/buildpattern/BuildPattern.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos.builder.buildpattern; 2 | 3 | import com.sk89q.worldedit.extent.clipboard.Clipboard; 4 | import org.bukkit.Location; 5 | 6 | import java.util.List; 7 | 8 | public abstract class BuildPattern { 9 | 10 | /* The blocks to place per iteration. This may or may not be used by child implementations */ 11 | protected final int blocksPerIteration; 12 | 13 | /** 14 | * Constructs a new BuildPattern. 15 | * 16 | * @param blocksPerIteration The blocks to place per each iteration. This may or may no do anything depending on 17 | * the child implementation. 18 | */ 19 | public BuildPattern(int blocksPerIteration) { 20 | this.blocksPerIteration = Math.max(1, blocksPerIteration); 21 | } 22 | 23 | public BuildPattern() { 24 | this(Integer.MAX_VALUE); 25 | } 26 | 27 | /** 28 | * Arranges a set of unordered block locations into an order in which they should be placed. For examples, view the 29 | * default build patters provided by Phobos which implement this interface 30 | * 31 | * @param toArrange The list of unordered blocks to arrange/order into a list to return 32 | * @param clipboard The clipboard from which these blocks came from. This may not be required by all build patterns. 33 | * @return A list of arranged/ordered block locations to build this schematic in 34 | */ 35 | public abstract List> arrange(List toArrange, Clipboard clipboard); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/builder/buildpattern/impl/Default.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos.builder.buildpattern.impl; 2 | 3 | import com.sk89q.worldedit.extent.clipboard.Clipboard; 4 | import org.bukkit.Location; 5 | import sh.sagan.phobos.Util; 6 | import sh.sagan.phobos.builder.buildpattern.BuildPattern; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class Default extends BuildPattern { 12 | 13 | @Override 14 | public List> arrange(List toArrange, Clipboard clipboard) { 15 | if (blocksPerIteration == Integer.MAX_VALUE) { 16 | return Collections.singletonList(toArrange); 17 | } else { 18 | return Util.split(toArrange, blocksPerIteration); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/builder/buildpattern/impl/EdgeToEdgeLayered.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos.builder.buildpattern.impl; 2 | 3 | import com.sk89q.worldedit.extent.clipboard.Clipboard; 4 | import org.bukkit.Location; 5 | import sh.sagan.phobos.Util; 6 | import sh.sagan.phobos.builder.buildpattern.BuildPattern; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Comparator; 10 | import java.util.List; 11 | import java.util.function.Function; 12 | 13 | public class EdgeToEdgeLayered extends BuildPattern { 14 | 15 | private final Direction direction; 16 | 17 | public EdgeToEdgeLayered(Direction direction) { 18 | this.direction = direction; 19 | } 20 | 21 | public EdgeToEdgeLayered() { 22 | this(Direction.Y_POSITIVE); 23 | } 24 | 25 | @Override 26 | public List> arrange(List toArrange, Clipboard clipboard) { 27 | List sorted = new ArrayList<>(toArrange); 28 | Function splitOn; 29 | switch (direction) { 30 | case X_POSITIVE -> { 31 | sorted.sort(Comparator.comparingInt(Location::getBlockX)); 32 | splitOn = Location::getBlockX; 33 | } 34 | case Z_POSITIVE -> { 35 | sorted.sort(Comparator.comparingInt(Location::getBlockZ)); 36 | splitOn = Location::getBlockZ; 37 | } 38 | case Y_NEGATIVE -> { 39 | sorted.sort(Comparator.comparingInt(Location::getBlockY).reversed()); 40 | splitOn = Location::getBlockY; 41 | } 42 | case X_NEGATIVE -> { 43 | sorted.sort(Comparator.comparingInt(Location::getBlockX).reversed()); 44 | splitOn = Location::getBlockX; 45 | } 46 | case Z_NEGATIVE -> { 47 | sorted.sort(Comparator.comparingInt(Location::getBlockZ).reversed()); 48 | splitOn = Location::getBlockZ; 49 | } 50 | // handles default and Y_POSITIVE case 51 | default -> { 52 | sorted.sort(Comparator.comparingInt(Location::getBlockY)); 53 | splitOn = Location::getBlockY; 54 | } 55 | } 56 | 57 | return Util.splitOnNewValue(sorted, splitOn); 58 | } 59 | 60 | /** 61 | * The direction to build this in. 62 | */ 63 | public enum Direction { 64 | /** 65 | * Builds in the Y direction moving positively. (bottom to top) 66 | */ 67 | Y_POSITIVE, 68 | X_POSITIVE, 69 | Z_POSITIVE, 70 | Y_NEGATIVE, 71 | X_NEGATIVE, 72 | Z_NEGATIVE 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/builder/buildpattern/impl/Random.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos.builder.buildpattern.impl; 2 | 3 | import com.sk89q.worldedit.extent.clipboard.Clipboard; 4 | import org.bukkit.Location; 5 | import sh.sagan.phobos.Util; 6 | import sh.sagan.phobos.builder.buildpattern.BuildPattern; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class Random extends BuildPattern { 12 | 13 | @Override 14 | public List> arrange(List toArrange, Clipboard clipboard) { 15 | Collections.shuffle(toArrange); 16 | if (super.blocksPerIteration == Integer.MAX_VALUE) { 17 | return Collections.singletonList(toArrange); 18 | } else { 19 | return Util.split(toArrange, super.blocksPerIteration); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/builder/placeeffect/PlaceEffect.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos.builder.placeeffect; 2 | 3 | import com.sk89q.worldedit.extent.clipboard.Clipboard; 4 | import org.bukkit.Location; 5 | 6 | @FunctionalInterface 7 | public interface PlaceEffect { 8 | 9 | /** 10 | * A function to be run whenever a single block is placed. This effect can literally be anything but should probably 11 | * be a sound/particle effect 12 | * 13 | * @param blockLocation The location this block will be placed at 14 | * @param clipboard The clipboard for this schematic builder 15 | */ 16 | void onPlace(Location blockLocation, Clipboard clipboard); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/sh/sagan/phobos/builder/placeeffect/impl/GreenSparkleEffect.java: -------------------------------------------------------------------------------- 1 | package sh.sagan.phobos.builder.placeeffect.impl; 2 | 3 | import com.sk89q.worldedit.extent.clipboard.Clipboard; 4 | import org.bukkit.Location; 5 | import org.bukkit.Particle; 6 | import sh.sagan.phobos.builder.placeeffect.PlaceEffect; 7 | 8 | import java.util.concurrent.ThreadLocalRandom; 9 | 10 | public class GreenSparkleEffect implements PlaceEffect { 11 | 12 | private final ThreadLocalRandom rand = ThreadLocalRandom.current(); 13 | 14 | @Override 15 | public void onPlace(Location blockLocation, Clipboard clipboard) { 16 | blockLocation.getWorld().spawnParticle(Particle.VILLAGER_HAPPY, blockLocation.clone().add( 17 | rand.nextDouble(-1, 1), 18 | rand.nextDouble(-1, 1), 19 | rand.nextDouble(-1, 1) 20 | ), 1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: Phobos 2 | description: An asyncronously processed world edit schematic builder 3 | main: sh.sagan.phobos.Phobos 4 | version: 3.0.0 5 | api-version: 1.16 6 | depend: 7 | - WorldEdit --------------------------------------------------------------------------------