mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-05-19 16:10:42 +00:00
Object Studio and Plausibilizer 1.0
This commit is contained in:
@@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.commands;
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.ExternalDataPackPipeline;
|
import art.arcane.iris.core.ExternalDataPackPipeline;
|
||||||
|
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -101,6 +103,18 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Player studioPlayer = player();
|
||||||
|
if (studioPlayer != null) {
|
||||||
|
try {
|
||||||
|
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
|
||||||
|
sender().sendMessage(C.GREEN + "Object Studio: teleporting to " + object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Iris.reportError(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e.hasObjectPlacement(object)) {
|
if (e.hasObjectPlacement(object)) {
|
||||||
e.gotoObject(object, player(), teleport);
|
e.gotoObject(object, player(), teleport);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -21,16 +21,19 @@ package art.arcane.iris.core.commands;
|
|||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.link.WorldEditLink;
|
import art.arcane.iris.core.link.WorldEditLink;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
import art.arcane.iris.core.service.ObjectSVC;
|
import art.arcane.iris.core.service.ObjectSVC;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
import art.arcane.iris.core.service.WandSVC;
|
import art.arcane.iris.core.service.WandSVC;
|
||||||
import art.arcane.iris.core.tools.IrisConverter;
|
import art.arcane.iris.core.tools.IrisConverter;
|
||||||
|
import art.arcane.iris.core.tools.TreePlausibilizer;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.*;
|
import art.arcane.iris.engine.object.*;
|
||||||
import art.arcane.volmlib.util.data.Cuboid;
|
import art.arcane.volmlib.util.data.Cuboid;
|
||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.iris.util.common.data.registry.Materials;
|
import art.arcane.iris.util.common.data.registry.Materials;
|
||||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||||
import art.arcane.volmlib.util.director.annotations.Director;
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
@@ -222,6 +225,220 @@ public class CommandObject implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Bridge unreachable leaves with hidden logs so trees are vanilla-decay-plausible",
|
||||||
|
origin = DirectorOrigin.BOTH, studio = false)
|
||||||
|
public void plausibilize(
|
||||||
|
@Param(description = "Pack key (trees/bonsai/smbase1), pack prefix (trees/), or filesystem path to a .iob file or directory")
|
||||||
|
String target,
|
||||||
|
@Param(description = "Analyze only, do not write", defaultValue = "false")
|
||||||
|
boolean dryRun,
|
||||||
|
@Param(description = "Flip persistent=true leaves to false and bridge them as well",
|
||||||
|
defaultValue = "false")
|
||||||
|
boolean normalize,
|
||||||
|
@Param(description = "Wipe scattered leaves and repaint a canopy shell around every log, then bridge any gaps with interior log tendrils",
|
||||||
|
defaultValue = "false")
|
||||||
|
boolean smoke,
|
||||||
|
@Param(description = "Canopy shell radius (smoke mode only), clamped to [0,5]",
|
||||||
|
defaultValue = "2")
|
||||||
|
int radius
|
||||||
|
) {
|
||||||
|
List<Target> targets = resolveTargets(target);
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
sender().sendMessage(C.RED + "No objects matched: " + target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender().sendMessage(C.IRIS + "Plausibilize: queued " + targets.size() + " object(s)"
|
||||||
|
+ (dryRun ? " [DRY RUN]" : "")
|
||||||
|
+ (normalize ? " [NORMALIZE]" : "")
|
||||||
|
+ (smoke ? " [SMOKE r=" + radius + "]" : ""));
|
||||||
|
|
||||||
|
org.bukkit.command.CommandSender s = sender();
|
||||||
|
J.a(() -> runPlausibilize(targets, dryRun, normalize, smoke, radius, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Target> resolveTargets(String target) {
|
||||||
|
List<Target> out = new ArrayList<>();
|
||||||
|
if (target == null || target.isEmpty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
File direct = new File(target);
|
||||||
|
if (direct.isFile() && target.toLowerCase().endsWith(".iob")) {
|
||||||
|
out.add(new Target(direct.getName().replaceAll("\\.iob$", ""), direct));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (direct.isDirectory()) {
|
||||||
|
walkIob(direct, direct, out);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisData irisData = data();
|
||||||
|
if (irisData != null) {
|
||||||
|
ResourceLoader<IrisObject> loader = irisData.getObjectLoader();
|
||||||
|
if (!target.endsWith("/") && loader.findFile(target) != null) {
|
||||||
|
out.add(new Target(target, null));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
String prefix = target.endsWith("/") ? target : target + "/";
|
||||||
|
for (String k : loader.getPossibleKeys()) {
|
||||||
|
if (k.startsWith(prefix)) {
|
||||||
|
out.add(new Target(k, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
File packsFolder = Iris.instance.getDataFolder("packs");
|
||||||
|
File[] packs = packsFolder.listFiles(File::isDirectory);
|
||||||
|
if (packs != null) {
|
||||||
|
for (File pack : packs) {
|
||||||
|
File objectsRoot = new File(pack, "objects");
|
||||||
|
if (!objectsRoot.isDirectory()) continue;
|
||||||
|
File candidate = new File(objectsRoot, target + ".iob");
|
||||||
|
if (candidate.isFile()) {
|
||||||
|
out.add(new Target(pack.getName() + "/" + target, candidate));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
File candidateDir = new File(objectsRoot, target);
|
||||||
|
if (candidateDir.isDirectory()) {
|
||||||
|
walkIob(candidateDir, objectsRoot, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void walkIob(File root, File keyRoot, List<Target> out) {
|
||||||
|
File[] kids = root.listFiles();
|
||||||
|
if (kids == null) return;
|
||||||
|
for (File f : kids) {
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
walkIob(f, keyRoot, out);
|
||||||
|
} else if (f.getName().toLowerCase().endsWith(".iob")) {
|
||||||
|
String rel = keyRoot.toPath().relativize(f.toPath()).toString()
|
||||||
|
.replace(File.separatorChar, '/')
|
||||||
|
.replaceAll("\\.iob$", "");
|
||||||
|
out.add(new Target(rel, f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Target(String key, File file) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runPlausibilize(
|
||||||
|
List<Target> targets,
|
||||||
|
boolean dryRun,
|
||||||
|
boolean normalize,
|
||||||
|
boolean smoke,
|
||||||
|
int radius,
|
||||||
|
org.bukkit.command.CommandSender s
|
||||||
|
) {
|
||||||
|
int processed = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
int failed = 0;
|
||||||
|
int changed = 0;
|
||||||
|
long totalLogsAdded = 0L;
|
||||||
|
long totalReachableBefore = 0L;
|
||||||
|
long totalLeaves = 0L;
|
||||||
|
long totalPersistentInput = 0L;
|
||||||
|
long totalLeavesAdded = 0L;
|
||||||
|
long totalLeavesRemoved = 0L;
|
||||||
|
long totalNormalized = 0L;
|
||||||
|
long totalUnreachableAfter = 0L;
|
||||||
|
|
||||||
|
int progressStep = Math.max(1, targets.size() / 20);
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
for (Target t : targets) {
|
||||||
|
index++;
|
||||||
|
try {
|
||||||
|
IrisObject o = loadTarget(t);
|
||||||
|
if (o == null) {
|
||||||
|
s.sendMessage(C.YELLOW + " skip " + t.key() + ": failed to load");
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreePlausibilizer.Result r = dryRun
|
||||||
|
? TreePlausibilizer.analyze(o, normalize, smoke, radius)
|
||||||
|
: TreePlausibilizer.apply(o, normalize, smoke, radius);
|
||||||
|
|
||||||
|
if (r.skipReason() != null) {
|
||||||
|
s.sendMessage(C.YELLOW + " skip " + t.key() + ": " + r.skipReason());
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean touched = r.logsAdded() > 0 || r.leavesAdded() > 0
|
||||||
|
|| r.leavesRemoved() > 0 || r.leavesNormalized() > 0;
|
||||||
|
if (!dryRun && touched) {
|
||||||
|
File dest = o.getLoadFile() != null ? o.getLoadFile() : t.file();
|
||||||
|
if (dest != null) {
|
||||||
|
o.write(dest);
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processed++;
|
||||||
|
totalLogsAdded += r.logsAdded();
|
||||||
|
totalReachableBefore += r.reachableBefore();
|
||||||
|
totalLeaves += r.totalLeaves();
|
||||||
|
totalPersistentInput += r.persistentLeavesInput();
|
||||||
|
totalLeavesAdded += r.leavesAdded();
|
||||||
|
totalLeavesRemoved += r.leavesRemoved();
|
||||||
|
totalNormalized += r.leavesNormalized();
|
||||||
|
totalUnreachableAfter += r.unreachableAfter();
|
||||||
|
|
||||||
|
if (touched || targets.size() == 1) {
|
||||||
|
s.sendMessage(C.GRAY + " " + t.key()
|
||||||
|
+ " leaves=" + r.totalLeaves()
|
||||||
|
+ " persistentIn=" + r.persistentLeavesInput()
|
||||||
|
+ " reachable=" + r.reachableBefore()
|
||||||
|
+ " logsAdded=" + r.logsAdded()
|
||||||
|
+ " leavesAdded=" + r.leavesAdded()
|
||||||
|
+ " leavesRemoved=" + r.leavesRemoved()
|
||||||
|
+ " normalized=" + r.leavesNormalized()
|
||||||
|
+ " remaining=" + r.unreachableAfter());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.size() > 1 && index % progressStep == 0) {
|
||||||
|
s.sendMessage(C.IRIS + " [" + index + "/" + targets.size() + "]");
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
s.sendMessage(C.RED + " fail " + t.key() + ": " + e.getClass().getSimpleName()
|
||||||
|
+ ": " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sendMessage(C.IRIS + "Done."
|
||||||
|
+ " processed=" + processed
|
||||||
|
+ " changed=" + changed
|
||||||
|
+ " skipped=" + skipped
|
||||||
|
+ " failed=" + failed
|
||||||
|
+ " leaves=" + totalLeaves
|
||||||
|
+ " persistentIn=" + totalPersistentInput
|
||||||
|
+ " reachableBefore=" + totalReachableBefore
|
||||||
|
+ " logsAdded=" + totalLogsAdded
|
||||||
|
+ " leavesAdded=" + totalLeavesAdded
|
||||||
|
+ " leavesRemoved=" + totalLeavesRemoved
|
||||||
|
+ " normalized=" + totalNormalized
|
||||||
|
+ " remaining=" + totalUnreachableAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisObject loadTarget(Target t) throws IOException {
|
||||||
|
if (t.file() != null) {
|
||||||
|
IrisObject o = new IrisObject();
|
||||||
|
o.read(t.file());
|
||||||
|
o.setLoadFile(t.file());
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
return IrisData.loadAnyObject(t.key(), null);
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.")
|
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.")
|
||||||
public void convert () {
|
public void convert () {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
|||||||
import art.arcane.iris.core.gui.VisionGUI;
|
import art.arcane.iris.core.gui.VisionGUI;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.project.IrisProject;
|
import art.arcane.iris.core.project.IrisProject;
|
||||||
|
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
@@ -123,6 +125,101 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
|
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"obj", "objs"}, sync = true)
|
||||||
|
public void object(
|
||||||
|
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class)
|
||||||
|
IrisDimension dimension,
|
||||||
|
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
|
||||||
|
long seed
|
||||||
|
) {
|
||||||
|
VolmitSender commandSender = sender();
|
||||||
|
java.util.Map<String, IrisData> sources = new java.util.LinkedHashMap<>();
|
||||||
|
IrisDimension hostDimension = dimension;
|
||||||
|
|
||||||
|
if (dimension != null) {
|
||||||
|
IrisData data = dimension.getLoader();
|
||||||
|
if (data == null) {
|
||||||
|
data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile());
|
||||||
|
}
|
||||||
|
sources.put(data.getDataFolder().getName(), data);
|
||||||
|
} else {
|
||||||
|
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
||||||
|
File[] packs = workspace == null ? null : workspace.listFiles();
|
||||||
|
if (packs != null) {
|
||||||
|
Arrays.sort(packs, java.util.Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
|
||||||
|
for (File pack : packs) {
|
||||||
|
if (!pack.isDirectory()) continue;
|
||||||
|
File dimensionsDir = new File(pack, "dimensions");
|
||||||
|
if (!dimensionsDir.isDirectory()) continue;
|
||||||
|
IrisData data = IrisData.get(pack);
|
||||||
|
String[] keys = data.getObjectLoader().getPossibleKeys();
|
||||||
|
if (keys == null || keys.length == 0) continue;
|
||||||
|
sources.put(pack.getName(), data);
|
||||||
|
if (hostDimension == null) {
|
||||||
|
File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json"));
|
||||||
|
if (dimFiles != null && dimFiles.length > 0) {
|
||||||
|
String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", "");
|
||||||
|
IrisDimension loaded = data.getDimensionLoader().load(loadKey);
|
||||||
|
if (loaded != null) {
|
||||||
|
hostDimension = loaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostDimension == null || sources.isEmpty()) {
|
||||||
|
commandSender.sendMessage(C.RED + "No packs with objects were found on this server.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalObjects = 0;
|
||||||
|
for (IrisData d : sources.values()) {
|
||||||
|
String[] k = d.getObjectLoader().getPossibleKeys();
|
||||||
|
if (k != null) totalObjects += k.length;
|
||||||
|
}
|
||||||
|
if (totalObjects == 0) {
|
||||||
|
commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
|
||||||
|
ObjectStudioActivation.activate(hostDimension.getLoadKey());
|
||||||
|
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
|
||||||
|
|
||||||
|
String scope = dimension == null
|
||||||
|
? ("all packs [" + sources.size() + "]")
|
||||||
|
: ("\"" + hostDimension.getName() + "\"");
|
||||||
|
commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " ("
|
||||||
|
+ totalObjects + " objects)");
|
||||||
|
|
||||||
|
IrisDimension finalHost = hostDimension;
|
||||||
|
try {
|
||||||
|
Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> {
|
||||||
|
if (world == null) return;
|
||||||
|
try {
|
||||||
|
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandSender.isPlayer()) {
|
||||||
|
Player p = commandSender.player();
|
||||||
|
if (p != null) {
|
||||||
|
Location target = new Location(world, 0.5D, 66D, 0.5D);
|
||||||
|
J.runEntity(p, () -> {
|
||||||
|
PaperLib.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
|
||||||
|
commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
|
@Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
|
||||||
public void vscode(
|
public void vscode(
|
||||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
|
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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 art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class ObjectStudioActivation {
|
||||||
|
private static final Set<String> ACTIVE = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
private static final Map<String, Map<String, IrisData>> SOURCES = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private ObjectStudioActivation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void activate(String packKey) {
|
||||||
|
if (packKey == null) return;
|
||||||
|
ACTIVE.add(normalize(packKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deactivate(String packKey) {
|
||||||
|
if (packKey == null) return;
|
||||||
|
String norm = normalize(packKey);
|
||||||
|
ACTIVE.remove(norm);
|
||||||
|
SOURCES.remove(norm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isActive(String packKey) {
|
||||||
|
if (packKey == null) return false;
|
||||||
|
return ACTIVE.contains(normalize(packKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setSources(String packKey, Map<String, IrisData> sources) {
|
||||||
|
if (packKey == null || sources == null || sources.isEmpty()) return;
|
||||||
|
SOURCES.put(normalize(packKey), new LinkedHashMap<>(sources));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, IrisData> getSources(String packKey) {
|
||||||
|
if (packKey == null) return null;
|
||||||
|
return SOURCES.get(normalize(packKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String key) {
|
||||||
|
return key.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* 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 art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
import org.bukkit.util.BlockVector;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class ObjectStudioLayout {
|
||||||
|
public static final int FLOOR_Y = 64;
|
||||||
|
private static final int DEFAULT_ROW_WIDTH_CAP = 160;
|
||||||
|
|
||||||
|
private final int padding;
|
||||||
|
private final int rowWidthCap;
|
||||||
|
private final List<GridCell> cells;
|
||||||
|
private final Map<String, GridCell> byKey;
|
||||||
|
|
||||||
|
private ObjectStudioLayout(int padding, int rowWidthCap, List<GridCell> cells) {
|
||||||
|
this.padding = padding;
|
||||||
|
this.rowWidthCap = rowWidthCap;
|
||||||
|
this.cells = Collections.unmodifiableList(cells);
|
||||||
|
Map<String, GridCell> index = new LinkedHashMap<>();
|
||||||
|
for (GridCell cell : cells) {
|
||||||
|
index.putIfAbsent(cell.key(), cell);
|
||||||
|
index.putIfAbsent(cell.pack() + "/" + cell.key(), cell);
|
||||||
|
}
|
||||||
|
this.byKey = Collections.unmodifiableMap(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObjectStudioLayout build(IrisData data, int padding) {
|
||||||
|
Map<String, IrisData> sources = new LinkedHashMap<>();
|
||||||
|
sources.put(data.getDataFolder().getName(), data);
|
||||||
|
return build(sources, padding, DEFAULT_ROW_WIDTH_CAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObjectStudioLayout build(Map<String, IrisData> sources, int padding) {
|
||||||
|
return build(sources, padding, DEFAULT_ROW_WIDTH_CAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObjectStudioLayout build(Map<String, IrisData> sources, int padding, int rowWidthCap) {
|
||||||
|
List<PackEntry> entries = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, IrisData> entry : sources.entrySet()) {
|
||||||
|
String packName = entry.getKey();
|
||||||
|
IrisData data = entry.getValue();
|
||||||
|
String[] possible = data.getObjectLoader().getPossibleKeys();
|
||||||
|
if (possible == null) continue;
|
||||||
|
List<String> sorted = new ArrayList<>(Arrays.asList(possible));
|
||||||
|
Collections.sort(sorted);
|
||||||
|
for (String key : sorted) {
|
||||||
|
entries.add(new PackEntry(packName, data, key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.sort((a, b) -> {
|
||||||
|
int c = a.pack.compareTo(b.pack);
|
||||||
|
if (c != 0) return c;
|
||||||
|
return a.key.compareTo(b.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<GridCell> packed = new ArrayList<>(entries.size());
|
||||||
|
int cursorX = 0;
|
||||||
|
int rowZ = 0;
|
||||||
|
int rowMaxDepth = 0;
|
||||||
|
|
||||||
|
for (PackEntry entry : entries) {
|
||||||
|
File file = entry.data.getObjectLoader().findFile(entry.key);
|
||||||
|
if (file == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockVector size;
|
||||||
|
try {
|
||||||
|
size = IrisObject.sampleSize(file);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w = Math.max(1, size.getBlockX());
|
||||||
|
int h = Math.max(1, size.getBlockY());
|
||||||
|
int d = Math.max(1, size.getBlockZ());
|
||||||
|
|
||||||
|
int cellWidth = w + padding * 2;
|
||||||
|
int cellDepth = d + padding * 2;
|
||||||
|
|
||||||
|
if (cursorX > 0 && cursorX + cellWidth > rowWidthCap) {
|
||||||
|
rowZ += rowMaxDepth;
|
||||||
|
cursorX = 0;
|
||||||
|
rowMaxDepth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int originX = cursorX + padding;
|
||||||
|
int originZ = rowZ + padding;
|
||||||
|
int originY = FLOOR_Y + 1;
|
||||||
|
|
||||||
|
packed.add(new GridCell(entry.pack, entry.key, originX, originY, originZ, w, h, d));
|
||||||
|
|
||||||
|
cursorX += cellWidth;
|
||||||
|
rowMaxDepth = Math.max(rowMaxDepth, cellDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ObjectStudioLayout(padding, rowWidthCap, packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObjectStudioLayout load(File file, Map<String, IrisData> sources, int padding) {
|
||||||
|
if (file == null || !file.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String raw = Files.readString(file.toPath());
|
||||||
|
JSONObject root = new JSONObject(raw);
|
||||||
|
int storedPadding = root.optInt("padding", padding);
|
||||||
|
int storedCap = root.optInt("rowWidthCap", DEFAULT_ROW_WIDTH_CAP);
|
||||||
|
JSONArray arr = root.getJSONArray("cells");
|
||||||
|
|
||||||
|
List<GridCell> stored = new ArrayList<>();
|
||||||
|
Set<String> storedIds = new HashSet<>();
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONObject c = arr.getJSONObject(i);
|
||||||
|
String pack = c.optString("pack", null);
|
||||||
|
if (pack == null || pack.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GridCell cell = new GridCell(
|
||||||
|
pack,
|
||||||
|
c.getString("key"),
|
||||||
|
c.getInt("x"),
|
||||||
|
c.getInt("y"),
|
||||||
|
c.getInt("z"),
|
||||||
|
c.getInt("w"),
|
||||||
|
c.getInt("h"),
|
||||||
|
c.getInt("d")
|
||||||
|
);
|
||||||
|
stored.add(cell);
|
||||||
|
storedIds.add(cell.pack() + "/" + cell.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> liveIds = new HashSet<>();
|
||||||
|
for (Map.Entry<String, IrisData> entry : sources.entrySet()) {
|
||||||
|
String[] live = entry.getValue().getObjectLoader().getPossibleKeys();
|
||||||
|
if (live == null) continue;
|
||||||
|
for (String k : live) {
|
||||||
|
liveIds.add(entry.getKey() + "/" + k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveIds.size() == storedIds.size() && liveIds.containsAll(storedIds)) {
|
||||||
|
return new ObjectStudioLayout(storedPadding, storedCap, stored);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(File file) {
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (file.getParentFile() != null) {
|
||||||
|
file.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
JSONObject root = new JSONObject();
|
||||||
|
root.put("padding", padding);
|
||||||
|
root.put("rowWidthCap", rowWidthCap);
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
for (GridCell cell : cells) {
|
||||||
|
JSONObject c = new JSONObject();
|
||||||
|
c.put("pack", cell.pack());
|
||||||
|
c.put("key", cell.key());
|
||||||
|
c.put("x", cell.originX());
|
||||||
|
c.put("y", cell.originY());
|
||||||
|
c.put("z", cell.originZ());
|
||||||
|
c.put("w", cell.w());
|
||||||
|
c.put("h", cell.h());
|
||||||
|
c.put("d", cell.d());
|
||||||
|
arr.put(c);
|
||||||
|
}
|
||||||
|
root.put("cells", arr);
|
||||||
|
Files.writeString(file.toPath(), root.toString(2));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int padding() {
|
||||||
|
return padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GridCell> cells() {
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GridCell findAt(int worldX, int worldZ) {
|
||||||
|
for (GridCell cell : cells) {
|
||||||
|
if (worldX >= cell.originX() && worldX < cell.originX() + cell.w()
|
||||||
|
&& worldZ >= cell.originZ() && worldZ < cell.originZ() + cell.d()) {
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GridCell get(String key) {
|
||||||
|
return byKey.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PackEntry(String pack, IrisData data, String key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record GridCell(String pack, String key, int originX, int originY, int originZ, int w, int h, int d) {
|
||||||
|
public int chunkMinX() {
|
||||||
|
return originX >> 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int chunkMaxX() {
|
||||||
|
return (originX + w - 1) >> 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int chunkMinZ() {
|
||||||
|
return originZ >> 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int chunkMaxZ() {
|
||||||
|
return (originZ + d - 1) >> 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,35 @@ public final class WorldRuntimeControlService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean applyObjectStudioWorldRules(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyStudioWorldRules(world);
|
||||||
|
|
||||||
|
setBooleanGameRule(world, false, "DO_FIRE_TICK", "doFireTick");
|
||||||
|
setBooleanGameRule(world, false, "DO_MOB_SPAWNING", "doMobSpawning");
|
||||||
|
setBooleanGameRule(world, false, "DO_MOB_LOOT", "doMobLoot");
|
||||||
|
setBooleanGameRule(world, false, "DO_TRADER_SPAWNING", "doTraderSpawning");
|
||||||
|
setBooleanGameRule(world, false, "DO_PATROL_SPAWNING", "doPatrolSpawning");
|
||||||
|
setBooleanGameRule(world, false, "DO_INSOMNIA", "doInsomnia");
|
||||||
|
setBooleanGameRule(world, true, "DO_IMMEDIATE_RESPAWN", "doImmediateRespawn");
|
||||||
|
setBooleanGameRule(world, false, "FALL_DAMAGE", "fallDamage");
|
||||||
|
setBooleanGameRule(world, false, "FIRE_DAMAGE", "fireDamage");
|
||||||
|
setBooleanGameRule(world, false, "DROWNING_DAMAGE", "drowningDamage");
|
||||||
|
setBooleanGameRule(world, false, "FREEZE_DAMAGE", "freezeDamage");
|
||||||
|
setBooleanGameRule(world, false, "DO_WARDEN_SPAWNING", "doWardenSpawning");
|
||||||
|
setBooleanGameRule(world, false, "MOB_GRIEFING", "mobGriefing");
|
||||||
|
setBooleanGameRule(world, false, "DO_TILE_DROPS", "doTileDrops");
|
||||||
|
setBooleanGameRule(world, true, "KEEP_INVENTORY", "keepInventory");
|
||||||
|
setIntGameRule(world, 0, "RANDOM_TICK_SPEED", "randomTickSpeed");
|
||||||
|
setIntGameRule(world, 0, "SPAWN_RADIUS", "spawnRadius");
|
||||||
|
setIntGameRule(world, 0, "MAX_ENTITY_CRAMMING", "maxEntityCramming");
|
||||||
|
applyNoonTimeLock(world);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean applyNoonTimeLock(World world) {
|
public boolean applyNoonTimeLock(World world) {
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -347,6 +376,74 @@ public final class WorldRuntimeControlService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void setIntGameRule(World world, int value, String... names) {
|
||||||
|
GameRule<Integer> gameRule = resolveIntGameRule(world, names);
|
||||||
|
if (gameRule != null) {
|
||||||
|
world.setGameRule(gameRule, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static GameRule<Integer> resolveIntGameRule(World world, String... names) {
|
||||||
|
if (world == null || names == null || names.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> candidates = buildRuleNameCandidates(names);
|
||||||
|
for (String name : candidates) {
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Field field = GameRule.class.getField(name);
|
||||||
|
Object value = field.get(null);
|
||||||
|
if (value instanceof GameRule<?> gameRule && Integer.class.equals(gameRule.getType())) {
|
||||||
|
return (GameRule<Integer>) gameRule;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
GameRule<?> byName = GameRule.getByName(name);
|
||||||
|
if (byName != null && Integer.class.equals(byName.getType())) {
|
||||||
|
return (GameRule<Integer>) byName;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] availableRules = world.getGameRules();
|
||||||
|
if (availableRules == null || availableRules.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> normalizedCandidates = new LinkedHashSet<>();
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
if (candidate != null && !candidate.isBlank()) {
|
||||||
|
normalizedCandidates.add(normalizeRuleName(candidate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String availableRule : availableRules) {
|
||||||
|
String normalizedAvailable = normalizeRuleName(availableRule);
|
||||||
|
if (!normalizedCandidates.contains(normalizedAvailable)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
GameRule<?> byName = GameRule.getByName(availableRule);
|
||||||
|
if (byName != null && Integer.class.equals(byName.getType())) {
|
||||||
|
return (GameRule<Integer>) byName;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
|
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
|
||||||
if (world == null || names == null || names.length == 0) {
|
if (world == null || names == null || names.length == 0) {
|
||||||
|
|||||||
@@ -173,13 +173,19 @@ public class BoardSVC implements IrisService, BoardProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void tick() {
|
private void tick() {
|
||||||
if (cancelled || !boardEnabled || !player.isOnline()) {
|
if (!boardEnabled || !player.isOnline()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
board.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEligibleWorld(player)) {
|
if (!isEligibleWorld(player)) {
|
||||||
boards.remove(player);
|
boards.remove(player);
|
||||||
cancel();
|
cancelled = true;
|
||||||
|
board.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,8 +195,15 @@ public class BoardSVC implements IrisService, BoardProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
J.runEntity(player, board::remove);
|
if (J.isOwnedByCurrentRegion(player) && player.isOnline()) {
|
||||||
|
board.remove();
|
||||||
|
} else {
|
||||||
|
J.runEntity(player, board::remove);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update() {
|
public void update() {
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
/*
|
||||||
|
* 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 art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.ObjectStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator;
|
||||||
|
import art.arcane.iris.util.common.plugin.IrisService;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import io.papermc.lib.PaperLib;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
|
import org.bukkit.event.entity.EntitySpawnEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.event.world.WorldUnloadEvent;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public class ObjectStudioSaveService implements IrisService {
|
||||||
|
public static final int INTERVAL_TICKS = 100;
|
||||||
|
private static final int CELLS_PER_PASS = 50;
|
||||||
|
|
||||||
|
private static ObjectStudioSaveService INSTANCE;
|
||||||
|
|
||||||
|
private final Map<UUID, ActiveStudio> studios = new ConcurrentHashMap<>();
|
||||||
|
private int taskId = -1;
|
||||||
|
|
||||||
|
public static ObjectStudioSaveService get() {
|
||||||
|
ObjectStudioSaveService svc = INSTANCE;
|
||||||
|
if (svc != null) return svc;
|
||||||
|
svc = Iris.service(ObjectStudioSaveService.class);
|
||||||
|
return svc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
INSTANCE = this;
|
||||||
|
taskId = J.ar(this::pass, INTERVAL_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
if (taskId != -1) {
|
||||||
|
J.car(taskId);
|
||||||
|
taskId = -1;
|
||||||
|
}
|
||||||
|
studios.clear();
|
||||||
|
INSTANCE = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(Engine engine, ObjectStudioGenerator generator) {
|
||||||
|
World world = engine.getTarget().getWorld().realWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
ObjectStudioLayout layout = generator.getLayout();
|
||||||
|
if (layout == null) return;
|
||||||
|
|
||||||
|
Map<String, IrisData> sources = generator.getPackData();
|
||||||
|
if (sources == null || sources.isEmpty()) {
|
||||||
|
Iris.warn("Object Studio save disabled: no pack data sources available for world %s", world.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, File> objectsDirs = new ConcurrentHashMap<>();
|
||||||
|
for (Map.Entry<String, IrisData> e : sources.entrySet()) {
|
||||||
|
File dir = resolveObjectsDir(e.getValue());
|
||||||
|
if (dir != null) {
|
||||||
|
objectsDirs.put(e.getKey(), dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (objectsDirs.isEmpty()) {
|
||||||
|
Iris.warn("Object Studio save disabled: no resolvable objects folders for world %s", world.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveStudio existing = studios.get(world.getUID());
|
||||||
|
if (existing != null && existing.layout == layout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey();
|
||||||
|
studios.put(world.getUID(), new ActiveStudio(world.getUID(), layout, objectsDirs, packKey));
|
||||||
|
Iris.info("Object Studio live-save registered: world=%s cells=%d packs=%d",
|
||||||
|
world.getName(), layout.cells().size(), objectsDirs.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister(World world) {
|
||||||
|
if (world == null) return;
|
||||||
|
ActiveStudio removed = studios.remove(world.getUID());
|
||||||
|
if (removed != null) {
|
||||||
|
if (removed.packKey != null) {
|
||||||
|
ObjectStudioActivation.deactivate(removed.packKey);
|
||||||
|
}
|
||||||
|
Iris.info("Object Studio live-save unregistered: world=%s", world.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onWorldUnload(WorldUnloadEvent event) {
|
||||||
|
unregister(event.getWorld());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onCreatureSpawn(CreatureSpawnEvent event) {
|
||||||
|
if (!studios.containsKey(event.getLocation().getWorld().getUID())) return;
|
||||||
|
CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason();
|
||||||
|
if (reason == CreatureSpawnEvent.SpawnReason.CUSTOM
|
||||||
|
|| reason == CreatureSpawnEvent.SpawnReason.COMMAND
|
||||||
|
|| reason == CreatureSpawnEvent.SpawnReason.SPAWNER_EGG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onEntitySpawn(EntitySpawnEvent event) {
|
||||||
|
if (event instanceof CreatureSpawnEvent) return;
|
||||||
|
if (!studios.containsKey(event.getLocation().getWorld().getUID())) return;
|
||||||
|
if (event.getEntity() instanceof org.bukkit.entity.Player) return;
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.LEFT_CLICK_BLOCK) return;
|
||||||
|
Block clicked = event.getClickedBlock();
|
||||||
|
if (clicked == null) return;
|
||||||
|
|
||||||
|
World world = clicked.getWorld();
|
||||||
|
ActiveStudio studio = studios.get(world.getUID());
|
||||||
|
if (studio == null) return;
|
||||||
|
|
||||||
|
GridCell cell = studio.layout.findAt(clicked.getX(), clicked.getZ());
|
||||||
|
if (cell == null) return;
|
||||||
|
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
Iris.info("Object Studio save triggered by %s for %s", player.getName(), cell.key());
|
||||||
|
J.runRegion(world, cell.chunkMinX(), cell.chunkMinZ(), () -> {
|
||||||
|
try {
|
||||||
|
captureAndSave(studio, world, cell);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean teleportTo(Player player, String objectKey) {
|
||||||
|
if (player == null || objectKey == null) return false;
|
||||||
|
for (ActiveStudio studio : studios.values()) {
|
||||||
|
GridCell cell = studio.layout.get(objectKey);
|
||||||
|
if (cell == null) continue;
|
||||||
|
World world = Bukkit.getWorld(studio.worldId);
|
||||||
|
if (world == null) continue;
|
||||||
|
|
||||||
|
double targetX = cell.originX() + cell.w() / 2.0D + 0.5D;
|
||||||
|
double targetZ = cell.originZ() + cell.d() / 2.0D + 0.5D;
|
||||||
|
double targetY = cell.originY() + cell.h() + 2.0D;
|
||||||
|
Location location = new Location(world, targetX, targetY, targetZ);
|
||||||
|
J.runEntity(player, () -> PaperLib.teleportAsync(player, location));
|
||||||
|
Iris.info("Object Studio goto: %s -> %s at %.0f,%.0f,%.0f",
|
||||||
|
player.getName(), objectKey, location.getX(), location.getY(), location.getZ());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File resolveObjectsDir(IrisData data) {
|
||||||
|
File root = data.getDataFolder();
|
||||||
|
if (root == null) return null;
|
||||||
|
File objects = new File(root, "objects");
|
||||||
|
if (!objects.exists()) {
|
||||||
|
objects.mkdirs();
|
||||||
|
}
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pass() {
|
||||||
|
if (studios.isEmpty()) return;
|
||||||
|
|
||||||
|
for (ActiveStudio studio : studios.values()) {
|
||||||
|
World world = Bukkit.getWorld(studio.worldId);
|
||||||
|
if (world == null) continue;
|
||||||
|
|
||||||
|
int budget = CELLS_PER_PASS;
|
||||||
|
int size = studio.layout.cells().size();
|
||||||
|
if (size == 0) continue;
|
||||||
|
|
||||||
|
while (budget-- > 0) {
|
||||||
|
int idx = studio.cursor.getAndIncrement();
|
||||||
|
if (idx >= size) {
|
||||||
|
studio.cursor.set(0);
|
||||||
|
idx = 0;
|
||||||
|
}
|
||||||
|
GridCell cell = studio.layout.cells().get(idx);
|
||||||
|
scheduleCapture(studio, world, cell);
|
||||||
|
if (size <= CELLS_PER_PASS && idx == size - 1) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleCapture(ActiveStudio studio, World world, GridCell cell) {
|
||||||
|
int chunkX = cell.chunkMinX();
|
||||||
|
int chunkZ = cell.chunkMinZ();
|
||||||
|
J.runRegion(world, chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
captureAndSave(studio, world, cell);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void captureAndSave(ActiveStudio studio, World world, GridCell cell) {
|
||||||
|
if (!allChunksLoaded(world, cell)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisObject snapshot = new IrisObject(cell.w(), cell.h(), cell.d());
|
||||||
|
int originX = cell.originX();
|
||||||
|
int originY = cell.originY();
|
||||||
|
int originZ = cell.originZ();
|
||||||
|
|
||||||
|
boolean anyBlock = false;
|
||||||
|
for (int dx = 0; dx < cell.w(); dx++) {
|
||||||
|
for (int dy = 0; dy < cell.h(); dy++) {
|
||||||
|
for (int dz = 0; dz < cell.d(); dz++) {
|
||||||
|
Block block = world.getBlockAt(originX + dx, originY + dy, originZ + dz);
|
||||||
|
if (block.getType() == Material.AIR) continue;
|
||||||
|
snapshot.setUnsigned(dx, dy, dz, block, false);
|
||||||
|
anyBlock = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String hashKey = cell.pack() + "/" + cell.key();
|
||||||
|
long hash = hashOf(snapshot);
|
||||||
|
Long prior = studio.hashes.get(hashKey);
|
||||||
|
if (prior != null && prior == hash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyBlock && prior == null) {
|
||||||
|
studio.hashes.put(hashKey, hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
studio.hashes.put(hashKey, hash);
|
||||||
|
|
||||||
|
File targetFile = objectFileFor(studio, cell);
|
||||||
|
if (targetFile == null) return;
|
||||||
|
|
||||||
|
J.a(() -> {
|
||||||
|
try {
|
||||||
|
File parent = targetFile.getParentFile();
|
||||||
|
if (parent != null && !parent.exists()) {
|
||||||
|
parent.mkdirs();
|
||||||
|
}
|
||||||
|
snapshot.write(targetFile);
|
||||||
|
Iris.info("Object Studio saved: %s/%s (%dx%dx%d)",
|
||||||
|
cell.pack(), cell.key(), cell.w(), cell.h(), cell.d());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean allChunksLoaded(World world, GridCell cell) {
|
||||||
|
for (int cx = cell.chunkMinX(); cx <= cell.chunkMaxX(); cx++) {
|
||||||
|
for (int cz = cell.chunkMinZ(); cz <= cell.chunkMaxZ(); cz++) {
|
||||||
|
if (!world.isChunkLoaded(cx, cz)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long hashOf(IrisObject snapshot) {
|
||||||
|
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
|
snapshot.write(baos);
|
||||||
|
byte[] bytes = baos.toByteArray();
|
||||||
|
long h = 1125899906842597L;
|
||||||
|
for (byte b : bytes) {
|
||||||
|
h = 31 * h + b;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
return System.nanoTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File objectFileFor(ActiveStudio studio, GridCell cell) {
|
||||||
|
File objectsDir = studio.objectsDirs.get(cell.pack());
|
||||||
|
if (objectsDir == null) return null;
|
||||||
|
String relative = cell.key().replace('\\', '/');
|
||||||
|
return new File(objectsDir, relative + ".iob");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ActiveStudio {
|
||||||
|
final UUID worldId;
|
||||||
|
final ObjectStudioLayout layout;
|
||||||
|
final Map<String, File> objectsDirs;
|
||||||
|
final String packKey;
|
||||||
|
final Map<String, Long> hashes = new ConcurrentHashMap<>();
|
||||||
|
final AtomicInteger cursor = new AtomicInteger();
|
||||||
|
|
||||||
|
ActiveStudio(UUID worldId, ObjectStudioLayout layout, Map<String, File> objectsDirs, String packKey) {
|
||||||
|
this.worldId = worldId;
|
||||||
|
this.layout = layout;
|
||||||
|
this.objectsDirs = objectsDirs;
|
||||||
|
this.packKey = packKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,544 @@
|
|||||||
|
/*
|
||||||
|
* 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 art.arcane.iris.core.tools;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
|
import art.arcane.iris.util.common.data.B;
|
||||||
|
import art.arcane.iris.util.common.data.VectorMap;
|
||||||
|
import org.bukkit.Axis;
|
||||||
|
import org.bukkit.Tag;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.block.data.Orientable;
|
||||||
|
import org.bukkit.block.data.type.Leaves;
|
||||||
|
import org.bukkit.util.BlockVector;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class TreePlausibilizer {
|
||||||
|
public static final int MAX_DISTANCE = 6;
|
||||||
|
public static final int DEFAULT_SHELL_RADIUS = 2;
|
||||||
|
private static final int[][] NEIGHBORS = {
|
||||||
|
{1, 0, 0}, {-1, 0, 0},
|
||||||
|
{0, 1, 0}, {0, -1, 0},
|
||||||
|
{0, 0, 1}, {0, 0, -1}
|
||||||
|
};
|
||||||
|
private static final BlockData FALLBACK_LOG = B.get("minecraft:oak_log[axis=y]");
|
||||||
|
private static final BlockData FALLBACK_LEAF = B.get("minecraft:oak_leaves[distance=1,persistent=false,waterlogged=false]");
|
||||||
|
|
||||||
|
private TreePlausibilizer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Result(
|
||||||
|
int totalLeaves,
|
||||||
|
int persistentLeavesInput,
|
||||||
|
int reachableBefore,
|
||||||
|
int logsAdded,
|
||||||
|
int leavesAdded,
|
||||||
|
int leavesRemoved,
|
||||||
|
int leavesNormalized,
|
||||||
|
int unreachableAfter,
|
||||||
|
String skipReason
|
||||||
|
) {
|
||||||
|
public static Result skipped(String reason) {
|
||||||
|
return new Result(0, 0, 0, 0, 0, 0, 0, 0, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result analyze(IrisObject obj, boolean normalize, boolean smoke, int shellRadius) {
|
||||||
|
return run(obj, false, normalize, smoke, shellRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result apply(IrisObject obj, boolean normalize, boolean smoke, int shellRadius) {
|
||||||
|
return run(obj, true, normalize, smoke, shellRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result run(IrisObject obj, boolean mutate, boolean normalize, boolean smoke, int shellRadius) {
|
||||||
|
VectorMap<BlockData> blocks = obj.getBlocks();
|
||||||
|
Map<Long, BlockData> positions = new HashMap<>(blocks.size() * 2);
|
||||||
|
Set<Long> logPositions = new HashSet<>();
|
||||||
|
Set<Long> originalLeafPositions = new HashSet<>();
|
||||||
|
Set<Long> persistentLeafPositions = new HashSet<>();
|
||||||
|
Map<BlockData, Integer> leafTypeCounts = new HashMap<>();
|
||||||
|
|
||||||
|
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE;
|
||||||
|
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
for (Map.Entry<BlockVector, BlockData> entry : blocks) {
|
||||||
|
BlockVector pos = entry.getKey();
|
||||||
|
BlockData data = entry.getValue();
|
||||||
|
long key = packKey(pos);
|
||||||
|
positions.put(key, data);
|
||||||
|
int x = pos.getBlockX();
|
||||||
|
int y = pos.getBlockY();
|
||||||
|
int z = pos.getBlockZ();
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (z < minZ) minZ = z;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
if (z > maxZ) maxZ = z;
|
||||||
|
if (isLog(data)) {
|
||||||
|
logPositions.add(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isLeaf(data)) {
|
||||||
|
boolean persistent = data instanceof Leaves leaves && leaves.isPersistent();
|
||||||
|
if (persistent) {
|
||||||
|
persistentLeafPositions.add(key);
|
||||||
|
}
|
||||||
|
originalLeafPositions.add(key);
|
||||||
|
leafTypeCounts.merge(data, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int persistentInput = persistentLeafPositions.size();
|
||||||
|
int totalLeavesInitial = originalLeafPositions.size();
|
||||||
|
|
||||||
|
if (logPositions.isEmpty() && !originalLeafPositions.isEmpty()) {
|
||||||
|
return Result.skipped("leaves present but no logs to bridge from");
|
||||||
|
}
|
||||||
|
if (logPositions.isEmpty() && !smoke) {
|
||||||
|
return new Result(0, 0, 0, 0, 0, 0, 0, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> leafPositions;
|
||||||
|
int leavesRemoved = 0;
|
||||||
|
List<Long> removedLeafKeys = new ArrayList<>();
|
||||||
|
|
||||||
|
if (smoke) {
|
||||||
|
leafPositions = new HashSet<>();
|
||||||
|
for (long key : originalLeafPositions) {
|
||||||
|
removedLeafKeys.add(key);
|
||||||
|
positions.remove(key);
|
||||||
|
}
|
||||||
|
leavesRemoved = removedLeafKeys.size();
|
||||||
|
int r = Math.max(0, Math.min(shellRadius, 5));
|
||||||
|
BlockData leafTemplate = pickDominantLeaf(leafTypeCounts);
|
||||||
|
paintShell(
|
||||||
|
logPositions, positions, leafPositions,
|
||||||
|
leafTemplate, r,
|
||||||
|
minX, minY, minZ, maxX, maxY, maxZ
|
||||||
|
);
|
||||||
|
} else if (normalize) {
|
||||||
|
leafPositions = new HashSet<>(originalLeafPositions);
|
||||||
|
} else {
|
||||||
|
leafPositions = new HashSet<>(originalLeafPositions);
|
||||||
|
leafPositions.removeAll(persistentLeafPositions);
|
||||||
|
}
|
||||||
|
|
||||||
|
int reachableBefore;
|
||||||
|
int logsAdded = 0;
|
||||||
|
Set<Long> unreached;
|
||||||
|
Map<Long, Integer> distances;
|
||||||
|
List<LogInsertion> inserts = new ArrayList<>();
|
||||||
|
|
||||||
|
if (!leafPositions.isEmpty() && !logPositions.isEmpty()) {
|
||||||
|
Set<Long> connectivityLeaves;
|
||||||
|
if (!normalize && !smoke) {
|
||||||
|
connectivityLeaves = new HashSet<>(leafPositions);
|
||||||
|
connectivityLeaves.addAll(persistentLeafPositions);
|
||||||
|
} else {
|
||||||
|
connectivityLeaves = leafPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
distances = seedDistances(logPositions, connectivityLeaves);
|
||||||
|
reachableBefore = countReachable(leafPositions, distances);
|
||||||
|
unreached = new HashSet<>(leafPositions);
|
||||||
|
unreached.removeAll(distances.keySet());
|
||||||
|
Set<Long> frontier = computeInitialFrontier(unreached, logPositions, distances);
|
||||||
|
|
||||||
|
logsAdded = bridgeLoop(
|
||||||
|
unreached, frontier, distances,
|
||||||
|
logPositions, leafPositions, connectivityLeaves, positions,
|
||||||
|
inserts
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
distances = new HashMap<>();
|
||||||
|
unreached = new HashSet<>();
|
||||||
|
reachableBefore = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int leavesAdded = 0;
|
||||||
|
List<LeafAddition> leafAdds = new ArrayList<>();
|
||||||
|
if (smoke) {
|
||||||
|
BlockData leafTemplate = pickDominantLeaf(leafTypeCounts);
|
||||||
|
for (long key : leafPositions) {
|
||||||
|
leafAdds.add(new LeafAddition(key, leafTemplate));
|
||||||
|
}
|
||||||
|
leavesAdded = leafAdds.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
int leavesNormalized = 0;
|
||||||
|
List<LeafRewrite> normalizeRewrites = new ArrayList<>();
|
||||||
|
if (normalize && !smoke) {
|
||||||
|
for (long pos : leafPositions) {
|
||||||
|
BlockData data = positions.get(pos);
|
||||||
|
if (data instanceof Leaves leaves && leaves.isPersistent()) {
|
||||||
|
BlockData cloned = data.clone();
|
||||||
|
((Leaves) cloned).setPersistent(false);
|
||||||
|
normalizeRewrites.add(new LeafRewrite(pos, cloned));
|
||||||
|
leavesNormalized++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutate) {
|
||||||
|
if (smoke) {
|
||||||
|
for (long key : removedLeafKeys) {
|
||||||
|
if (!positions.containsKey(key)) {
|
||||||
|
blocks.remove(unpackKey(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (LeafAddition addition : leafAdds) {
|
||||||
|
blocks.put(unpackKey(addition.key()), addition.data());
|
||||||
|
}
|
||||||
|
for (LogInsertion insertion : inserts) {
|
||||||
|
blocks.put(unpackKey(insertion.key()), insertion.data());
|
||||||
|
}
|
||||||
|
for (LeafRewrite rewrite : normalizeRewrites) {
|
||||||
|
blocks.put(unpackKey(rewrite.key()), rewrite.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int finalLeafCount = leafPositions.size();
|
||||||
|
if (!normalize && !smoke) {
|
||||||
|
finalLeafCount += persistentInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Result(
|
||||||
|
smoke ? leavesAdded : totalLeavesInitial,
|
||||||
|
persistentInput,
|
||||||
|
reachableBefore,
|
||||||
|
logsAdded,
|
||||||
|
leavesAdded,
|
||||||
|
leavesRemoved,
|
||||||
|
leavesNormalized,
|
||||||
|
unreached.size(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void paintShell(
|
||||||
|
Set<Long> logPositions,
|
||||||
|
Map<Long, BlockData> positions,
|
||||||
|
Set<Long> leafPositions,
|
||||||
|
BlockData leafTemplate,
|
||||||
|
int radius,
|
||||||
|
int minX, int minY, int minZ, int maxX, int maxY, int maxZ
|
||||||
|
) {
|
||||||
|
if (radius <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int r2 = radius * radius;
|
||||||
|
int bxMin = minX;
|
||||||
|
int byMin = minY;
|
||||||
|
int bzMin = minZ;
|
||||||
|
int bxMax = maxX;
|
||||||
|
int byMax = maxY;
|
||||||
|
int bzMax = maxZ;
|
||||||
|
|
||||||
|
for (long log : logPositions) {
|
||||||
|
int[] lx = unpack(log);
|
||||||
|
for (int dx = -radius; dx <= radius; dx++) {
|
||||||
|
int ax = lx[0] + dx;
|
||||||
|
if (ax < bxMin || ax > bxMax) continue;
|
||||||
|
int dx2 = dx * dx;
|
||||||
|
for (int dy = -radius; dy <= radius; dy++) {
|
||||||
|
int ay = lx[1] + dy;
|
||||||
|
if (ay < byMin || ay > byMax) continue;
|
||||||
|
int dy2 = dy * dy;
|
||||||
|
int partial = dx2 + dy2;
|
||||||
|
if (partial > r2) continue;
|
||||||
|
for (int dz = -radius; dz <= radius; dz++) {
|
||||||
|
if (partial + dz * dz > r2) continue;
|
||||||
|
int az = lx[2] + dz;
|
||||||
|
if (az < bzMin || az > bzMax) continue;
|
||||||
|
long nk = packXYZ(ax, ay, az);
|
||||||
|
if (logPositions.contains(nk)) continue;
|
||||||
|
if (positions.containsKey(nk)) continue;
|
||||||
|
positions.put(nk, leafTemplate);
|
||||||
|
leafPositions.add(nk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int bridgeLoop(
|
||||||
|
Set<Long> unreached,
|
||||||
|
Set<Long> frontier,
|
||||||
|
Map<Long, Integer> distances,
|
||||||
|
Set<Long> logPositions,
|
||||||
|
Set<Long> leafPositions,
|
||||||
|
Set<Long> connectivityLeaves,
|
||||||
|
Map<Long, BlockData> positions,
|
||||||
|
List<LogInsertion> inserts
|
||||||
|
) {
|
||||||
|
int logsAdded = 0;
|
||||||
|
int safetyLimit = unreached.size() + 32;
|
||||||
|
while (!unreached.isEmpty() && logsAdded < safetyLimit) {
|
||||||
|
long candidateKey = pickInteriorCandidate(frontier, unreached, connectivityLeaves);
|
||||||
|
BlockData logData = pickLogVariant(candidateKey, positions, logPositions);
|
||||||
|
inserts.add(new LogInsertion(candidateKey, logData));
|
||||||
|
|
||||||
|
logPositions.add(candidateKey);
|
||||||
|
leafPositions.remove(candidateKey);
|
||||||
|
connectivityLeaves.remove(candidateKey);
|
||||||
|
distances.remove(candidateKey);
|
||||||
|
unreached.remove(candidateKey);
|
||||||
|
frontier.remove(candidateKey);
|
||||||
|
positions.put(candidateKey, logData);
|
||||||
|
logsAdded++;
|
||||||
|
|
||||||
|
int[] cx = unpack(candidateKey);
|
||||||
|
Deque<Long> q = new ArrayDeque<>();
|
||||||
|
for (int[] n : NEIGHBORS) {
|
||||||
|
long nk = packXYZ(cx[0] + n[0], cx[1] + n[1], cx[2] + n[2]);
|
||||||
|
if (connectivityLeaves.contains(nk)) {
|
||||||
|
Integer cur = distances.get(nk);
|
||||||
|
if (cur == null || cur > 1) {
|
||||||
|
if (cur == null) {
|
||||||
|
unreached.remove(nk);
|
||||||
|
frontier.remove(nk);
|
||||||
|
}
|
||||||
|
distances.put(nk, 1);
|
||||||
|
q.add(nk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unreached.contains(nk)) {
|
||||||
|
frontier.add(nk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!q.isEmpty()) {
|
||||||
|
long pos = q.poll();
|
||||||
|
int d = distances.get(pos);
|
||||||
|
if (d >= MAX_DISTANCE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int[] px = unpack(pos);
|
||||||
|
for (int[] n : NEIGHBORS) {
|
||||||
|
long nk = packXYZ(px[0] + n[0], px[1] + n[1], px[2] + n[2]);
|
||||||
|
if (connectivityLeaves.contains(nk)) {
|
||||||
|
Integer cur = distances.get(nk);
|
||||||
|
if (cur == null || cur > d + 1) {
|
||||||
|
if (cur == null) {
|
||||||
|
unreached.remove(nk);
|
||||||
|
frontier.remove(nk);
|
||||||
|
}
|
||||||
|
distances.put(nk, d + 1);
|
||||||
|
q.add(nk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unreached.contains(nk)) {
|
||||||
|
frontier.add(nk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logsAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long pickInteriorCandidate(
|
||||||
|
Set<Long> frontier, Set<Long> unreached, Set<Long> connectivityLeaves
|
||||||
|
) {
|
||||||
|
Set<Long> pool = !frontier.isEmpty() ? frontier : unreached;
|
||||||
|
long best = -1L;
|
||||||
|
int bestScore = -1;
|
||||||
|
for (long pos : pool) {
|
||||||
|
int[] xyz = unpack(pos);
|
||||||
|
int score = 0;
|
||||||
|
for (int[] n : NEIGHBORS) {
|
||||||
|
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
|
||||||
|
if (connectivityLeaves.contains(nk)) {
|
||||||
|
score++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
best = pos;
|
||||||
|
if (score == 6) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Long> computeInitialFrontier(
|
||||||
|
Set<Long> unreached, Set<Long> logPositions, Map<Long, Integer> distances
|
||||||
|
) {
|
||||||
|
Set<Long> frontier = new HashSet<>();
|
||||||
|
for (long u : unreached) {
|
||||||
|
if (hasReachedNeighbor(u, distances, logPositions)) {
|
||||||
|
frontier.add(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return frontier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasReachedNeighbor(long key, Map<Long, Integer> distances, Set<Long> logPositions) {
|
||||||
|
int[] xyz = unpack(key);
|
||||||
|
for (int[] n : NEIGHBORS) {
|
||||||
|
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
|
||||||
|
if (logPositions.contains(nk) || distances.containsKey(nk)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Long, Integer> seedDistances(Set<Long> logPositions, Set<Long> leafPositions) {
|
||||||
|
Map<Long, Integer> dist = new HashMap<>();
|
||||||
|
Deque<Long> queue = new ArrayDeque<>();
|
||||||
|
|
||||||
|
for (long leaf : leafPositions) {
|
||||||
|
int[] xyz = unpack(leaf);
|
||||||
|
for (int[] n : NEIGHBORS) {
|
||||||
|
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
|
||||||
|
if (logPositions.contains(nk)) {
|
||||||
|
dist.put(leaf, 1);
|
||||||
|
queue.add(leaf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
long pos = queue.poll();
|
||||||
|
int d = dist.get(pos);
|
||||||
|
if (d >= MAX_DISTANCE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int[] xyz = unpack(pos);
|
||||||
|
for (int[] n : NEIGHBORS) {
|
||||||
|
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
|
||||||
|
if (leafPositions.contains(nk) && !dist.containsKey(nk)) {
|
||||||
|
dist.put(nk, d + 1);
|
||||||
|
queue.add(nk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countReachable(Set<Long> leafPositions, Map<Long, Integer> distances) {
|
||||||
|
int count = 0;
|
||||||
|
for (long leaf : leafPositions) {
|
||||||
|
if (distances.containsKey(leaf)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockData pickDominantLeaf(Map<BlockData, Integer> leafTypeCounts) {
|
||||||
|
BlockData best = null;
|
||||||
|
int bestCount = -1;
|
||||||
|
for (Map.Entry<BlockData, Integer> e : leafTypeCounts.entrySet()) {
|
||||||
|
if (e.getValue() > bestCount) {
|
||||||
|
bestCount = e.getValue();
|
||||||
|
best = e.getKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best == null) {
|
||||||
|
return FALLBACK_LEAF.clone();
|
||||||
|
}
|
||||||
|
BlockData clone = best.clone();
|
||||||
|
if (clone instanceof Leaves leaves) {
|
||||||
|
leaves.setPersistent(false);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockData pickLogVariant(long target, Map<Long, BlockData> positions, Set<Long> logPositions) {
|
||||||
|
if (logPositions.isEmpty()) {
|
||||||
|
return FALLBACK_LOG.clone();
|
||||||
|
}
|
||||||
|
int[] tx = unpack(target);
|
||||||
|
long nearest = -1L;
|
||||||
|
long nearestDistSq = Long.MAX_VALUE;
|
||||||
|
for (long lp : logPositions) {
|
||||||
|
int[] lx = unpack(lp);
|
||||||
|
long dx = tx[0] - lx[0];
|
||||||
|
long dy = tx[1] - lx[1];
|
||||||
|
long dz = tx[2] - lx[2];
|
||||||
|
long d2 = dx * dx + dy * dy + dz * dz;
|
||||||
|
if (d2 < nearestDistSq) {
|
||||||
|
nearestDistSq = d2;
|
||||||
|
nearest = lp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlockData source = positions.get(nearest);
|
||||||
|
if (source == null) {
|
||||||
|
return FALLBACK_LOG.clone();
|
||||||
|
}
|
||||||
|
BlockData clone = source.clone();
|
||||||
|
if (clone instanceof Orientable orientable) {
|
||||||
|
orientable.setAxis(Axis.Y);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLog(BlockData data) {
|
||||||
|
return Tag.LOGS.isTagged(data.getMaterial());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLeaf(BlockData data) {
|
||||||
|
return Tag.LEAVES.isTagged(data.getMaterial()) || data instanceof Leaves;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long packKey(BlockVector pos) {
|
||||||
|
return packXYZ(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long packXYZ(int x, int y, int z) {
|
||||||
|
long lx = (x + 32768L) & 0xFFFFL;
|
||||||
|
long ly = (y + 32768L) & 0xFFFFL;
|
||||||
|
long lz = (z + 32768L) & 0xFFFFL;
|
||||||
|
return (lx << 32) | (ly << 16) | lz;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] unpack(long key) {
|
||||||
|
int x = (int) ((key >> 32) & 0xFFFFL) - 32768;
|
||||||
|
int y = (int) ((key >> 16) & 0xFFFFL) - 32768;
|
||||||
|
int z = (int) (key & 0xFFFFL) - 32768;
|
||||||
|
return new int[]{x, y, z};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockVector unpackKey(long key) {
|
||||||
|
int[] xyz = unpack(key);
|
||||||
|
return new BlockVector(xyz[0], xyz[1], xyz[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LogInsertion(long key, BlockData data) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LeafAddition(long key, BlockData data) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LeafRewrite(long key, BlockData data) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import art.arcane.iris.core.IrisSettings;
|
|||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.loader.ResourceLoader;
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
import art.arcane.iris.core.nms.container.BlockPos;
|
import art.arcane.iris.core.nms.container.BlockPos;
|
||||||
import art.arcane.iris.core.nms.container.Pair;
|
import art.arcane.iris.core.nms.container.Pair;
|
||||||
@@ -106,6 +107,7 @@ public class IrisEngine implements Engine {
|
|||||||
private double maxBiomeLayerDensity;
|
private double maxBiomeLayerDensity;
|
||||||
private double maxBiomeDecoratorDensity;
|
private double maxBiomeDecoratorDensity;
|
||||||
private IrisComplex complex;
|
private IrisComplex complex;
|
||||||
|
private UpperDimensionContext upperContext;
|
||||||
private final AtomicBoolean modeFallbackLogged;
|
private final AtomicBoolean modeFallbackLogged;
|
||||||
|
|
||||||
public IrisEngine(EngineTarget target, boolean studio) {
|
public IrisEngine(EngineTarget target, boolean studio) {
|
||||||
@@ -207,6 +209,7 @@ public class IrisEngine implements Engine {
|
|||||||
Iris.debug("Setup Engine " + getCacheID());
|
Iris.debug("Setup Engine " + getCacheID());
|
||||||
cacheId = RNG.r.nextInt();
|
cacheId = RNG.r.nextInt();
|
||||||
complex = ensureComplex();
|
complex = ensureComplex();
|
||||||
|
upperContext = buildUpperContext();
|
||||||
effects = new IrisEngineEffects(this);
|
effects = new IrisEngineEffects(this);
|
||||||
hash32 = new CompletableFuture<>();
|
hash32 = new CompletableFuture<>();
|
||||||
mantle.hotload();
|
mantle.hotload();
|
||||||
@@ -234,6 +237,25 @@ public class IrisEngine implements Engine {
|
|||||||
Iris.debug("Engine Setup Complete " + getCacheID());
|
Iris.debug("Engine Setup Complete " + getCacheID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UpperDimensionContext buildUpperContext() {
|
||||||
|
IrisDimension dim = getDimension();
|
||||||
|
if (!dim.hasUpperDimension()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String upperKey = dim.getUpperDimension();
|
||||||
|
IrisDimension upperDim = upperKey.equals(dim.getLoadKey())
|
||||||
|
? dim
|
||||||
|
: IrisData.loadAnyDimension(upperKey, getData());
|
||||||
|
if (upperDim != null) {
|
||||||
|
UpperDimensionContext ctx = UpperDimensionContext.create(this, upperDim);
|
||||||
|
Iris.info("Upper dimension enabled: " + upperKey
|
||||||
|
+ (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)"));
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
Iris.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void setupMode() {
|
private void setupMode() {
|
||||||
EngineMode currentMode = mode;
|
EngineMode currentMode = mode;
|
||||||
if (currentMode != null) {
|
if (currentMode != null) {
|
||||||
@@ -344,6 +366,7 @@ public class IrisEngine implements Engine {
|
|||||||
public void hotloadComplex() {
|
public void hotloadComplex() {
|
||||||
complex.close();
|
complex.close();
|
||||||
complex = new IrisComplex(this);
|
complex = new IrisComplex(this);
|
||||||
|
upperContext = buildUpperContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hotloadSilently() {
|
public void hotloadSilently() {
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.IrisSettings;
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.object.*;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
|
import art.arcane.iris.util.common.data.DataProvider;
|
||||||
|
import art.arcane.volmlib.util.math.M;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds;
|
||||||
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
|
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class UpperDimensionContext implements DataProvider {
|
||||||
|
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||||
|
private final IrisDimension dimension;
|
||||||
|
private final IrisData data;
|
||||||
|
private final int chunkHeight;
|
||||||
|
private final ProceduralStream<Double> heightStream;
|
||||||
|
private final ProceduralStream<IrisBiome> biomeStream;
|
||||||
|
private final ProceduralStream<BlockData> rockStream;
|
||||||
|
private final boolean selfReferencing;
|
||||||
|
|
||||||
|
private UpperDimensionContext(IrisDimension dimension, IrisData data, int chunkHeight,
|
||||||
|
ProceduralStream<Double> heightStream,
|
||||||
|
ProceduralStream<IrisBiome> biomeStream,
|
||||||
|
ProceduralStream<BlockData> rockStream,
|
||||||
|
boolean selfReferencing) {
|
||||||
|
this.dimension = dimension;
|
||||||
|
this.data = data;
|
||||||
|
this.chunkHeight = chunkHeight;
|
||||||
|
this.heightStream = heightStream;
|
||||||
|
this.biomeStream = biomeStream;
|
||||||
|
this.rockStream = rockStream;
|
||||||
|
this.selfReferencing = selfReferencing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) {
|
||||||
|
boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey());
|
||||||
|
int chunkHeight = engine.getHeight();
|
||||||
|
if (selfRef) {
|
||||||
|
return createSelfReferencing(engine, chunkHeight);
|
||||||
|
}
|
||||||
|
return createCrossReferencing(engine, upperDim, chunkHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpperDimensionContext createSelfReferencing(Engine engine, int chunkHeight) {
|
||||||
|
IrisComplex complex = engine.getComplex();
|
||||||
|
return new UpperDimensionContext(
|
||||||
|
engine.getDimension(),
|
||||||
|
engine.getData(),
|
||||||
|
chunkHeight,
|
||||||
|
complex.getHeightStream(),
|
||||||
|
complex.getBaseBiomeStream(),
|
||||||
|
complex.getRockStream(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpperDimensionContext createCrossReferencing(Engine engine, IrisDimension upperDim, int chunkHeight) {
|
||||||
|
IrisData resolvedData = upperDim.getLoader();
|
||||||
|
if (resolvedData == null) {
|
||||||
|
resolvedData = engine.getData();
|
||||||
|
}
|
||||||
|
IrisData upperData = resolvedData;
|
||||||
|
long seedOffset = upperDim.getLoadKey().hashCode();
|
||||||
|
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
|
||||||
|
double fluidHeight = upperDim.getFluidHeight();
|
||||||
|
DataProvider dataProvider = () -> upperData;
|
||||||
|
|
||||||
|
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
||||||
|
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
upperDim.getRegions().forEach(regionKey -> {
|
||||||
|
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||||
|
if (region != null) {
|
||||||
|
region.getAllBiomes(dataProvider).forEach(biome -> {
|
||||||
|
allBiomes.add(biome);
|
||||||
|
biome.getGenerators().forEach(link -> {
|
||||||
|
IrisGenerator gen = link.getCachedGenerator(dataProvider);
|
||||||
|
if (gen != null) {
|
||||||
|
generators.computeIfAbsent(gen.getInterpolator(), k -> new HashSet<>()).add(gen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
|
||||||
|
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||||
|
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
|
||||||
|
for (IrisBiome biome : allBiomes) {
|
||||||
|
double min = 0D;
|
||||||
|
double max = 0D;
|
||||||
|
for (IrisGenerator gen : entry.getValue()) {
|
||||||
|
String key = gen.getLoadKey();
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
max += biome.getGenLinkMax(key, engine);
|
||||||
|
min += biome.getGenLinkMin(key, engine);
|
||||||
|
}
|
||||||
|
interpolatorBounds.put(biome, new NoiseBounds(min, max));
|
||||||
|
}
|
||||||
|
generatorBounds.put(entry.getKey(), interpolatorBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
|
||||||
|
.create(rng.nextParallelRNG(883), upperData).stream()
|
||||||
|
.zoom(upperDim.getRegionZoom());
|
||||||
|
ProceduralStream<IrisRegion> regionStream = regionStyleStream
|
||||||
|
.selectRarity(upperData.getRegionLoader().loadAll(upperDim.getRegions()));
|
||||||
|
|
||||||
|
ProceduralStream<IrisBiome> landBiomeStream = regionStream
|
||||||
|
.convert(r -> upperDim.getLandBiomeStyle()
|
||||||
|
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), upperData).stream()
|
||||||
|
.zoom(upperDim.getBiomeZoom())
|
||||||
|
.zoom(upperDim.getLandZoom())
|
||||||
|
.zoom(r.getLandBiomeZoom())
|
||||||
|
.selectRarity(upperData.getBiomeLoader().loadAll(r.getLandBiomes(),
|
||||||
|
t -> t.setInferredType(InferredType.LAND))))
|
||||||
|
.convertAware2D(ProceduralStream::get);
|
||||||
|
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
|
||||||
|
.convert(r -> upperDim.getSeaBiomeStyle()
|
||||||
|
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), upperData).stream()
|
||||||
|
.zoom(upperDim.getBiomeZoom())
|
||||||
|
.zoom(upperDim.getSeaZoom())
|
||||||
|
.zoom(r.getSeaBiomeZoom())
|
||||||
|
.selectRarity(upperData.getBiomeLoader().loadAll(r.getSeaBiomes(),
|
||||||
|
t -> t.setInferredType(InferredType.SEA))))
|
||||||
|
.convertAware2D(ProceduralStream::get);
|
||||||
|
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
|
||||||
|
.convert(r -> upperDim.getShoreBiomeStyle()
|
||||||
|
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
|
||||||
|
.zoom(upperDim.getBiomeZoom())
|
||||||
|
.zoom(r.getShoreBiomeZoom())
|
||||||
|
.selectRarity(upperData.getBiomeLoader().loadAll(r.getShoreBiomes(),
|
||||||
|
t -> t.setInferredType(InferredType.SHORE))))
|
||||||
|
.convertAware2D(ProceduralStream::get);
|
||||||
|
|
||||||
|
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||||
|
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||||
|
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||||
|
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||||
|
|
||||||
|
ProceduralStream<InferredType> bridgeStream = upperDim.getContinentalStyle()
|
||||||
|
.create(rng.nextParallelRNG(234234565), upperData)
|
||||||
|
.bake().scale(1D / upperDim.getContinentZoom()).bake().stream()
|
||||||
|
.convert(v -> v >= upperDim.getLandChance() ? InferredType.SEA : InferredType.LAND);
|
||||||
|
|
||||||
|
ProceduralStream<IrisBiome> baseBiomeStream = bridgeStream
|
||||||
|
.convertAware2D((t, x, z) -> {
|
||||||
|
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
|
||||||
|
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
|
||||||
|
});
|
||||||
|
|
||||||
|
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
|
||||||
|
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
||||||
|
? ProceduralStream.ofDouble((x, z) -> 0.0D)
|
||||||
|
: ProceduralStream.ofDouble((x, z) -> {
|
||||||
|
double value = 0D;
|
||||||
|
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||||
|
value += style.get(rng, upperData, x, z);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
|
||||||
|
|
||||||
|
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
|
||||||
|
IrisBiome b = baseBiomeStream.get(x, z);
|
||||||
|
if (b == null) {
|
||||||
|
return fluidHeight;
|
||||||
|
}
|
||||||
|
double interpolatedHeight = 0;
|
||||||
|
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
|
||||||
|
IrisInterpolator interpolator = entry.getKey();
|
||||||
|
Set<IrisGenerator> gens = entry.getValue();
|
||||||
|
if (gens.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
|
||||||
|
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (xx, zz) -> {
|
||||||
|
try {
|
||||||
|
IrisBiome bx = baseBiomeStream.get(xx, zz);
|
||||||
|
if (bx == null) {
|
||||||
|
return ZERO_NOISE_BOUNDS;
|
||||||
|
}
|
||||||
|
NoiseBounds bounds = cachedBounds != null ? cachedBounds.get(bx) : null;
|
||||||
|
if (bounds != null) {
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
double bMin = 0D;
|
||||||
|
double bMax = 0D;
|
||||||
|
for (IrisGenerator gen : gens) {
|
||||||
|
String key = gen.getLoadKey();
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bMax += bx.getGenLinkMax(key, engine);
|
||||||
|
bMin += bx.getGenLinkMin(key, engine);
|
||||||
|
}
|
||||||
|
return new NoiseBounds(bMin, bMax);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
return ZERO_NOISE_BOUNDS;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
double hi = sampledBounds.max();
|
||||||
|
double lo = sampledBounds.min();
|
||||||
|
double d = 0;
|
||||||
|
for (IrisGenerator gen : gens) {
|
||||||
|
d += M.lerp(lo, hi, gen.getHeight(x, z, heightSeed + 239945));
|
||||||
|
}
|
||||||
|
interpolatedHeight += d / gens.size();
|
||||||
|
}
|
||||||
|
return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0);
|
||||||
|
}, Interpolated.DOUBLE);
|
||||||
|
|
||||||
|
ProceduralStream<BlockData> rockStream = upperDim.getRockPalette()
|
||||||
|
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
|
||||||
|
.select(upperDim.getRockPalette().getBlockData(upperData));
|
||||||
|
|
||||||
|
return new UpperDimensionContext(
|
||||||
|
upperDim,
|
||||||
|
upperData,
|
||||||
|
chunkHeight,
|
||||||
|
heightStream,
|
||||||
|
baseBiomeStream,
|
||||||
|
rockStream,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUpperSurfaceY(int x, int z) {
|
||||||
|
double rawHeight = heightStream.get((double) x, (double) z);
|
||||||
|
return chunkHeight - 1 - (int) Math.round(rawHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisBiome getUpperBiome(int x, int z) {
|
||||||
|
return biomeStream.get((double) x, (double) z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockData getRockBlock(int x, int z) {
|
||||||
|
return rockStream.get((double) x, (double) z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisDimension getDimension() {
|
||||||
|
return dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IrisData getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSelfReferencing() {
|
||||||
|
return selfReferencing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import art.arcane.iris.engine.framework.Engine;
|
|||||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
|
import art.arcane.iris.engine.UpperDimensionContext;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -167,6 +168,39 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
||||||
|
if (upperContext != null) {
|
||||||
|
int chunkHeight = h.getHeight();
|
||||||
|
boolean bedrockEnabled = getDimension().isBedrock();
|
||||||
|
int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ);
|
||||||
|
int upperGap = getDimension().getUpperDimensionGap();
|
||||||
|
int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap);
|
||||||
|
|
||||||
|
if (upperSurfaceY < chunkHeight - 1) {
|
||||||
|
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
|
||||||
|
BlockData upperRock = upperContext.getRockBlock(realX, realZ);
|
||||||
|
int upperThickness = chunkHeight - 1 - upperSurfaceY;
|
||||||
|
KList<BlockData> upperBlocks = upperBiome != null
|
||||||
|
? upperBiome.generateLayers(upperContext.getDimension(),
|
||||||
|
realX, realZ, rng, upperThickness, upperThickness,
|
||||||
|
upperContext.getData(), getComplex())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
|
||||||
|
if (y == chunkHeight - 1 && bedrockEnabled) {
|
||||||
|
h.setRaw(xf, y, zf, BEDROCK);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int depthFromFace = y - upperSurfaceY;
|
||||||
|
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
|
||||||
|
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
|
||||||
|
} else {
|
||||||
|
h.setRaw(xf, y, zf, upperRock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +220,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
|
|||||||
ChunkedDataCache<BlockData> fluidCache = context.getFluid();
|
ChunkedDataCache<BlockData> fluidCache = context.getFluid();
|
||||||
ChunkedDataCache<BlockData> rockCache = context.getRock();
|
ChunkedDataCache<BlockData> rockCache = context.getRock();
|
||||||
int realX = xf + x;
|
int realX = xf + x;
|
||||||
|
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
||||||
|
|
||||||
for (int zf = 0; zf < chunkDepth; zf++) {
|
for (int zf = 0; zf < chunkDepth; zf++) {
|
||||||
int realZ = zf + z;
|
int realZ = zf + z;
|
||||||
@@ -261,6 +296,36 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (upperContext != null) {
|
||||||
|
int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ);
|
||||||
|
int upperGap = dimension.getUpperDimensionGap();
|
||||||
|
int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap);
|
||||||
|
|
||||||
|
if (upperSurfaceY < chunkHeight - 1) {
|
||||||
|
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
|
||||||
|
BlockData upperRock = upperContext.getRockBlock(realX, realZ);
|
||||||
|
int upperThickness = chunkHeight - 1 - upperSurfaceY;
|
||||||
|
KList<BlockData> upperBlocks = upperBiome != null
|
||||||
|
? upperBiome.generateLayers(upperContext.getDimension(),
|
||||||
|
realX, realZ, localRng, upperThickness, upperThickness,
|
||||||
|
upperContext.getData(), complex)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
|
||||||
|
if (y == chunkHeight - 1 && bedrockEnabled) {
|
||||||
|
h.setRaw(xf, y, zf, BEDROCK);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int depthFromFace = y - upperSurfaceY;
|
||||||
|
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
|
||||||
|
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
|
||||||
|
} else {
|
||||||
|
h.setRaw(xf, y, zf, upperRock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import art.arcane.iris.core.nms.container.Pair;
|
|||||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
|
import art.arcane.iris.engine.UpperDimensionContext;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
@@ -95,6 +96,10 @@ import java.util.stream.Collectors;
|
|||||||
public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdater, Renderer, Hotloadable {
|
public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdater, Renderer, Hotloadable {
|
||||||
IrisComplex getComplex();
|
IrisComplex getComplex();
|
||||||
|
|
||||||
|
default @Nullable UpperDimensionContext getUpperContext() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
EngineMode getMode();
|
EngineMode getMode();
|
||||||
|
|
||||||
int getBlockUpdatesPerSecond();
|
int getBlockUpdatesPerSecond();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import art.arcane.iris.core.loader.IrisData;
|
|||||||
import art.arcane.iris.core.nms.container.Pair;
|
import art.arcane.iris.core.nms.container.Pair;
|
||||||
import art.arcane.iris.core.link.Identifier;
|
import art.arcane.iris.core.link.Identifier;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
|
import art.arcane.iris.engine.UpperDimensionContext;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||||
@@ -191,9 +192,34 @@ public interface EngineMantle extends MatterGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunk = getMantle().getChunk(x, z).use();
|
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||||
|
boolean protectUpper = t == BlockData.class && upperCtx != null;
|
||||||
|
|
||||||
|
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
|
||||||
try {
|
try {
|
||||||
chunk.iterate(t, blocks::set);
|
if (protectUpper) {
|
||||||
|
int chunkBlockX = x << 4;
|
||||||
|
int chunkBlockZ = z << 4;
|
||||||
|
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||||
|
int[] upperYs = new int[256];
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
int lx = i >> 4;
|
||||||
|
int lz = i & 15;
|
||||||
|
int worldX = chunkBlockX + lx;
|
||||||
|
int worldZ = chunkBlockZ + lz;
|
||||||
|
int he = (int) Math.round(getEngine().getComplex().getHeightStream().get((double) worldX, (double) worldZ));
|
||||||
|
int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ);
|
||||||
|
upperYs[i] = Math.max(rawUpper, he + gap);
|
||||||
|
}
|
||||||
|
chunk.iterate(t, (lx, y, lz, value) -> {
|
||||||
|
int colIdx = (lx << 4) | (lz & 15);
|
||||||
|
if (y < upperYs[colIdx]) {
|
||||||
|
blocks.set(lx, y, lz, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
chunk.iterate(t, blocks::set);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
chunk.release();
|
chunk.release();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,20 @@ public class IrisCaveCarver3D {
|
|||||||
double thresholdPenalty,
|
double thresholdPenalty,
|
||||||
IrisRange worldYRange,
|
IrisRange worldYRange,
|
||||||
int[] precomputedSurfaceHeights
|
int[] precomputedSurfaceHeights
|
||||||
|
) {
|
||||||
|
return carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, worldYRange, precomputedSurfaceHeights, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int carve(
|
||||||
|
MantleWriter writer,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
double[] columnWeights,
|
||||||
|
double minWeight,
|
||||||
|
double thresholdPenalty,
|
||||||
|
IrisRange worldYRange,
|
||||||
|
int[] precomputedSurfaceHeights,
|
||||||
|
IrisRange overrideVerticalRange
|
||||||
) {
|
) {
|
||||||
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
||||||
try {
|
try {
|
||||||
@@ -165,8 +179,9 @@ public class IrisCaveCarver3D {
|
|||||||
double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight));
|
double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight));
|
||||||
double resolvedThresholdPenalty = Math.max(0D, thresholdPenalty);
|
double resolvedThresholdPenalty = Math.max(0D, thresholdPenalty);
|
||||||
int worldHeight = writer.getMantle().getWorldHeight();
|
int worldHeight = writer.getMantle().getWorldHeight();
|
||||||
int minY = Math.max(0, (int) Math.floor(profile.getVerticalRange().getMin()));
|
IrisRange effectiveVerticalRange = overrideVerticalRange != null ? overrideVerticalRange : profile.getVerticalRange();
|
||||||
int maxY = Math.min(worldHeight - 1, (int) Math.ceil(profile.getVerticalRange().getMax()));
|
int minY = Math.max(0, (int) Math.floor(effectiveVerticalRange.getMin()));
|
||||||
|
int maxY = Math.min(worldHeight - 1, (int) Math.ceil(effectiveVerticalRange.getMax()));
|
||||||
if (worldYRange != null) {
|
if (worldYRange != null) {
|
||||||
int worldMinHeight = engine.getWorld().minHeight();
|
int worldMinHeight = engine.getWorld().minHeight();
|
||||||
int rangeMinY = (int) Math.floor(worldYRange.getMin() - worldMinHeight);
|
int rangeMinY = (int) Math.floor(worldYRange.getMin() - worldMinHeight);
|
||||||
|
|||||||
+58
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.engine.mantle.components;
|
package art.arcane.iris.engine.mantle.components;
|
||||||
|
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
|
import art.arcane.iris.engine.UpperDimensionContext;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
@@ -91,6 +92,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
for (WeightedProfile weightedProfile : weightedProfiles) {
|
for (WeightedProfile weightedProfile : weightedProfiles) {
|
||||||
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
|
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
|
||||||
|
if (upperCtx != null && getDimension().isUpperDimensionCarving()) {
|
||||||
|
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ChunkCoordinates
|
@ChunkCoordinates
|
||||||
@@ -99,6 +105,58 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights);
|
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles, MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights) {
|
||||||
|
int chunkHeight = getEngineMantle().getEngine().getHeight();
|
||||||
|
int worldMinHeight = getEngineMantle().getEngine().getWorld().minHeight();
|
||||||
|
int gap = getDimension().getUpperDimensionGap();
|
||||||
|
int baseX = PowerOfTwoCoordinates.chunkToBlock(cx);
|
||||||
|
int baseZ = PowerOfTwoCoordinates.chunkToBlock(cz);
|
||||||
|
|
||||||
|
int minUpperSurfaceY = chunkHeight;
|
||||||
|
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
|
||||||
|
int worldX = baseX + localX;
|
||||||
|
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
|
||||||
|
int worldZ = baseZ + localZ;
|
||||||
|
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||||
|
int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ);
|
||||||
|
int upperY = Math.max(rawUpper, lowerSurfaceHeights[columnIndex] + gap);
|
||||||
|
if (upperY < minUpperSurfaceY) {
|
||||||
|
minUpperSurfaceY = upperY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minUpperSurfaceY >= chunkHeight - 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisRange upperYRange = new IrisRange(
|
||||||
|
minUpperSurfaceY + worldMinHeight,
|
||||||
|
chunkHeight - 1 + worldMinHeight
|
||||||
|
);
|
||||||
|
IrisRange fullVerticalRange = new IrisRange(0, chunkHeight);
|
||||||
|
|
||||||
|
int[] ceilingSurfaceHeights = new int[CHUNK_AREA];
|
||||||
|
Arrays.fill(ceilingSurfaceHeights, chunkHeight - 1);
|
||||||
|
|
||||||
|
for (WeightedProfile weightedProfile : normalProfiles) {
|
||||||
|
IrisRange constrainedRange;
|
||||||
|
if (weightedProfile.worldYRange != null) {
|
||||||
|
double min = Math.max(weightedProfile.worldYRange.getMin(), upperYRange.getMin());
|
||||||
|
double max = Math.min(weightedProfile.worldYRange.getMax(), upperYRange.getMax());
|
||||||
|
if (min >= max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
constrainedRange = new IrisRange(min, max);
|
||||||
|
} else {
|
||||||
|
constrainedRange = upperYRange;
|
||||||
|
}
|
||||||
|
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
|
||||||
|
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
|
||||||
|
constrainedRange, ceilingSurfaceHeights, fullVerticalRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState) {
|
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState) {
|
||||||
BlendScratch blendScratch = BLEND_SCRATCH.get();
|
BlendScratch blendScratch = BLEND_SCRATCH.get();
|
||||||
IrisCaveProfile[] profileField = blendScratch.profileField;
|
IrisCaveProfile[] profileField = blendScratch.profileField;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine.modifier;
|
package art.arcane.iris.engine.modifier;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.UpperDimensionContext;
|
||||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
||||||
@@ -82,10 +83,22 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
|||||||
MatterCavern[] boundaryCaverns = scratch.boundaryCaverns;
|
MatterCavern[] boundaryCaverns = scratch.boundaryCaverns;
|
||||||
int[] surfaceHeights = scratch.surfaceHeights;
|
int[] surfaceHeights = scratch.surfaceHeights;
|
||||||
Map<String, IrisBiome> customBiomeCache = scratch.customBiomeCache;
|
Map<String, IrisBiome> customBiomeCache = scratch.customBiomeCache;
|
||||||
|
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||||
|
boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving();
|
||||||
|
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
|
||||||
|
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
|
||||||
|
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
|
||||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
||||||
int localZ = columnIndex & 15;
|
int localZ = columnIndex & 15;
|
||||||
surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ);
|
surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ);
|
||||||
|
if (protectUpper) {
|
||||||
|
int worldX = localX + chunkBlockX;
|
||||||
|
int worldZ = localZ + chunkBlockZ;
|
||||||
|
int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ);
|
||||||
|
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||||
|
upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -102,6 +115,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
|||||||
int rx = xx & 15;
|
int rx = xx & 15;
|
||||||
int rz = zz & 15;
|
int rz = zz & 15;
|
||||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
||||||
|
|
||||||
|
if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
BlockData current = output.getRaw(rx, yy, rz);
|
BlockData current = output.getRaw(rx, yy, rz);
|
||||||
|
|
||||||
if (B.isFluid(current)) {
|
if (B.isFluid(current)) {
|
||||||
@@ -723,6 +741,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
|||||||
private final int[] surfaceHeights = new int[256];
|
private final int[] surfaceHeights = new int[256];
|
||||||
private final PackedWallBuffer walls = new PackedWallBuffer(512);
|
private final PackedWallBuffer walls = new PackedWallBuffer(512);
|
||||||
private final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
|
private final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
|
||||||
|
private int[] upperSurfaceHeights;
|
||||||
|
|
||||||
private CarveScratch() {
|
private CarveScratch() {
|
||||||
for (int index = 0; index < columnMasks.length; index++) {
|
for (int index = 0; index < columnMasks.length; index++) {
|
||||||
@@ -731,6 +750,13 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int[] getOrCreateUpperSurfaceHeights() {
|
||||||
|
if (upperSurfaceHeights == null) {
|
||||||
|
upperSurfaceHeights = new int[256];
|
||||||
|
}
|
||||||
|
return upperSurfaceHeights;
|
||||||
|
}
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
for (int index = 0; index < columnMasks.length; index++) {
|
for (int index = 0; index < columnMasks.length; index++) {
|
||||||
columnMasks[index].clear();
|
columnMasks[index].clear();
|
||||||
|
|||||||
@@ -177,6 +177,19 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
private IrisRange dimensionHeight = new IrisRange(-64, 320);
|
private IrisRange dimensionHeight = new IrisRange(-64, 320);
|
||||||
@Desc("Define options for this dimension")
|
@Desc("Define options for this dimension")
|
||||||
private IrisDimensionTypeOptions dimensionOptions = new IrisDimensionTypeOptions();
|
private IrisDimensionTypeOptions dimensionOptions = new IrisDimensionTypeOptions();
|
||||||
|
@Desc("When true, sets the dimension's ambient light to maximum, making all blocks fully lit regardless of light level.")
|
||||||
|
private boolean fullbright = false;
|
||||||
|
@RegistryListResource(IrisDimension.class)
|
||||||
|
@Desc("When set, generates the referenced dimension's terrain upside-down at the world ceiling, creating a nether-like canopy. Use the dimension's load key (e.g., 'overworld'). Set to 'none' to disable.")
|
||||||
|
private String upperDimension = "none";
|
||||||
|
@MinNumber(0)
|
||||||
|
@MaxNumber(256)
|
||||||
|
@Desc("Minimum air gap in blocks between the lower terrain surface and the upper terrain surface.")
|
||||||
|
private int upperDimensionGap = 32;
|
||||||
|
@Desc("When true, cave carving will cut through the upper dimension terrain. When false, the upper terrain is a solid untouched mass.")
|
||||||
|
private boolean upperDimensionCarving = false;
|
||||||
|
@Desc("When true, objects from the mantle (structures, trees, etc.) can be placed in the upper dimension terrain zone. When false, the upper terrain is protected from object placement.")
|
||||||
|
private boolean upperDimensionObjects = false;
|
||||||
@RegistryListResource(IrisBiome.class)
|
@RegistryListResource(IrisBiome.class)
|
||||||
@Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.")
|
@Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.")
|
||||||
private String focus = "";
|
private String focus = "";
|
||||||
@@ -650,7 +663,15 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimensionType getDimensionType() {
|
public IrisDimensionType getDimensionType() {
|
||||||
return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
|
IrisDimensionTypeOptions options = getDimensionOptions();
|
||||||
|
if (fullbright) {
|
||||||
|
options = options.copy().ambientLight(1.0f);
|
||||||
|
}
|
||||||
|
return new IrisDimensionType(getBaseDimension(), options, getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUpperDimension() {
|
||||||
|
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
|
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.object;
|
|||||||
import art.arcane.iris.engine.object.annotations.Desc;
|
import art.arcane.iris.engine.object.annotations.Desc;
|
||||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.engine.platform.studio.generators.BiomeBuffetGenerator;
|
import art.arcane.iris.engine.platform.studio.generators.BiomeBuffetGenerator;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ public enum StudioMode {
|
|||||||
BIOME_BUFFET_18x18(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 18))),
|
BIOME_BUFFET_18x18(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 18))),
|
||||||
BIOME_BUFFET_36x36(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 36))),
|
BIOME_BUFFET_36x36(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 36))),
|
||||||
REGION_BUFFET(i -> i.setStudioGenerator(null)),
|
REGION_BUFFET(i -> i.setStudioGenerator(null)),
|
||||||
OBJECT_BUFFET(i -> i.setStudioGenerator(null)),
|
OBJECT_BUFFET(i -> i.setStudioGenerator(new ObjectStudioGenerator(i.getEngine()))),
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -778,9 +778,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void computeStudioGenerator() {
|
private void computeStudioGenerator() {
|
||||||
if (!getEngine().getDimension().getStudioMode().equals(lastMode)) {
|
StudioMode desired = getEngine().getDimension().getStudioMode();
|
||||||
lastMode = getEngine().getDimension().getStudioMode();
|
if (studio && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||||
getEngine().getDimension().getStudioMode().inject(this);
|
desired = StudioMode.OBJECT_BUFFET;
|
||||||
|
}
|
||||||
|
if (!desired.equals(lastMode)) {
|
||||||
|
lastMode = desired;
|
||||||
|
desired.inject(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,6 +811,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldGenerateStructures() {
|
public boolean shouldGenerateStructures() {
|
||||||
|
if (isStudio() && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures();
|
return IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+289
@@ -0,0 +1,289 @@
|
|||||||
|
/*
|
||||||
|
* 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 art.arcane.iris.engine.platform.studio.generators;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.ObjectStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell;
|
||||||
|
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||||
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||||
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
|
import art.arcane.iris.engine.platform.studio.EnginedStudioGenerator;
|
||||||
|
import art.arcane.iris.util.common.data.VectorMap;
|
||||||
|
import art.arcane.iris.util.common.math.Vector3i;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Biome;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.util.BlockVector;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class ObjectStudioGenerator extends EnginedStudioGenerator {
|
||||||
|
public static final int DEFAULT_PADDING = 2;
|
||||||
|
private static final BlockData FLOOR = Material.POLISHED_DEEPSLATE.createBlockData();
|
||||||
|
private static final BlockData FRAME = Material.SMOOTH_QUARTZ.createBlockData();
|
||||||
|
private static final BlockData MARKER = Material.END_ROD.createBlockData();
|
||||||
|
private static final Biome DEFAULT_BIOME = Biome.PLAINS;
|
||||||
|
|
||||||
|
private final int padding;
|
||||||
|
private final BlockData floor;
|
||||||
|
private final BlockData frame;
|
||||||
|
private final BlockData marker;
|
||||||
|
private final AtomicBoolean layoutBuilt = new AtomicBoolean(false);
|
||||||
|
private final Object layoutLock = new Object();
|
||||||
|
private final Map<String, IrisObject> objectCache = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, IrisData> packData = new ConcurrentHashMap<>();
|
||||||
|
private volatile ObjectStudioLayout layout;
|
||||||
|
|
||||||
|
public ObjectStudioGenerator(Engine engine) {
|
||||||
|
this(engine, DEFAULT_PADDING, FLOOR, FRAME, MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectStudioGenerator(Engine engine, int padding, BlockData floor, BlockData frame, BlockData marker) {
|
||||||
|
super(engine);
|
||||||
|
this.padding = padding;
|
||||||
|
this.floor = floor;
|
||||||
|
this.frame = frame;
|
||||||
|
this.marker = marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectStudioLayout getLayout() {
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPadding() {
|
||||||
|
return padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException {
|
||||||
|
ensureLayout(engine);
|
||||||
|
|
||||||
|
int floorY = Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y);
|
||||||
|
for (int bx = 0; bx < 16; bx++) {
|
||||||
|
for (int bz = 0; bz < 16; bz++) {
|
||||||
|
tc.setBiome(bx, floorY, bz, DEFAULT_BIOME);
|
||||||
|
tc.setBlock(bx, floorY, bz, floor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectStudioLayout currentLayout = layout;
|
||||||
|
if (currentLayout == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chunkWorldX = x << 4;
|
||||||
|
int chunkWorldZ = z << 4;
|
||||||
|
int chunkMaxX = chunkWorldX + 15;
|
||||||
|
int chunkMaxZ = chunkWorldZ + 15;
|
||||||
|
int minHeight = engine.getMinHeight();
|
||||||
|
int maxHeight = engine.getMaxHeight();
|
||||||
|
|
||||||
|
int plinthY = floorY + 1;
|
||||||
|
|
||||||
|
for (GridCell cell : currentLayout.cells()) {
|
||||||
|
int frameMinX = cell.originX() - 1;
|
||||||
|
int frameMaxX = cell.originX() + cell.w();
|
||||||
|
int frameMinZ = cell.originZ() - 1;
|
||||||
|
int frameMaxZ = cell.originZ() + cell.d();
|
||||||
|
|
||||||
|
if (frameMaxX < chunkWorldX || frameMinX > chunkMaxX) continue;
|
||||||
|
if (frameMaxZ < chunkWorldZ || frameMinZ > chunkMaxZ) continue;
|
||||||
|
|
||||||
|
paintFrame(cell, tc, plinthY, chunkWorldX, chunkWorldZ, minHeight, maxHeight);
|
||||||
|
|
||||||
|
IrisObject object = loadObject(cell);
|
||||||
|
if (object != null) {
|
||||||
|
placeSlice(object, cell, tc, chunkWorldX, chunkWorldZ, minHeight, maxHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintFrame(GridCell cell, TerrainChunk tc, int plinthY, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) {
|
||||||
|
int x0 = cell.originX() - 1;
|
||||||
|
int x1 = cell.originX() + cell.w();
|
||||||
|
int z0 = cell.originZ() - 1;
|
||||||
|
int z1 = cell.originZ() + cell.d();
|
||||||
|
int topY = Math.min(maxHeight - 1, plinthY + cell.h() + 1);
|
||||||
|
|
||||||
|
if (plinthY >= minHeight && plinthY < maxHeight) {
|
||||||
|
paintFrameRowX(tc, x0, x1, plinthY, z0, chunkWorldX, chunkWorldZ);
|
||||||
|
paintFrameRowX(tc, x0, x1, plinthY, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
paintFrameRowZ(tc, x0, plinthY, z0, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
paintFrameRowZ(tc, x1, plinthY, z0, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topY > plinthY && topY < maxHeight) {
|
||||||
|
paintFrameRowX(tc, x0, x1, topY, z0, chunkWorldX, chunkWorldZ);
|
||||||
|
paintFrameRowX(tc, x0, x1, topY, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
paintFrameRowZ(tc, x0, topY, z0, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
paintFrameRowZ(tc, x1, topY, z0, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
int edgeLo = plinthY + 1;
|
||||||
|
int edgeHi = Math.min(topY - 1, maxHeight - 1);
|
||||||
|
if (edgeHi >= edgeLo) {
|
||||||
|
paintEdgePillar(tc, x0, edgeLo, edgeHi, z0, chunkWorldX, chunkWorldZ);
|
||||||
|
paintEdgePillar(tc, x1, edgeLo, edgeHi, z0, chunkWorldX, chunkWorldZ);
|
||||||
|
paintEdgePillar(tc, x0, edgeLo, edgeHi, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
paintEdgePillar(tc, x1, edgeLo, edgeHi, z1, chunkWorldX, chunkWorldZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintEdgePillar(TerrainChunk tc, int x, int yMin, int yMax, int z, int chunkWorldX, int chunkWorldZ) {
|
||||||
|
if (x < chunkWorldX || x > chunkWorldX + 15) return;
|
||||||
|
if (z < chunkWorldZ || z > chunkWorldZ + 15) return;
|
||||||
|
int localX = x - chunkWorldX;
|
||||||
|
int localZ = z - chunkWorldZ;
|
||||||
|
for (int y = yMin; y <= yMax; y++) {
|
||||||
|
tc.setBlock(localX, y, localZ, marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintFrameRowX(TerrainChunk tc, int xMin, int xMax, int y, int z, int chunkWorldX, int chunkWorldZ) {
|
||||||
|
if (z < chunkWorldZ || z > chunkWorldZ + 15) return;
|
||||||
|
int lo = Math.max(xMin, chunkWorldX);
|
||||||
|
int hi = Math.min(xMax, chunkWorldX + 15);
|
||||||
|
for (int x = lo; x <= hi; x++) {
|
||||||
|
tc.setBlock(x - chunkWorldX, y, z - chunkWorldZ, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintFrameRowZ(TerrainChunk tc, int x, int y, int zMin, int zMax, int chunkWorldX, int chunkWorldZ) {
|
||||||
|
if (x < chunkWorldX || x > chunkWorldX + 15) return;
|
||||||
|
int lo = Math.max(zMin, chunkWorldZ);
|
||||||
|
int hi = Math.min(zMax, chunkWorldZ + 15);
|
||||||
|
for (int z = lo; z <= hi; z++) {
|
||||||
|
tc.setBlock(x - chunkWorldX, y, z - chunkWorldZ, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeSlice(IrisObject object, GridCell cell, TerrainChunk tc, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) {
|
||||||
|
VectorMap<BlockData> blocks = object.getBlocks();
|
||||||
|
if (blocks == null || blocks.isEmpty()) return;
|
||||||
|
|
||||||
|
Vector3i center = object.getCenter();
|
||||||
|
int centerX = center == null ? 0 : center.getBlockX();
|
||||||
|
int centerY = center == null ? 0 : center.getBlockY();
|
||||||
|
int centerZ = center == null ? 0 : center.getBlockZ();
|
||||||
|
|
||||||
|
int originX = cell.originX();
|
||||||
|
int originY = cell.originY();
|
||||||
|
int originZ = cell.originZ();
|
||||||
|
|
||||||
|
for (Map.Entry<BlockVector, BlockData> entry : blocks) {
|
||||||
|
BlockVector signed = entry.getKey();
|
||||||
|
int worldX = originX + signed.getBlockX() + centerX;
|
||||||
|
int worldY = originY + signed.getBlockY() + centerY;
|
||||||
|
int worldZ = originZ + signed.getBlockZ() + centerZ;
|
||||||
|
|
||||||
|
if (worldX < chunkWorldX || worldX > chunkWorldX + 15) continue;
|
||||||
|
if (worldZ < chunkWorldZ || worldZ > chunkWorldZ + 15) continue;
|
||||||
|
if (worldY < minHeight || worldY >= maxHeight) continue;
|
||||||
|
|
||||||
|
BlockData data = entry.getValue();
|
||||||
|
if (data == null) continue;
|
||||||
|
|
||||||
|
tc.setBlock(worldX - chunkWorldX, worldY, worldZ - chunkWorldZ, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisObject loadObject(GridCell cell) {
|
||||||
|
String cacheKey = cell.pack() + "/" + cell.key();
|
||||||
|
IrisObject cached = objectCache.get(cacheKey);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
IrisData data = packData.get(cell.pack());
|
||||||
|
if (data == null) return null;
|
||||||
|
IrisObject loaded = data.getObjectLoader().load(cell.key());
|
||||||
|
if (loaded != null) {
|
||||||
|
objectCache.put(cacheKey, loaded);
|
||||||
|
}
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, IrisData> getPackData() {
|
||||||
|
return packData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureLayout(Engine engine) {
|
||||||
|
if (layoutBuilt.get()) return;
|
||||||
|
synchronized (layoutLock) {
|
||||||
|
if (layoutBuilt.get()) return;
|
||||||
|
|
||||||
|
Map<String, IrisData> sources = resolveSources(engine);
|
||||||
|
packData.putAll(sources);
|
||||||
|
|
||||||
|
File layoutFile = layoutFile(engine);
|
||||||
|
ObjectStudioLayout resumed = ObjectStudioLayout.load(layoutFile, sources, padding);
|
||||||
|
if (resumed != null) {
|
||||||
|
layout = resumed;
|
||||||
|
} else {
|
||||||
|
layout = ObjectStudioLayout.build(sources, padding);
|
||||||
|
layout.save(layoutFile);
|
||||||
|
}
|
||||||
|
layoutBuilt.set(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ObjectStudioSaveService.get().register(engine, this);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cellCount = layout.cells().size();
|
||||||
|
BlockVector worldExtent = computeExtent(layout);
|
||||||
|
Iris.info("Object Studio layout built: %d cells from %d pack(s), extent %d x %d blocks",
|
||||||
|
cellCount, sources.size(), worldExtent.getBlockX(), worldExtent.getBlockZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, IrisData> resolveSources(Engine engine) {
|
||||||
|
String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey();
|
||||||
|
Map<String, IrisData> registered = packKey == null ? null : ObjectStudioActivation.getSources(packKey);
|
||||||
|
if (registered != null && !registered.isEmpty()) {
|
||||||
|
return registered;
|
||||||
|
}
|
||||||
|
Map<String, IrisData> fallback = new LinkedHashMap<>();
|
||||||
|
IrisData data = engine.getData();
|
||||||
|
fallback.put(data.getDataFolder().getName(), data);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File layoutFile(Engine engine) {
|
||||||
|
File worldFolder = engine.getTarget().getWorld().worldFolder();
|
||||||
|
return new File(new File(worldFolder, ".iris"), "object-studio-layout.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockVector computeExtent(ObjectStudioLayout layout) {
|
||||||
|
int maxX = 0;
|
||||||
|
int maxZ = 0;
|
||||||
|
for (GridCell cell : layout.cells()) {
|
||||||
|
maxX = Math.max(maxX, cell.originX() + cell.w());
|
||||||
|
maxZ = Math.max(maxZ, cell.originZ() + cell.d());
|
||||||
|
}
|
||||||
|
return new BlockVector(maxX, 0, maxZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user