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;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
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.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
@@ -101,6 +103,18 @@ public class CommandFind implements DirectorExecutor {
|
||||
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)) {
|
||||
e.gotoObject(object, player(), teleport);
|
||||
return;
|
||||
|
||||
@@ -21,16 +21,19 @@ package art.arcane.iris.core.commands;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.WorldEditLink;
|
||||
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.StudioSVC;
|
||||
import art.arcane.iris.core.service.WandSVC;
|
||||
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.object.*;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.data.registry.Materials;
|
||||
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.annotations.Director;
|
||||
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.")
|
||||
public void convert () {
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,8 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
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.tools.IrisToolbelt;
|
||||
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());
|
||||
}
|
||||
|
||||
@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"})
|
||||
public void vscode(
|
||||
@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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (world == null) {
|
||||
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")
|
||||
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
|
||||
if (world == null || names == null || names.length == 0) {
|
||||
|
||||
@@ -173,13 +173,19 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (cancelled || !boardEnabled || !player.isOnline()) {
|
||||
if (!boardEnabled || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
board.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEligibleWorld(player)) {
|
||||
boards.remove(player);
|
||||
cancel();
|
||||
cancelled = true;
|
||||
board.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,9 +195,16 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
cancelled = true;
|
||||
if (J.isOwnedByCurrentRegion(player) && player.isOnline()) {
|
||||
board.remove();
|
||||
} else {
|
||||
J.runEntity(player, board::remove);
|
||||
}
|
||||
}
|
||||
|
||||
public void update() {
|
||||
final World world = player.getWorld();
|
||||
|
||||
@@ -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.events.IrisEngineHotloadEvent;
|
||||
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.nms.container.BlockPos;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
@@ -106,6 +107,7 @@ public class IrisEngine implements Engine {
|
||||
private double maxBiomeLayerDensity;
|
||||
private double maxBiomeDecoratorDensity;
|
||||
private IrisComplex complex;
|
||||
private UpperDimensionContext upperContext;
|
||||
private final AtomicBoolean modeFallbackLogged;
|
||||
|
||||
public IrisEngine(EngineTarget target, boolean studio) {
|
||||
@@ -207,6 +209,7 @@ public class IrisEngine implements Engine {
|
||||
Iris.debug("Setup Engine " + getCacheID());
|
||||
cacheId = RNG.r.nextInt();
|
||||
complex = ensureComplex();
|
||||
upperContext = buildUpperContext();
|
||||
effects = new IrisEngineEffects(this);
|
||||
hash32 = new CompletableFuture<>();
|
||||
mantle.hotload();
|
||||
@@ -234,6 +237,25 @@ public class IrisEngine implements Engine {
|
||||
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() {
|
||||
EngineMode currentMode = mode;
|
||||
if (currentMode != null) {
|
||||
@@ -344,6 +366,7 @@ public class IrisEngine implements Engine {
|
||||
public void hotloadComplex() {
|
||||
complex.close();
|
||||
complex = new IrisComplex(this);
|
||||
upperContext = buildUpperContext();
|
||||
}
|
||||
|
||||
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.core.loader.IrisData;
|
||||
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.IrisDimension;
|
||||
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> rockCache = context.getRock();
|
||||
int realX = xf + x;
|
||||
UpperDimensionContext upperContext = getEngine().getUpperContext();
|
||||
|
||||
for (int zf = 0; zf < chunkDepth; zf++) {
|
||||
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.tools.IrisToolbelt;
|
||||
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.chunk.TerrainChunk;
|
||||
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 {
|
||||
IrisComplex getComplex();
|
||||
|
||||
default @Nullable UpperDimensionContext getUpperContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
EngineMode getMode();
|
||||
|
||||
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.link.Identifier;
|
||||
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.EngineTarget;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
@@ -191,9 +192,34 @@ public interface EngineMantle extends MatterGenerator {
|
||||
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 {
|
||||
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 {
|
||||
chunk.release();
|
||||
}
|
||||
|
||||
@@ -150,6 +150,20 @@ public class IrisCaveCarver3D {
|
||||
double thresholdPenalty,
|
||||
IrisRange worldYRange,
|
||||
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();
|
||||
try {
|
||||
@@ -165,8 +179,9 @@ public class IrisCaveCarver3D {
|
||||
double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight));
|
||||
double resolvedThresholdPenalty = Math.max(0D, thresholdPenalty);
|
||||
int worldHeight = writer.getMantle().getWorldHeight();
|
||||
int minY = Math.max(0, (int) Math.floor(profile.getVerticalRange().getMin()));
|
||||
int maxY = Math.min(worldHeight - 1, (int) Math.ceil(profile.getVerticalRange().getMax()));
|
||||
IrisRange effectiveVerticalRange = overrideVerticalRange != null ? overrideVerticalRange : profile.getVerticalRange();
|
||||
int minY = Math.max(0, (int) Math.floor(effectiveVerticalRange.getMin()));
|
||||
int maxY = Math.min(worldHeight - 1, (int) Math.ceil(effectiveVerticalRange.getMax()));
|
||||
if (worldYRange != null) {
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int rangeMinY = (int) Math.floor(worldYRange.getMin() - worldMinHeight);
|
||||
|
||||
+58
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
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.mantle.ComponentFlag;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
@@ -91,6 +92,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
for (WeightedProfile weightedProfile : weightedProfiles) {
|
||||
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
|
||||
}
|
||||
|
||||
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
|
||||
if (upperCtx != null && getDimension().isUpperDimensionCarving()) {
|
||||
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights);
|
||||
}
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
@@ -99,6 +105,58 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
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) {
|
||||
BlendScratch blendScratch = BLEND_SCRATCH.get();
|
||||
IrisCaveProfile[] profileField = blendScratch.profileField;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
||||
@@ -82,10 +83,22 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
||||
MatterCavern[] boundaryCaverns = scratch.boundaryCaverns;
|
||||
int[] surfaceHeights = scratch.surfaceHeights;
|
||||
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++) {
|
||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
|
||||
int localZ = columnIndex & 15;
|
||||
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 {
|
||||
@@ -102,6 +115,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
||||
int rx = xx & 15;
|
||||
int rz = zz & 15;
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
||||
|
||||
if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockData current = output.getRaw(rx, yy, rz);
|
||||
|
||||
if (B.isFluid(current)) {
|
||||
@@ -723,6 +741,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
||||
private final int[] surfaceHeights = new int[256];
|
||||
private final PackedWallBuffer walls = new PackedWallBuffer(512);
|
||||
private final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
|
||||
private int[] upperSurfaceHeights;
|
||||
|
||||
private CarveScratch() {
|
||||
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() {
|
||||
for (int index = 0; index < columnMasks.length; index++) {
|
||||
columnMasks[index].clear();
|
||||
|
||||
@@ -177,6 +177,19 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private IrisRange dimensionHeight = new IrisRange(-64, 320);
|
||||
@Desc("Define options for this dimension")
|
||||
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)
|
||||
@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 = "";
|
||||
@@ -650,7 +663,15 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.object;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.platform.studio.generators.BiomeBuffetGenerator;
|
||||
import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator;
|
||||
|
||||
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_36x36(i -> i.setStudioGenerator(new BiomeBuffetGenerator(i.getEngine(), 36))),
|
||||
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() {
|
||||
if (!getEngine().getDimension().getStudioMode().equals(lastMode)) {
|
||||
lastMode = getEngine().getDimension().getStudioMode();
|
||||
getEngine().getDimension().getStudioMode().inject(this);
|
||||
StudioMode desired = getEngine().getDimension().getStudioMode();
|
||||
if (studio && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||
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
|
||||
public boolean shouldGenerateStructures() {
|
||||
if (isStudio() && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
|
||||
return false;
|
||||
}
|
||||
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