diff --git a/build.gradle b/build.gradle index 7a9b56801..fc5a8fed1 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ plugins { } group 'com.volmit.iris' -version '1.7.3' +version '1.7.4' def apiVersion = '1.17' def name = getRootProject().getName() // See settings.gradle def main = 'com.volmit.iris.Iris' diff --git a/src/main/java/com/volmit/iris/Iris.java b/src/main/java/com/volmit/iris/Iris.java index 7ce94e736..fd93d97a8 100644 --- a/src/main/java/com/volmit/iris/Iris.java +++ b/src/main/java/com/volmit/iris/Iris.java @@ -75,7 +75,6 @@ import java.util.Map; @SuppressWarnings("CanBeFinal") public class Iris extends VolmitPlugin implements Listener { private KMap, IrisService> services; - public static KList executors = new KList<>(); public static Iris instance; public static BukkitAudiences audiences; public static MultiverseCoreLink linkMultiverseCore; @@ -85,6 +84,7 @@ public class Iris extends VolmitPlugin implements Listener { public static IrisCompat compat; public static FileWatcher configWatcher; private static VolmitSender sender; + private final KList postShutdown = new KList<>(); @Permission public static PermissionIris perm; @@ -126,6 +126,11 @@ public class Iris extends VolmitPlugin implements Listener { services.values().forEach(this::registerListener); } + public void postShutdown(Runnable r) + { + postShutdown.add(r); + } + private void postEnable() { J.a(() -> PaperLib.suggestPaper(this)); J.a(() -> IO.delete(getTemp())); @@ -155,18 +160,10 @@ public class Iris extends VolmitPlugin implements Listener { } public void onDisable() { - for (GroupedExecutor i : executors) { - Iris.debug("Closing Executor " + i.toString()); - i.closeNow(); - } - - executors.clear(); - + services.values().forEach(IrisService::onDisable); Bukkit.getScheduler().cancelTasks(this); HandlerList.unregisterAll((Plugin) this); - MultiBurst.burst.shutdown(); - - services.values().forEach(IrisService::onDisable); + postShutdown.forEach(Runnable::run); services.clear(); super.onDisable(); } diff --git a/src/main/java/com/volmit/iris/core/IrisSettings.java b/src/main/java/com/volmit/iris/core/IrisSettings.java index 4a87206e7..7c72ea270 100644 --- a/src/main/java/com/volmit/iris/core/IrisSettings.java +++ b/src/main/java/com/volmit/iris/core/IrisSettings.java @@ -104,6 +104,7 @@ public class IrisSettings { public boolean verbose = false; public boolean ignoreWorldEdit = false; public boolean disableNMS = false; + public boolean keepProductionOnReload = false; public boolean pluginMetrics = true; public boolean splashLogoStartup = true; public String forceMainWorld = ""; diff --git a/src/main/java/com/volmit/iris/core/service/EditSVC.java b/src/main/java/com/volmit/iris/core/service/EditSVC.java index ce7ce1be5..1012c7eea 100644 --- a/src/main/java/com/volmit/iris/core/service/EditSVC.java +++ b/src/main/java/com/volmit/iris/core/service/EditSVC.java @@ -37,7 +37,6 @@ public class EditSVC implements IrisService { @Override public void onEnable() { this.editors = new KMap<>(); - Iris.info("EDIT SVC ENABLED!"); Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::update, 1000, 1000); } diff --git a/src/main/java/com/volmit/iris/core/service/PreservationSVC.java b/src/main/java/com/volmit/iris/core/service/PreservationSVC.java index b6123d28d..49a7a8ad0 100644 --- a/src/main/java/com/volmit/iris/core/service/PreservationSVC.java +++ b/src/main/java/com/volmit/iris/core/service/PreservationSVC.java @@ -74,52 +74,53 @@ public class PreservationSVC implements IrisService @Override public void onDisable() { dereferencer.interrupt(); + dereference(); - for(Thread i : threads) - { - if(i.isAlive()) + postShutdown(() -> { + for(Thread i : threads) + { + if(i.isAlive()) + { + try + { + i.interrupt(); + Iris.info("Shutdown Thread " + i.getName()); + } + + catch(Throwable e) + { + Iris.reportError(e); + } + } + } + + for(MultiBurst i : bursts) { try { - i.interrupt(); - Iris.info("Shutdown Thread " + i.getName()); + i.shutdownNow(); + Iris.info("Shutdown Multiburst " + i); } catch(Throwable e) { - + Iris.reportError(e); } } - } - for(MultiBurst i : bursts) - { - try + for(ExecutorService i : services) { - i.shutdownNow(); - Iris.info("Shutdown Multiburst " + i); + try + { + i.shutdownNow(); + Iris.info("Shutdown Executor Service " + i); + } + + catch(Throwable e) + { + Iris.reportError(e); + } } - - catch(Throwable e) - { - - } - } - - for(ExecutorService i : services) - { - try - { - i.shutdownNow(); - Iris.info("Shutdown Executor Service " + i); - } - - catch(Throwable e) - { - - } - } - - dereference(); + }); } } diff --git a/src/main/java/com/volmit/iris/core/service/StudioSVC.java b/src/main/java/com/volmit/iris/core/service/StudioSVC.java index 063891ccb..5adadda95 100644 --- a/src/main/java/com/volmit/iris/core/service/StudioSVC.java +++ b/src/main/java/com/volmit/iris/core/service/StudioSVC.java @@ -73,15 +73,27 @@ public class StudioSVC implements IrisService { @Override public void onDisable() { - if (IrisSettings.get().isStudio()) { - Iris.debug("Studio Mode Active: Closing Projects"); + Iris.debug("Studio Mode Active: Closing Projects"); - for (World i : Bukkit.getWorlds()) { - if (IrisToolbelt.isIrisWorld(i)) { + for (World i : Bukkit.getWorlds()) { + if (IrisToolbelt.isIrisWorld(i)) { + if(IrisToolbelt.isStudio(i)) + { IrisToolbelt.evacuate(i); - Iris.debug("Closing Platform Generator " + i.getName()); IrisToolbelt.access(i).close(); } + + else + { + if(!IrisSettings.get().getGeneral().isKeepProductionOnReload()) + { + IrisToolbelt.evacuate(i); + IrisToolbelt.access(i).close(); + Iris.error("You cannot reload Iris while production worlds are active!"); + Iris.error("To prevent corrupted chunks, Iris is shutting the server down now!"); + Bukkit.shutdown(); + } + } } } } diff --git a/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java b/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java index 530dbde40..8277f117b 100644 --- a/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java +++ b/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java @@ -189,4 +189,12 @@ public class IrisToolbelt { return false; } + + public static boolean isStudio(World i) { + return isIrisWorld(i) && access(i).isStudio(); + } + + public static boolean isHeadless(World i) { + return isIrisWorld(i) && access(i).isHeadless(); + } } diff --git a/src/main/java/com/volmit/iris/engine/IrisEngine.java b/src/main/java/com/volmit/iris/engine/IrisEngine.java index 5bea6a8dc..0e6fdff5e 100644 --- a/src/main/java/com/volmit/iris/engine/IrisEngine.java +++ b/src/main/java/com/volmit/iris/engine/IrisEngine.java @@ -412,14 +412,14 @@ public class IrisEngine extends BlockPopulator implements Engine { switch (getDimension().getTerrainMode()) { case NORMAL -> { - getMantle().generateMatter(x >> 4, z >> 4); + getMantle().generateMatter(x >> 4, z >> 4, multicore); getTerrainActuator().actuate(x, z, vblocks, multicore); getBiomeActuator().actuate(x, z, vbiomes, multicore); getCaveModifier().modify(x, z, vblocks, multicore); getRavineModifier().modify(x, z, vblocks, multicore); getPostModifier().modify(x, z, vblocks, multicore); getDecorantActuator().actuate(x, z, blocks, multicore); - getMantle().insertMatter(x >> 4, z >> 4, BlockData.class, blocks); + getMantle().insertMatter(x >> 4, z >> 4, BlockData.class, blocks, multicore); getDepositModifier().modify(x, z, blocks, multicore); } case ISLANDS -> { diff --git a/src/main/java/com/volmit/iris/engine/actuator/IrisBiomeActuator.java b/src/main/java/com/volmit/iris/engine/actuator/IrisBiomeActuator.java index b78f27264..45b8fd31c 100644 --- a/src/main/java/com/volmit/iris/engine/actuator/IrisBiomeActuator.java +++ b/src/main/java/com/volmit/iris/engine/actuator/IrisBiomeActuator.java @@ -26,9 +26,12 @@ import com.volmit.iris.engine.framework.EngineAssignedActuator; import com.volmit.iris.engine.object.biome.IrisBiome; import com.volmit.iris.engine.object.biome.IrisBiomeCustom; import com.volmit.iris.util.documentation.BlockCoordinates; +import com.volmit.iris.util.format.Form; import com.volmit.iris.util.hunk.Hunk; import com.volmit.iris.util.hunk.view.BiomeGridHunkView; import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.parallel.BurstExecutor; +import com.volmit.iris.util.parallel.MultiBurst; import com.volmit.iris.util.scheduling.PrecisionStopwatch; import org.bukkit.block.Biome; import org.bukkit.generator.ChunkGenerator; @@ -65,41 +68,46 @@ public class IrisBiomeActuator extends EngineAssignedActuator { @Override public void onActuate(int x, int z, Hunk h, boolean multicore) { PrecisionStopwatch p = PrecisionStopwatch.start(); - int zf, maxHeight; - IrisBiome ib; + BurstExecutor burst = burst().burst(); + burst.setMulticore(multicore); for (int xf = 0; xf < h.getWidth(); xf++) { - for (zf = 0; zf < h.getDepth(); zf++) { - ib = getComplex().getTrueBiomeStream().get(modX(xf + x), modZ(zf + z)); - maxHeight = (int) (getComplex().getFluidHeight() + ib.getMaxWithObjectHeight(getData())); - if (ib.isCustom()) { - try { - IrisBiomeCustom custom = ib.getCustomBiome(rng, x, 0, z); - Object biomeBase = INMS.get().getCustomBiomeBaseFor(getDimension().getLoadKey() + ":" + custom.getId()); + int finalXf = xf; + burst.queue(() -> { + IrisBiome ib; + for (int zf = 0; zf < h.getDepth(); zf++) { + ib = getComplex().getTrueBiomeStream().get(modX(finalXf + x), modZ(zf + z)); + int maxHeight = (int) (getComplex().getFluidHeight() + ib.getMaxWithObjectHeight(getData())); + if (ib.isCustom()) { + try { + IrisBiomeCustom custom = ib.getCustomBiome(rng, x, 0, z); + Object biomeBase = INMS.get().getCustomBiomeBaseFor(getDimension().getLoadKey() + ":" + custom.getId()); - if (biomeBase == null || !injectBiome(h, x, 0, z, biomeBase)) { - throw new RuntimeException("Cant inject biome!"); - } + if (biomeBase == null || !injectBiome(h, x, 0, z, biomeBase)) { + throw new RuntimeException("Cant inject biome!"); + } - for (int i = 0; i < maxHeight; i++) { - injectBiome(h, xf, i, zf, biomeBase); + for (int i = 0; i < maxHeight; i++) { + injectBiome(h, finalXf, i, zf, biomeBase); + } + } catch (Throwable e) { + Iris.reportError(e); + Biome v = ib.getSkyBiome(rng, x, 0, z); + for (int i = 0; i < maxHeight; i++) { + h.set(finalXf, i, zf, v); + } } - } catch (Throwable e) { - Iris.reportError(e); + } else { Biome v = ib.getSkyBiome(rng, x, 0, z); for (int i = 0; i < maxHeight; i++) { - h.set(xf, i, zf, v); + h.set(finalXf, i, zf, v); } } - } else { - Biome v = ib.getSkyBiome(rng, x, 0, z); - for (int i = 0; i < maxHeight; i++) { - h.set(xf, i, zf, v); - } } - } + }); } + burst.complete(); getEngine().getMetrics().getBiome().put(p.getMilliseconds()); } } diff --git a/src/main/java/com/volmit/iris/engine/actuator/IrisDecorantActuator.java b/src/main/java/com/volmit/iris/engine/actuator/IrisDecorantActuator.java index 6184b62a6..744f30048 100644 --- a/src/main/java/com/volmit/iris/engine/actuator/IrisDecorantActuator.java +++ b/src/main/java/com/volmit/iris/engine/actuator/IrisDecorantActuator.java @@ -18,6 +18,7 @@ package com.volmit.iris.engine.actuator; +import com.volmit.iris.Iris; import com.volmit.iris.engine.decorator.*; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.EngineAssignedActuator; @@ -25,13 +26,16 @@ import com.volmit.iris.engine.framework.EngineDecorator; import com.volmit.iris.engine.object.biome.IrisBiome; import com.volmit.iris.engine.object.carve.IrisCaveLayer; import com.volmit.iris.util.documentation.BlockCoordinates; +import com.volmit.iris.util.format.Form; import com.volmit.iris.util.hunk.Hunk; import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.parallel.BurstExecutor; import com.volmit.iris.util.scheduling.PrecisionStopwatch; import lombok.Getter; import org.bukkit.Material; import org.bukkit.block.data.BlockData; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiPredicate; import java.util.function.Predicate; @@ -86,72 +90,82 @@ public class IrisDecorantActuator extends EngineAssignedActuator { } PrecisionStopwatch p = PrecisionStopwatch.start(); - - int j, realX, realZ, height; - IrisBiome biome, cave; - + BurstExecutor burst = burst().burst(); + burst.setMulticore(multicore); for (int i = 0; i < output.getWidth(); i++) { - for (j = 0; j < output.getDepth(); j++) { - boolean solid, liquid; - int emptyFor = 0; - int liquidFor = 0; - int lastSolid = 0; - realX = (int) Math.round(modX(x + i)); - realZ = (int) Math.round(modZ(z + j)); - height = (int) Math.round(getComplex().getHeightStream().get(realX, realZ)); - biome = getComplex().getTrueBiomeStream().get(realX, realZ); - cave = shouldRay ? getComplex().getCaveBiomeStream().get(realX, realZ) : null; + int finalI = i; + burst.queue(() -> { + int height; + int realX = (int) Math.round(modX(x + finalI)); + int realZ; + IrisBiome biome, cave; + for (int j=0; j < output.getDepth(); j++) { + boolean solid, liquid; + int emptyFor = 0; + int liquidFor = 0; + int lastSolid = 0; + realZ = (int) Math.round(modZ(z + j)); + height = (int) Math.round(getComplex().getHeightStream().get(realX, realZ)); + biome = getComplex().getTrueBiomeStream().get(realX, realZ); + cave = shouldRay ? getComplex().getCaveBiomeStream().get(realX, realZ) : null; - if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) { - continue; - } + if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) { + continue; + } - if (height == getDimension().getFluidHeight()) { - getShoreLineDecorator().decorate(i, j, - realX, (int) Math.round(modX(x + i + 1)), (int) Math.round(modX(x + i - 1)), - realZ, (int) Math.round(modZ(z + j + 1)), (int) Math.round(modZ(z + j - 1)), - output, biome, height, getEngine().getHeight()); - } else if (height == getDimension().getFluidHeight() + 1) { - getSeaSurfaceDecorator().decorate(i, j, - realX, (int) Math.round(modX(x + i + 1)), (int) Math.round(modX(x + i - 1)), - realZ, (int) Math.round(modZ(z + j + 1)), (int) Math.round(modZ(z + j - 1)), - output, biome, height, getEngine().getHeight()); - } else if (height < getDimension().getFluidHeight()) { - getSeaFloorDecorator().decorate(i, j, realX, realZ, output, biome, height + 1, getDimension().getFluidHeight() + 1); - } + if(height < getDimension().getFluidHeight()) + { + getSeaSurfaceDecorator().decorate(finalI, j, + realX, (int) Math.round(modX(x + finalI + 1)), (int) Math.round(modX(x + finalI - 1)), + realZ, (int) Math.round(modZ(z + j + 1)), (int) Math.round(modZ(z + j - 1)), + output, biome, getDimension().getFluidHeight(), getEngine().getHeight()); + getSeaFloorDecorator().decorate(finalI, j, + realX, realZ, output, biome, height + 1, + getDimension().getFluidHeight() + 1); + } - getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height); + if (height == getDimension().getFluidHeight()) { + getShoreLineDecorator().decorate(finalI, j, + realX, (int) Math.round(modX(x + finalI + 1)), (int) Math.round(modX(x + finalI - 1)), + realZ, (int) Math.round(modZ(z + j + 1)), (int) Math.round(modZ(z + j - 1)), + output, biome, height, getEngine().getHeight()); + } + + getSurfaceDecorator().decorate(finalI, j, realX, realZ, output, biome, height, getEngine().getHeight() - height); - if (cave != null && cave.getDecorators().isNotEmpty()) { - for (int k = height; k > 0; k--) { - solid = PREDICATE_SOLID.test(output.get(i, k, j)); - liquid = PREDICATE_CAVELIQUID.test(output.get(i, k + 1, j), k + 1); + if (cave != null && cave.getDecorators().isNotEmpty()) { + for (int k = height; k > 0; k--) { + solid = PREDICATE_SOLID.test(output.get(finalI, k, j)); + liquid = PREDICATE_CAVELIQUID.test(output.get(finalI, k + 1, j), k + 1); - if (solid) { - if (emptyFor > 0) { - if (liquid) { - getSeaFloorDecorator().decorate(i, j, realX, realZ, output, cave, k + 1, liquidFor + lastSolid - emptyFor + 1); - getSeaSurfaceDecorator().decorate(i, j, realX, realZ, output, cave, k + liquidFor + 1, emptyFor - liquidFor + lastSolid); - } else { - getSurfaceDecorator().decorate(i, j, realX, realZ, output, cave, k, lastSolid); - getCeilingDecorator().decorate(i, j, realX, realZ, output, cave, lastSolid - 1, emptyFor); + if (solid) { + if (emptyFor > 0) { + if (liquid) { + getSeaFloorDecorator().decorate(finalI, j, realX, realZ, output, cave, k + 1, liquidFor + lastSolid - emptyFor + 1); + getSeaSurfaceDecorator().decorate(finalI, j, realX, realZ, output, cave, k + liquidFor + 1, emptyFor - liquidFor + lastSolid); + } else { + getSurfaceDecorator().decorate(finalI, j, realX, realZ, output, cave, k, lastSolid); + getCeilingDecorator().decorate(finalI, j, realX, realZ, output, cave, lastSolid - 1, emptyFor); + } + emptyFor = 0; + liquidFor = 0; } - emptyFor = 0; - liquidFor = 0; + lastSolid = k; + } else { + emptyFor++; + if (liquid) liquidFor++; } - lastSolid = k; - } else { - emptyFor++; - if (liquid) liquidFor++; } } } - } + }); } + burst.complete(); getEngine().getMetrics().getDecoration().put(p.getMilliseconds()); + } private boolean shouldRayDecorate() { diff --git a/src/main/java/com/volmit/iris/engine/actuator/IrisTerrainNormalActuator.java b/src/main/java/com/volmit/iris/engine/actuator/IrisTerrainNormalActuator.java index 36b50ee35..3c46dde05 100644 --- a/src/main/java/com/volmit/iris/engine/actuator/IrisTerrainNormalActuator.java +++ b/src/main/java/com/volmit/iris/engine/actuator/IrisTerrainNormalActuator.java @@ -57,20 +57,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator public void onActuate(int x, int z, Hunk h, boolean multicore) { PrecisionStopwatch p = PrecisionStopwatch.start(); - if (multicore) { - BurstExecutor e = getEngine().burst().burst(h.getWidth()); - for (int xf = 0; xf < h.getWidth(); xf++) { - int finalXf = xf; - e.queue(() -> terrainSliver(x, z, finalXf, h)); - } - - e.complete(); - } else { - for (int xf = 0; xf < h.getWidth(); xf++) { - terrainSliver(x, z, xf, h); - } + BurstExecutor e = getEngine().burst().burst(h.getWidth()); + e.setMulticore(multicore); + for (int xf = 0; xf < h.getWidth(); xf++) { + int finalXf = xf; + e.queue(() -> terrainSliver(x, z, finalXf, h)); } + e.complete(); + getEngine().getMetrics().getTerrain().put(p.getMilliseconds()); } diff --git a/src/main/java/com/volmit/iris/engine/decorator/IrisSeaSurfaceDecorator.java b/src/main/java/com/volmit/iris/engine/decorator/IrisSeaSurfaceDecorator.java index 0a4298c2c..24bb11171 100644 --- a/src/main/java/com/volmit/iris/engine/decorator/IrisSeaSurfaceDecorator.java +++ b/src/main/java/com/volmit/iris/engine/decorator/IrisSeaSurfaceDecorator.java @@ -18,6 +18,7 @@ package com.volmit.iris.engine.decorator; +import com.volmit.iris.Iris; import com.volmit.iris.engine.data.cache.Cache; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.object.biome.IrisBiome; diff --git a/src/main/java/com/volmit/iris/engine/decorator/IrisSurfaceDecorator.java b/src/main/java/com/volmit/iris/engine/decorator/IrisSurfaceDecorator.java index 38351ea8a..3a66a28ac 100644 --- a/src/main/java/com/volmit/iris/engine/decorator/IrisSurfaceDecorator.java +++ b/src/main/java/com/volmit/iris/engine/decorator/IrisSurfaceDecorator.java @@ -77,10 +77,6 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { } int stack = decorator.getHeight(getRng().nextParallelRNG(Cache.key(realX, realZ)), realX, realZ, getData()); - if (decorator.isScaleStack()) { - int maxStack = max - height; - stack = (int) Math.ceil((double) maxStack * ((double) stack / 100)); - } else stack = Math.min(height - max, stack); if (stack == 1) { data.set(x, height, z, decorator.getBlockDataForTop(biome, getRng(), realX, height, realZ, getData())); diff --git a/src/main/java/com/volmit/iris/engine/framework/EngineComponent.java b/src/main/java/com/volmit/iris/engine/framework/EngineComponent.java index 389f52910..fb4b0a1af 100644 --- a/src/main/java/com/volmit/iris/engine/framework/EngineComponent.java +++ b/src/main/java/com/volmit/iris/engine/framework/EngineComponent.java @@ -23,6 +23,7 @@ import com.volmit.iris.core.project.loader.IrisData; import com.volmit.iris.engine.IrisComplex; import com.volmit.iris.engine.object.dimensional.IrisDimension; import com.volmit.iris.util.math.RollingSequence; +import com.volmit.iris.util.parallel.MultiBurst; import org.bukkit.event.Listener; public interface EngineComponent { @@ -32,6 +33,11 @@ public interface EngineComponent { String getName(); + default MultiBurst burst() + { + return getEngine().burst(); + } + default void close() { try { if (this instanceof Listener) { diff --git a/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java b/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java index 37d34a44f..3046f8d3c 100644 --- a/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java +++ b/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java @@ -184,7 +184,7 @@ public interface EngineMantle extends IObjectPlacer { @ChunkCoordinates - default void generateMatter(int x, int z) { + default void generateMatter(int x, int z, boolean multicore) { if (!getEngine().getDimension().isUseMantle()) { return; } @@ -199,6 +199,7 @@ public interface EngineMantle extends IObjectPlacer { }; int s = getRealRadius(); BurstExecutor burst = burst().burst(); + burst.setMulticore(multicore); for (int i = -s; i <= s; i++) { int xx = i + x; @@ -216,7 +217,16 @@ public interface EngineMantle extends IObjectPlacer { { KList px = post.copy(); post.clear(); - burst().burst(px); + + if(multicore) + { + burst().burst(px); + } + + else + { + burst().sync(px); + } } } @@ -225,7 +235,7 @@ public interface EngineMantle extends IObjectPlacer { } @ChunkCoordinates - default void insertMatter(int x, int z, Class t, Hunk blocks) { + default void insertMatter(int x, int z, Class t, Hunk blocks, boolean multicore) { if (!getEngine().getDimension().isUseMantle()) { return; } diff --git a/src/main/java/com/volmit/iris/engine/modifier/IrisDepositModifier.java b/src/main/java/com/volmit/iris/engine/modifier/IrisDepositModifier.java index 2bf4694e2..c3402d5d6 100644 --- a/src/main/java/com/volmit/iris/engine/modifier/IrisDepositModifier.java +++ b/src/main/java/com/volmit/iris/engine/modifier/IrisDepositModifier.java @@ -18,6 +18,7 @@ package com.volmit.iris.engine.modifier; +import com.volmit.iris.Iris; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.EngineAssignedModifier; import com.volmit.iris.engine.object.biome.IrisBiome; @@ -25,8 +26,11 @@ import com.volmit.iris.engine.object.deposits.IrisDepositGenerator; import com.volmit.iris.engine.object.objects.IrisObject; import com.volmit.iris.engine.object.regional.IrisRegion; import com.volmit.iris.util.data.HeightMap; +import com.volmit.iris.util.format.Form; import com.volmit.iris.util.hunk.Hunk; import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.parallel.BurstExecutor; +import com.volmit.iris.util.parallel.MultiBurst; import com.volmit.iris.util.scheduling.PrecisionStopwatch; import org.bukkit.block.data.BlockData; import org.bukkit.util.BlockVector; @@ -42,30 +46,32 @@ public class IrisDepositModifier extends EngineAssignedModifier { @Override public void onModify(int x, int z, Hunk output, boolean multicore) { PrecisionStopwatch p = PrecisionStopwatch.start(); - generateDeposits(rng, output, Math.floorDiv(x, 16), Math.floorDiv(z, 16)); + generateDeposits(rng, output, Math.floorDiv(x, 16), Math.floorDiv(z, 16), multicore); getEngine().getMetrics().getDeposit().put(p.getMilliseconds()); } - public void generateDeposits(RNG rx, Hunk terrain, int x, int z) { + public void generateDeposits(RNG rx, Hunk terrain, int x, int z, boolean multicore) { RNG ro = rx.nextParallelRNG(x * x).nextParallelRNG(z * z); IrisRegion region = getComplex().getRegionStream().get((x * 16) + 7, (z * 16) + 7); IrisBiome biome = getComplex().getTrueBiomeStream().get((x * 16) + 7, (z * 16) + 7); - + BurstExecutor burst = burst().burst(); + burst.setMulticore(multicore); for (IrisDepositGenerator k : getDimension().getDeposits()) { - generate(k, terrain, ro, x, z, false); + burst.queue(() -> generate(k, terrain, ro, x, z, false)); } for (IrisDepositGenerator k : region.getDeposits()) { for (int l = 0; l < ro.i(k.getMinPerChunk(), k.getMaxPerChunk()); l++) { - generate(k, terrain, ro, x, z, false); + burst.queue(() -> generate(k, terrain, ro, x, z, false)); } } for (IrisDepositGenerator k : biome.getDeposits()) { for (int l = 0; l < ro.i(k.getMinPerChunk(), k.getMaxPerChunk()); l++) { - generate(k, terrain, ro, x, z, false); + burst.queue(() -> generate(k, terrain, ro, x, z, false)); } } + burst.complete(); } public void generate(IrisDepositGenerator k, Hunk data, RNG rng, int cx, int cz, boolean safe) { diff --git a/src/main/java/com/volmit/iris/engine/modifier/IrisPostModifier.java b/src/main/java/com/volmit/iris/engine/modifier/IrisPostModifier.java index d2a5bd818..fee01f68a 100644 --- a/src/main/java/com/volmit/iris/engine/modifier/IrisPostModifier.java +++ b/src/main/java/com/volmit/iris/engine/modifier/IrisPostModifier.java @@ -19,14 +19,26 @@ package com.volmit.iris.engine.modifier; import com.volmit.iris.Iris; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.data.cache.Cache; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.EngineAssignedModifier; import com.volmit.iris.engine.object.biome.IrisBiome; import com.volmit.iris.engine.object.common.CaveResult; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.data.B; +import com.volmit.iris.util.format.Form; +import com.volmit.iris.util.function.*; import com.volmit.iris.util.hunk.Hunk; +import com.volmit.iris.util.hunk.storage.ArrayHunk; +import com.volmit.iris.util.math.Average; import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.math.RollingSequence; +import com.volmit.iris.util.parallel.BurstExecutor; import com.volmit.iris.util.scheduling.PrecisionStopwatch; +import com.volmit.iris.util.stream.ProceduralStream; +import com.volmit.iris.util.stream.interpolation.Interpolated; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Levelled; @@ -34,9 +46,11 @@ import org.bukkit.block.data.Waterlogged; import org.bukkit.block.data.type.Slab; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; public class IrisPostModifier extends EngineAssignedModifier { - private static final BlockData AIR = B.get("CAVE_AIR"); + private static final BlockData AIR = B.get("AIR"); private static final BlockData WATER = B.get("WATER"); private final RNG rng; @@ -48,15 +62,22 @@ public class IrisPostModifier extends EngineAssignedModifier { @Override public void onModify(int x, int z, Hunk output, boolean multicore) { PrecisionStopwatch p = PrecisionStopwatch.start(); - int i; + AtomicInteger i = new AtomicInteger(); AtomicInteger j = new AtomicInteger(); - - for (i = 0; i < output.getWidth(); i++) { - for (j.set(0); j.get() < output.getDepth(); j.getAndIncrement()) { - post(i, j.get(), output, i + x, j.get() + z); - } + BurstExecutor burst = burst().burst(); + burst.setMulticore(multicore); + Hunk sync = output.synchronize(); + for (i.set(0); i.get() < output.getWidth(); i.getAndIncrement()) { + burst.queue(() -> { + for (j.set(0); j.get() < output.getDepth(); j.getAndIncrement()) { + int ii = i.get(); + int jj = j.get(); + post(ii, jj, sync, ii + x, jj + z); + } + }); } + burst.complete(); getEngine().getMetrics().getPost().put(p.getMilliseconds()); } diff --git a/src/main/java/com/volmit/iris/engine/object/basic/IrisRange.java b/src/main/java/com/volmit/iris/engine/object/basic/IrisRange.java index b065d2e62..e1c962e2b 100644 --- a/src/main/java/com/volmit/iris/engine/object/basic/IrisRange.java +++ b/src/main/java/com/volmit/iris/engine/object/basic/IrisRange.java @@ -44,4 +44,8 @@ public class IrisRange { return rng.d(min, max); } + + public boolean contains(int v) { + return v >= min && v <= max; + } } diff --git a/src/main/java/com/volmit/iris/engine/object/decoration/IrisDecorator.java b/src/main/java/com/volmit/iris/engine/object/decoration/IrisDecorator.java index f5795fafc..99ce1e2d7 100644 --- a/src/main/java/com/volmit/iris/engine/object/decoration/IrisDecorator.java +++ b/src/main/java/com/volmit/iris/engine/object/decoration/IrisDecorator.java @@ -107,7 +107,10 @@ public class IrisDecorator { return stackMin; } - return getHeightGenerator(rng, data).fit(stackMin, stackMax, x / heightVariance.getZoom(), z / heightVariance.getZoom()) + 1; + return getHeightGenerator(rng, data) + .fit(stackMin, stackMax, + x / heightVariance.getZoom(), + z / heightVariance.getZoom()) + 1; } public CNG getHeightGenerator(RNG rng, IrisData data) { diff --git a/src/main/java/com/volmit/iris/engine/object/entity/IrisEntitySpawn.java b/src/main/java/com/volmit/iris/engine/object/entity/IrisEntitySpawn.java index 8c2532115..3ae2d9d39 100644 --- a/src/main/java/com/volmit/iris/engine/object/entity/IrisEntitySpawn.java +++ b/src/main/java/com/volmit/iris/engine/object/entity/IrisEntitySpawn.java @@ -105,8 +105,24 @@ public class IrisEntitySpawn implements IRare { }; if (l != null) { - if (spawn100(gen, l) != null) - s++; + if(referenceSpawner.getAllowedLightLevels().getMin() > 0 || referenceSpawner.getAllowedLightLevels().getMax() < 15) + { + if(referenceSpawner.getAllowedLightLevels().contains(l.getBlock().getLightLevel())) + { + if (spawn100(gen, l) != null) + { + s++; + } + } + } + + else + { + if (spawn100(gen, l) != null) + { + s++; + } + } } } } diff --git a/src/main/java/com/volmit/iris/engine/object/spawners/IrisSpawner.java b/src/main/java/com/volmit/iris/engine/object/spawners/IrisSpawner.java index a7773979e..ece3c420c 100644 --- a/src/main/java/com/volmit/iris/engine/object/spawners/IrisSpawner.java +++ b/src/main/java/com/volmit/iris/engine/object/spawners/IrisSpawner.java @@ -21,6 +21,7 @@ package com.volmit.iris.engine.object.spawners; import com.volmit.iris.core.project.loader.IrisRegistrant; import com.volmit.iris.engine.object.annotations.ArrayType; import com.volmit.iris.engine.object.annotations.Desc; +import com.volmit.iris.engine.object.basic.IrisRange; import com.volmit.iris.engine.object.basic.IrisRate; import com.volmit.iris.engine.object.basic.IrisTimeBlock; import com.volmit.iris.engine.object.basic.IrisWeather; @@ -32,6 +33,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import org.bukkit.Bukkit; import org.bukkit.World; @EqualsAndHashCode(callSuper = true) @@ -67,6 +69,9 @@ public class IrisSpawner extends IrisRegistrant { @Desc("The maximum rate this spawner can fire on a specific chunk") private IrisRate maximumRatePerChunk = new IrisRate(); + @Desc("The light levels this spawn is allowed to run in (0-15 inclusive)") + private IrisRange allowedLightLevels = new IrisRange(0, 15); + @Desc("Where should these spawns be placed") private IrisSpawnGroup group = IrisSpawnGroup.NORMAL; diff --git a/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java b/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java index 7ec34124a..907dd6572 100644 --- a/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java +++ b/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java @@ -29,6 +29,7 @@ import com.volmit.iris.engine.framework.WrongEngineBroException; import com.volmit.iris.engine.object.common.IrisWorld; import com.volmit.iris.engine.object.dimensional.IrisDimension; import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.format.C; import com.volmit.iris.util.hunk.Hunk; import com.volmit.iris.util.io.ReactiveFolder; import com.volmit.iris.util.scheduling.ChronoLatch; @@ -47,6 +48,7 @@ import org.bukkit.generator.ChunkGenerator; import org.jetbrains.annotations.NotNull; import javax.management.RuntimeErrorException; +import javax.swing.text.TableView; import java.io.File; import java.util.List; import java.util.Random; @@ -57,7 +59,7 @@ import java.util.concurrent.Semaphore; public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator { private static final int LOAD_LOCKS = 1_000_000; private final Semaphore loadLock; - private final Engine engine; + private Engine engine; private final IrisWorld world; private final File dataLocation; private final String dimensionKey; @@ -66,9 +68,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun private final ChronoLatch hotloadChecker; private final Looper hotloader; private final boolean studio; + private long lastSeed; public BukkitChunkGenerator(IrisWorld world, boolean studio, File dataLocation, String dimensionKey) { populators = new KList<>(); + lastSeed = world.seed(); loadLock = new Semaphore(LOAD_LOCKS); this.world = world; this.hotloadChecker = new ChronoLatch(1000, false); @@ -76,6 +80,23 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun this.dataLocation = dataLocation; this.dimensionKey = dimensionKey; this.folder = new ReactiveFolder(dataLocation, (_a, _b, _c) -> hotload()); + setupEngine(); + this.hotloader = new Looper() { + @Override + protected long loop() { + if (hotloadChecker.flip()) { + folder.check(); + } + + return 250; + } + }; + hotloader.setPriority(Thread.MIN_PRIORITY); + hotloader.start(); + hotloader.setName(getTarget().getWorld().name() + " Hotloader"); + } + + private void setupEngine() { IrisData data = IrisData.get(dataLocation); IrisDimension dimension = data.getDimensionLoader().load(dimensionKey); @@ -113,21 +134,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun } } - this.engine = new IrisEngine(new EngineTarget(world, dimension, data), studio); + engine = new IrisEngine(new EngineTarget(world, dimension, data), studio); + populators.clear(); populators.add((BlockPopulator) engine); - this.hotloader = new Looper() { - @Override - protected long loop() { - if (hotloadChecker.flip()) { - folder.check(); - } - - return 250; - } - }; - hotloader.setPriority(Thread.MIN_PRIORITY); - hotloader.start(); - hotloader.setName(getTarget().getWorld().name() + " Hotloader"); } @Override @@ -161,7 +170,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun r.run(); loadLock.release(LOAD_LOCKS); } catch (Throwable e) { - e.printStackTrace(); + Iris.reportError(e); } }); } @@ -169,6 +178,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun @Override public @NotNull ChunkData generateChunkData(@NotNull World world, @NotNull Random ignored, int x, int z, @NotNull BiomeGrid biome) { try { + if(lastSeed != world.getSeed()) + { + Iris.warn("Seed for engine " + lastSeed + " does not match world seed if " + world.getSeed()); + lastSeed = world.getSeed(); + engine.getTarget().getWorld().seed(lastSeed); + engine.hotload(); + Iris.success("Updated Engine seed to " + lastSeed); + } + loadLock.acquire(); PrecisionStopwatch ps = PrecisionStopwatch.start(); TerrainChunk tc = TerrainChunk.create(world, biome); @@ -182,6 +200,26 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun return c; } + catch (WrongEngineBroException e) + { + Iris.warn("Trying to generate with a shut-down engine! Did you reload? Attempting to resolve this..."); + + try + { + setupEngine(); + Iris.success("Resolved! Should generate now!"); + } + + catch(Throwable fe) + { + Iris.error("FATAL! Iris cannot generate in this world since it was reloaded! This will cause a crash, with missing chunks, so we're crashing right now!"); + Bukkit.shutdown(); + throw new RuntimeException(); + } + + return generateChunkData(world, ignored, x, z, biome); + } + catch (Throwable e) { loadLock.release(); Iris.error("======================================"); diff --git a/src/main/java/com/volmit/iris/util/hunk/Hunk.java b/src/main/java/com/volmit/iris/util/hunk/Hunk.java index e81c9673c..291b2b0e1 100644 --- a/src/main/java/com/volmit/iris/util/hunk/Hunk.java +++ b/src/main/java/com/volmit/iris/util/hunk/Hunk.java @@ -1449,4 +1449,9 @@ public interface Hunk { default boolean isEmpty() { return false; } + + default boolean contains(int x, int y, int z) + { + return x < getWidth() && x >= 0 && y < getHeight() && y >= 0 && z < getDepth() && z >= 0; + } } diff --git a/src/main/java/com/volmit/iris/util/mantle/Mantle.java b/src/main/java/com/volmit/iris/util/mantle/Mantle.java index afdae0de8..06a8a8e4c 100644 --- a/src/main/java/com/volmit/iris/util/mantle/Mantle.java +++ b/src/main/java/com/volmit/iris/util/mantle/Mantle.java @@ -111,6 +111,17 @@ public class Mantle { get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).iterate(type, iterator); } + @ChunkCoordinates + public void iterateChunk(int x, int z, Class type, Consumer4 iterator, BurstExecutor e, MantleFlag... requiredFlags) { + for (MantleFlag i : requiredFlags) { + if (!hasFlag(x, z, i)) { + return; + } + } + + get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).iterate(type, iterator, e); + } + @ChunkCoordinates public boolean hasFlag(int x, int z, MantleFlag flag) { return get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31).isFlagged(flag); @@ -207,7 +218,16 @@ public class Mantle { }); } - b.complete(); + try + { + b.complete(); + } + + catch(Throwable e) + { + Iris.reportError(e); + } + ioBurst.shutdownNow(); Iris.debug("The Mantle has Closed " + C.DARK_AQUA + dataFolder.getAbsolutePath()); } diff --git a/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java b/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java index ba3940d2a..85d248508 100644 --- a/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java +++ b/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java @@ -23,6 +23,8 @@ import com.volmit.iris.util.function.Consumer4; import com.volmit.iris.util.matter.IrisMatter; import com.volmit.iris.util.matter.Matter; import com.volmit.iris.util.matter.MatterSlice; +import com.volmit.iris.util.parallel.BurstExecutor; +import com.volmit.iris.util.parallel.MultiBurst; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -184,6 +186,26 @@ public class MantleChunk { } } + public void iterate(Class type, Consumer4 iterator, BurstExecutor burst) { + for (int i = 0; i < sections.length(); i++) { + int finalI = i; + burst.queue(() -> { + int bs = (finalI << 4); + Matter matter = get(finalI); + + if (matter != null) { + MatterSlice t = matter.getSlice(type); + + if (t != null) { + t.iterateSync((a, b, c, f) -> iterator.accept(a, b + bs, c, f)); + } + } + }); + } + + burst.complete(); + } + public void iterate(Class type, Consumer4 iterator) { for (int i = 0; i < sections.length(); i++) { int bs = (i << 4); diff --git a/src/main/java/com/volmit/iris/util/parallel/BurstExecutor.java b/src/main/java/com/volmit/iris/util/parallel/BurstExecutor.java index 642e15f6c..ef6935a97 100644 --- a/src/main/java/com/volmit/iris/util/parallel/BurstExecutor.java +++ b/src/main/java/com/volmit/iris/util/parallel/BurstExecutor.java @@ -20,6 +20,7 @@ package com.volmit.iris.util.parallel; import com.volmit.iris.Iris; import com.volmit.iris.util.collection.KList; +import lombok.Setter; import java.util.List; import java.util.concurrent.*; @@ -28,6 +29,8 @@ import java.util.concurrent.*; public class BurstExecutor { private final ExecutorService executor; private final KList> futures; + @Setter + private boolean multicore = true; public BurstExecutor(ExecutorService executor, int burstSizeEstimate) { this.executor = executor; @@ -36,6 +39,12 @@ public class BurstExecutor { @SuppressWarnings("UnusedReturnValue") public CompletableFuture queue(Runnable r) { + if(!multicore) + { + r.run(); + return null; + } + synchronized (futures) { CompletableFuture c = CompletableFuture.runAsync(r, executor); futures.add(c); @@ -44,6 +53,16 @@ public class BurstExecutor { } public BurstExecutor queue(List r) { + if(!multicore) + { + for(Runnable i : r) + { + i.run(); + } + + return this; + } + synchronized (futures) { for (Runnable i : new KList<>(r)) { CompletableFuture c = CompletableFuture.runAsync(i, executor); @@ -55,6 +74,16 @@ public class BurstExecutor { } public BurstExecutor queue(Runnable[] r) { + if(!multicore) + { + for(Runnable i : r) + { + i.run(); + } + + return this; + } + synchronized (futures) { for (Runnable i : r) { CompletableFuture c = CompletableFuture.runAsync(i, executor); @@ -66,6 +95,11 @@ public class BurstExecutor { } public void complete() { + if(!multicore) + { + return; + } + synchronized (futures) { if (futures.isEmpty()) { return; @@ -75,13 +109,17 @@ public class BurstExecutor { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); futures.clear(); } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); Iris.reportError(e); } } } public boolean complete(long maxDur) { + if(!multicore) + { + return true; + } + synchronized (futures) { if (futures.isEmpty()) { return true; diff --git a/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java b/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java index 29440720a..92a07825e 100644 --- a/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java +++ b/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java @@ -21,6 +21,7 @@ package com.volmit.iris.util.parallel; import com.volmit.iris.Iris; import com.volmit.iris.core.IrisSettings; import com.volmit.iris.core.service.PreservationSVC; +import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.io.InstanceState; import com.volmit.iris.util.math.M; import com.volmit.iris.util.scheduling.J; @@ -111,6 +112,12 @@ public class MultiBurst { } } + public void sync(KList r) { + for (Runnable i : r) { + i.run(); + } + } + public BurstExecutor burst(int estimate) { return new BurstExecutor(getService(), estimate); } @@ -159,16 +166,30 @@ public class MultiBurst { public void shutdownLater() { if (service != null) { - service.submit(() -> { - J.sleep(3000); + try + { + service.submit(() -> { + J.sleep(3000); + Iris.debug("Shutting down MultiBurst Pool " + heartbeat.getName() + "."); + + if (service != null) { + service.shutdown(); + } + }); + + heartbeat.interrupt(); + } + + catch(Throwable e) + { Iris.debug("Shutting down MultiBurst Pool " + heartbeat.getName() + "."); if (service != null) { service.shutdown(); } - }); - heartbeat.interrupt(); + heartbeat.interrupt(); + } } } diff --git a/src/main/java/com/volmit/iris/util/plugin/IrisService.java b/src/main/java/com/volmit/iris/util/plugin/IrisService.java index 34c222de4..b9068f79e 100644 --- a/src/main/java/com/volmit/iris/util/plugin/IrisService.java +++ b/src/main/java/com/volmit/iris/util/plugin/IrisService.java @@ -18,10 +18,16 @@ package com.volmit.iris.util.plugin; +import com.volmit.iris.Iris; import org.bukkit.event.Listener; public interface IrisService extends Listener { void onEnable(); void onDisable(); + + default void postShutdown(Runnable r) + { + Iris.instance.postShutdown(r); + } } diff --git a/src/main/java/com/volmit/iris/util/plugin/VolmitSender.java b/src/main/java/com/volmit/iris/util/plugin/VolmitSender.java index 73e1cee1e..25912ef04 100644 --- a/src/main/java/com/volmit/iris/util/plugin/VolmitSender.java +++ b/src/main/java/com/volmit/iris/util/plugin/VolmitSender.java @@ -226,7 +226,7 @@ public class VolmitSender implements CommandSender { } public static String pulse(double speed) { - return Form.f(invertSpread((((getTick() * 15D * speed) % 1000D) / 1000D)), 3).replaceAll("\\Q,\\E", "."); + return Form.f(invertSpread((((getTick() * 15D * speed) % 1000D) / 1000D)), 3).replaceAll("\\Q,\\E", ".").replaceAll("\\Q?\\E", "-"); } public static double invertSpread(double v) {