├── .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
--------------------------------------------------------------------------------