Iris/src/main/java/com/volmit/iris/engine/IrisEngine.java
2023-01-17 12:35:03 -05:00

523 lines
17 KiB
Java

/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.engine;
import com.google.common.util.concurrent.AtomicDouble;
import com.google.gson.Gson;
import com.volmit.iris.Iris;
import com.volmit.iris.core.ServerConfigurator;
import com.volmit.iris.core.events.IrisEngineHotloadEvent;
import com.volmit.iris.core.gui.PregeneratorJob;
import com.volmit.iris.core.project.IrisProject;
import com.volmit.iris.core.service.PreservationSVC;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.framework.*;
import com.volmit.iris.engine.mantle.EngineMantle;
import com.volmit.iris.engine.object.*;
import com.volmit.iris.engine.scripting.EngineExecutionEnvironment;
import com.volmit.iris.util.atomics.AtomicRollingSequence;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.context.IrisContext;
import com.volmit.iris.util.documentation.BlockCoordinates;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.hunk.Hunk;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.mantle.MantleFlag;
import com.volmit.iris.util.math.M;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.matter.MatterStructurePOI;
import com.volmit.iris.util.scheduling.ChronoLatch;
import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
import lombok.Data;
import net.minecraft.core.BlockPos;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.CommandSender;
import oshi.util.tuples.Pair;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Data
public class IrisEngine implements Engine {
private final AtomicInteger bud;
private final AtomicInteger buds;
private final AtomicInteger generated;
private final AtomicInteger generatedLast;
private final AtomicDouble perSecond;
private final AtomicLong lastGPS;
private final EngineTarget target;
private final IrisContext context;
private final EngineMantle mantle;
private final ChronoLatch perSecondLatch;
private final ChronoLatch perSecondBudLatch;
private final EngineMetrics metrics;
private final boolean studio;
private final AtomicRollingSequence wallClock;
private final int art;
private final AtomicCache<IrisEngineData> engineData = new AtomicCache<>();
private final AtomicBoolean cleaning;
private final ChronoLatch cleanLatch;
private final SeedManager seedManager;
private EngineMode mode;
private EngineEffects effects;
private EngineExecutionEnvironment execution;
private EngineWorldManager worldManager;
private volatile int parallelism;
private volatile int minHeight;
private boolean failing;
private boolean closed;
private int cacheId;
private double maxBiomeObjectDensity;
private double maxBiomeLayerDensity;
private double maxBiomeDecoratorDensity;
private IrisComplex complex;
public IrisEngine(EngineTarget target, boolean studio) {
this.studio = studio;
this.target = target;
getEngineData();
verifySeed();
this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed());
bud = new AtomicInteger(0);
buds = new AtomicInteger(0);
metrics = new EngineMetrics(32);
cleanLatch = new ChronoLatch(10000);
generatedLast = new AtomicInteger(0);
perSecond = new AtomicDouble(0);
perSecondLatch = new ChronoLatch(1000, false);
perSecondBudLatch = new ChronoLatch(1000, false);
wallClock = new AtomicRollingSequence(32);
lastGPS = new AtomicLong(M.ms());
generated = new AtomicInteger(0);
mantle = new IrisEngineMantle(this);
context = new IrisContext(this);
cleaning = new AtomicBoolean(false);
context.touch();
getData().setEngine(this);
getData().loadPrefetch(this);
Iris.info("Initializing Engine: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " (" + target.getDimension().getDimensionHeight() + " height) Seed: " + getSeedManager().getSeed());
minHeight = 0;
failing = false;
closed = false;
art = J.ar(this::tickRandomPlayer, 0);
setupEngine();
Iris.debug("Engine Initialized " + getCacheID());
}
private void verifySeed() {
if (getEngineData().getSeed() != null && getEngineData().getSeed() != target.getWorld().getRawWorldSeed()) {
target.getWorld().setRawWorldSeed(getEngineData().getSeed());
}
}
private void tickRandomPlayer() {
recycle();
if (perSecondBudLatch.flip()) {
buds.set(bud.get());
bud.set(0);
}
if (effects != null) {
effects.tickRandomPlayer();
}
}
private void prehotload() {
worldManager.close();
complex.close();
execution.close();
effects.close();
mode.close();
J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace());
}
private void setupEngine() {
try {
Iris.debug("Setup Engine " + getCacheID());
cacheId = RNG.r.nextInt();
worldManager = new IrisWorldManager(this);
complex = new IrisComplex(this);
execution = new IrisExecutionEnvironment(this);
effects = new IrisEngineEffects(this);
setupMode();
J.a(this::computeBiomeMaxes);
} catch (Throwable e) {
Iris.error("FAILED TO SETUP ENGINE!");
e.printStackTrace();
}
Iris.debug("Engine Setup Complete " + getCacheID());
}
private void setupMode() {
if (mode != null) {
mode.close();
}
mode = getDimension().getMode().getType().create(this);
}
@Override
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateMatter(x, z, multicore, context);
}
@Override
public Set<String> getObjectsAt(int x, int z) {
return getMantle().getObjectComponent().guess(x, z);
}
@Override
public Set<Pair<String, BlockPos>> getPOIsAt(int chunkX, int chunkY) {
Set<Pair<String, BlockPos>> pois = new HashSet<>();
getMantle().getMantle().iterateChunk(chunkX, chunkY, MatterStructurePOI.class, (x, y, z, d) -> pois.add(new Pair<>(d.getType(), new BlockPos(x, y, z))));
return pois;
}
@Override
public IrisJigsawStructure getStructureAt(int x, int z) {
return getMantle().getJigsawComponent().guess(x, z);
}
private void warmupChunk(int x, int z) {
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
int xx = x + (i << 4);
int zz = z + (z << 4);
getComplex().getTrueBiomeStream().get(xx, zz);
getComplex().getHeightStream().get(xx, zz);
}
}
}
@Override
public void hotload() {
hotloadSilently();
Iris.callEvent(new IrisEngineHotloadEvent(this));
}
public void hotloadComplex() {
complex.close();
complex = new IrisComplex(this);
}
public void hotloadSilently() {
getData().dump();
getData().clearLists();
getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey()));
prehotload();
setupEngine();
J.a(() -> {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
});
}
@Override
public IrisEngineData getEngineData() {
return engineData.aquire(() -> {
//TODO: Method this file
File f = new File(getWorld().worldFolder(), "iris/engine-data/" + getDimension().getLoadKey() + ".json");
if (!f.exists()) {
try {
f.getParentFile().mkdirs();
IO.writeAll(f, new Gson().toJson(new IrisEngineData()));
} catch (IOException e) {
e.printStackTrace();
}
}
try {
return new Gson().fromJson(IO.readAll(f), IrisEngineData.class);
} catch (Throwable e) {
e.printStackTrace();
}
return new IrisEngineData();
});
}
@Override
public int getGenerated() {
return generated.get();
}
@Override
public double getGeneratedPerSecond() {
if (perSecondLatch.flip()) {
double g = generated.get() - generatedLast.get();
generatedLast.set(generated.get());
if (g == 0) {
return 0;
}
long dur = M.ms() - lastGPS.get();
lastGPS.set(M.ms());
perSecond.set(g / ((double) (dur) / 1000D));
}
return perSecond.get();
}
@Override
public boolean isStudio() {
return studio;
}
private void computeBiomeMaxes() {
for (IrisBiome i : getDimension().getAllBiomes(this)) {
double density = 0;
for (IrisObjectPlacement j : i.getObjects()) {
density += j.getDensity() * j.getChance();
}
maxBiomeObjectDensity = Math.max(maxBiomeObjectDensity, density);
density = 0;
for (IrisDecorator j : i.getDecorators()) {
density += Math.max(j.getStackMax(), 1) * j.getChance();
}
maxBiomeDecoratorDensity = Math.max(maxBiomeDecoratorDensity, density);
density = 0;
for (IrisBiomePaletteLayer j : i.getLayers()) {
density++;
}
maxBiomeLayerDensity = Math.max(maxBiomeLayerDensity, density);
}
}
@Override
public int getBlockUpdatesPerSecond() {
return buds.get();
}
public void printMetrics(CommandSender sender) {
KMap<String, Double> totals = new KMap<>();
KMap<String, Double> weights = new KMap<>();
double masterWallClock = wallClock.getAverage();
KMap<String, Double> timings = getMetrics().pull();
double totalWeight = 0;
double wallClock = getMetrics().getTotal().getAverage();
for (double j : timings.values()) {
totalWeight += j;
}
for (String j : timings.k()) {
weights.put(getName() + "." + j, (wallClock / totalWeight) * timings.get(j));
}
totals.put(getName(), wallClock);
double mtotals = 0;
for (double i : totals.values()) {
mtotals += i;
}
for (String i : totals.k()) {
totals.put(i, (masterWallClock / mtotals) * totals.get(i));
}
double v = 0;
for (double i : weights.values()) {
v += i;
}
for (String i : weights.k()) {
weights.put(i, weights.get(i) / v);
}
sender.sendMessage("Total: " + C.BOLD + C.WHITE + Form.duration(masterWallClock, 0));
for (String i : totals.k()) {
sender.sendMessage(" Engine " + C.UNDERLINE + C.GREEN + i + C.RESET + ": " + C.BOLD + C.WHITE + Form.duration(totals.get(i), 0));
}
sender.sendMessage("Details: ");
for (String i : weights.sortKNumber().reverse()) {
String befb = C.UNDERLINE + "" + C.GREEN + "" + i.split("\\Q[\\E")[0] + C.RESET + C.GRAY + "[";
String num = C.GOLD + i.split("\\Q[\\E")[1].split("]")[0] + C.RESET + C.GRAY + "].";
String afb = C.ITALIC + "" + C.AQUA + i.split("\\Q]\\E")[1].substring(1) + C.RESET + C.GRAY;
sender.sendMessage(" " + befb + num + afb + ": " + C.BOLD + C.WHITE + Form.pc(weights.get(i), 0));
}
}
@Override
public void close() {
PregeneratorJob.shutdownInstance();
closed = true;
J.car(art);
getWorldManager().close();
getTarget().close();
saveEngineData();
getMantle().close();
getComplex().close();
mode.close();
getData().dump();
getData().clearLists();
Iris.service(PreservationSVC.class).dereference();
Iris.debug("Engine Fully Shutdown!");
complex = null;
}
@Override
public boolean isClosed() {
return closed;
}
@Override
public void recycle() {
if (!cleanLatch.flip()) {
return;
}
if (cleaning.get()) {
cleanLatch.flipDown();
return;
}
cleaning.set(true);
J.a(() -> {
try {
getMantle().trim();
getData().getObjectLoader().clean();
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Cleanup failed! Enable debug to see stacktrace.");
}
cleaning.lazySet(false);
});
}
@BlockCoordinates
@Override
public void generate(int x, int z, Hunk<BlockData> vblocks, Hunk<Biome> vbiomes, boolean multicore) throws WrongEngineBroException {
if (closed) {
throw new WrongEngineBroException();
}
context.touch();
getEngineData().getStatistics().generatedChunk();
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
Hunk<BlockData> blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y + getMinHeight(), z + zz, t));
if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) {
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
blocks.set(i, 0, j, Material.CRYING_OBSIDIAN.createBlockData());
}
}
} else {
mode.generate(x, z, blocks, vbiomes, multicore);
}
getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true);
getMetrics().getTotal().put(p.getMilliseconds());
generated.incrementAndGet();
if (generated.get() == 661) {
J.a(() -> getData().savePrefetch(this));
}
} catch (Throwable e) {
Iris.reportError(e);
fail("Failed to generate " + x + ", " + z, e);
}
}
@Override
public void saveEngineData() {
//TODO: Method this file
File f = new File(getWorld().worldFolder(), "iris/engine-data/" + getDimension().getLoadKey() + ".json");
f.getParentFile().mkdirs();
try {
IO.writeAll(f, new Gson().toJson(getEngineData()));
Iris.debug("Saved Engine Data");
} catch (IOException e) {
Iris.error("Failed to save Engine Data");
e.printStackTrace();
}
}
@Override
public void blockUpdatedMetric() {
bud.incrementAndGet();
}
@Override
public IrisBiome getFocus() {
if (getDimension().getFocus() == null || getDimension().getFocus().trim().isEmpty()) {
return null;
}
return getData().getBiomeLoader().load(getDimension().getFocus());
}
@Override
public IrisRegion getFocusRegion() {
if (getDimension().getFocusRegion() == null || getDimension().getFocusRegion().trim().isEmpty()) {
return null;
}
return getData().getRegionLoader().load(getDimension().getFocusRegion());
}
@Override
public void fail(String error, Throwable e) {
failing = true;
Iris.error(error);
e.printStackTrace();
}
@Override
public boolean hasFailed() {
return failing;
}
@Override
public int getCacheID() {
return cacheId;
}
}