mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
ADA
This commit is contained in:
32 files changed
+2254
-40
No files matched your search
@@ -80,6 +80,7 @@ import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
public class ServerConfigurator {
|
||||
private static final Object DATAPACK_INSTALL_LOCK = new Object();
|
||||
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
|
||||
|
||||
public static void configure() {
|
||||
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
|
||||
@@ -266,50 +267,71 @@ public class ServerConfigurator {
|
||||
public static DatapackInstallResult installDataPacksIfChanged(boolean fullInstall) {
|
||||
synchronized (DATAPACK_INSTALL_LOCK) {
|
||||
File packsDir = IrisPlatforms.get().dataFolder("packs");
|
||||
String current;
|
||||
File cacheFile = new File(IrisPlatforms.get().dataFolder("cache"), "datapack-fingerprint");
|
||||
FingerprintCache cached = readFingerprintCache(cacheFile.toPath());
|
||||
PackFingerprint fingerprint;
|
||||
try {
|
||||
current = computePackFingerprint(packsDir);
|
||||
fingerprint = resolvePackFingerprint(packsDir, cached.metadata(), cached.content());
|
||||
} catch (RuntimeException exception) {
|
||||
IrisLogging.reportError("Unable to fingerprint Iris packs safely", exception);
|
||||
return DatapackInstallResult.failedResult();
|
||||
}
|
||||
File cacheFile = new File(IrisPlatforms.get().dataFolder("cache"), "datapack-fingerprint");
|
||||
String cached = "";
|
||||
if (cacheFile.exists()) {
|
||||
try {
|
||||
cached = Files.readString(cacheFile.toPath(), StandardCharsets.UTF_8).trim();
|
||||
} catch (IOException e) {
|
||||
cached = "";
|
||||
String current = fingerprint.content();
|
||||
if (!current.isEmpty() && current.equals(cached.content())) {
|
||||
if (!fingerprint.metadata().equals(cached.metadata())) {
|
||||
writeFingerprintCache(cacheFile.toPath(), fingerprint);
|
||||
}
|
||||
}
|
||||
if (!current.isEmpty() && current.equals(cached)) {
|
||||
IrisLogging.debug("Data packs unchanged, skipping install.");
|
||||
return DatapackInstallResult.unchangedResult();
|
||||
}
|
||||
DatapackInstallResult result = installDataPacksLocked(resolveDataFixer(), fullInstall);
|
||||
if (result.succeeded()) {
|
||||
try {
|
||||
writeFingerprintAtomic(cacheFile.toPath(), current);
|
||||
} catch (IOException e) {
|
||||
IrisLogging.warn("Failed to write datapack fingerprint cache: " + e.getMessage());
|
||||
}
|
||||
writeFingerprintCache(cacheFile.toPath(), fingerprint);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static String computePackFingerprint(File packsDir) {
|
||||
if (packsDir == null) {
|
||||
static PackFingerprint resolvePackFingerprint(File packsDir, String cachedMetadata, String cachedContent) {
|
||||
String metadata = computePackMetadataDigest(packsDir);
|
||||
if (!metadata.isEmpty()
|
||||
&& metadata.equals(cachedMetadata)
|
||||
&& cachedContent != null
|
||||
&& !cachedContent.isEmpty()) {
|
||||
return new PackFingerprint(metadata, cachedContent);
|
||||
}
|
||||
return new PackFingerprint(metadata, computePackFingerprint(packsDir));
|
||||
}
|
||||
|
||||
public static String computePackMetadataDigest(File packsDir) {
|
||||
Path root = resolveFingerprintRoot(packsDir);
|
||||
if (root == null) {
|
||||
return "";
|
||||
}
|
||||
Path root = packsDir.toPath().toAbsolutePath().normalize();
|
||||
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return "";
|
||||
}
|
||||
if (!Files.isDirectory(root)) {
|
||||
if (Files.isSymbolicLink(root)) {
|
||||
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
List<FingerprintEntry> entries = collectFingerprintEntries(root.toRealPath());
|
||||
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
|
||||
for (FingerprintEntry entry : entries) {
|
||||
BasicFileAttributes attributes = Files.readAttributes(
|
||||
entry.source(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
||||
byte[] relativePath = entry.relativePath().getBytes(StandardCharsets.UTF_8);
|
||||
updateDigestInt(digest, relativePath.length);
|
||||
digest.update(relativePath);
|
||||
updateDigestLong(digest, attributes.size());
|
||||
updateDigestLong(digest, attributes.lastModifiedTime().toMillis());
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String computePackFingerprint(File packsDir) {
|
||||
Path root = resolveFingerprintRoot(packsDir);
|
||||
if (root == null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
@@ -340,6 +362,45 @@ public class ServerConfigurator {
|
||||
}
|
||||
}
|
||||
|
||||
private static Path resolveFingerprintRoot(File packsDir) {
|
||||
if (packsDir == null) {
|
||||
return null;
|
||||
}
|
||||
Path root = packsDir.toPath().toAbsolutePath().normalize();
|
||||
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return null;
|
||||
}
|
||||
if (!Files.isDirectory(root)) {
|
||||
if (Files.isSymbolicLink(root)) {
|
||||
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private static FingerprintCache readFingerprintCache(Path cacheFile) {
|
||||
if (!Files.isRegularFile(cacheFile)) {
|
||||
return new FingerprintCache("", "");
|
||||
}
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(cacheFile, StandardCharsets.UTF_8);
|
||||
String content = lines.isEmpty() ? "" : lines.getFirst().trim();
|
||||
String metadata = lines.size() > 1 ? lines.get(1).trim() : "";
|
||||
return new FingerprintCache(content, metadata);
|
||||
} catch (IOException e) {
|
||||
return new FingerprintCache("", "");
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeFingerprintCache(Path cacheFile, PackFingerprint fingerprint) {
|
||||
try {
|
||||
writeFingerprintAtomic(cacheFile, fingerprint.content() + "\n" + fingerprint.metadata());
|
||||
} catch (IOException e) {
|
||||
IrisLogging.warn("Failed to write datapack fingerprint cache: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeFingerprintAtomic(Path target, String fingerprint) throws IOException {
|
||||
Path absoluteTarget = target.toAbsolutePath().normalize();
|
||||
Path parent = absoluteTarget.getParent();
|
||||
@@ -368,7 +429,7 @@ public class ServerConfigurator {
|
||||
try (Stream<Path> children = Files.list(root)) {
|
||||
for (Path child : children.toList()) {
|
||||
String childName = child.getFileName().toString();
|
||||
if (PackDirectoryResolver.isHiddenName(childName)) {
|
||||
if (PackDirectoryResolver.isHiddenName(childName) || isGeneratedPackFile(childName)) {
|
||||
continue;
|
||||
}
|
||||
if (Files.isSymbolicLink(child)) {
|
||||
@@ -406,7 +467,8 @@ public class ServerConfigurator {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
if (PackDirectoryResolver.isHiddenName(file.getFileName().toString())) {
|
||||
String fileName = file.getFileName().toString();
|
||||
if (PackDirectoryResolver.isHiddenName(fileName) || isGeneratedPackFile(fileName)) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
|
||||
@@ -427,9 +489,19 @@ public class ServerConfigurator {
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isGeneratedPackFile(String name) {
|
||||
return name != null && name.endsWith(CODE_WORKSPACE_SUFFIX);
|
||||
}
|
||||
|
||||
private record FingerprintEntry(Path source, String relativePath) {
|
||||
}
|
||||
|
||||
record PackFingerprint(String metadata, String content) {
|
||||
}
|
||||
|
||||
private record FingerprintCache(String content, String metadata) {
|
||||
}
|
||||
|
||||
private static void updateDigestInt(MessageDigest digest, int value) {
|
||||
for (int shift = Integer.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
|
||||
digest.update((byte) (value >>> shift));
|
||||
|
||||
@@ -352,9 +352,13 @@ public final class DatapackIngestService {
|
||||
boolean successful = true;
|
||||
for (Entry entry : manifest.entries) {
|
||||
File stagedDir = new File(stagingDir, entry.id);
|
||||
if (isRecordedUnchangedInstall(stagedDir, worldFolders, entry, stripOverrides)) {
|
||||
continue;
|
||||
}
|
||||
if (!isUsableStaging(stagedDir, entry)) {
|
||||
IrisLogging.error("Managed datapack staging is unusable for '" + entry.id
|
||||
+ "' at " + stagedDir.getPath());
|
||||
forgetInstallMetadata(entry);
|
||||
successful = false;
|
||||
continue;
|
||||
}
|
||||
@@ -364,8 +368,10 @@ public final class DatapackIngestService {
|
||||
IrisLogging.warn("Repaired installed datapack '" + entry.id
|
||||
+ "' from Iris staging before datapack compilation.");
|
||||
}
|
||||
recordInstallMetadata(stagedDir, worldFolders, entry);
|
||||
} catch (IOException e) {
|
||||
IrisLogging.reportError(e);
|
||||
forgetInstallMetadata(entry);
|
||||
successful = false;
|
||||
}
|
||||
}
|
||||
@@ -373,6 +379,127 @@ public final class DatapackIngestService {
|
||||
return successful;
|
||||
}
|
||||
|
||||
private static boolean isRecordedUnchangedInstall(
|
||||
File stagedDir,
|
||||
KList<File> worldFolders,
|
||||
Entry entry,
|
||||
boolean stripOverrides
|
||||
) {
|
||||
if (entry.stagingMetadata == null || entry.stagingMetadata.isBlank()
|
||||
|| entry.installMetadata == null || entry.installMetadata.size() != worldFolders.size()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!isRecordedManagedDirectory(stagedDir, entry)
|
||||
|| !entry.stagingMetadata.equals(metadataDigest(stagedDir))) {
|
||||
return false;
|
||||
}
|
||||
for (File worldFolder : worldFolders) {
|
||||
File target = new File(worldFolder, entry.id);
|
||||
if (!isRecordedManagedDirectory(target, entry)
|
||||
|| new File(target, OVERRIDES_STRIPPED_MARKER).isFile() != stripOverrides) {
|
||||
return false;
|
||||
}
|
||||
String recorded = entry.installMetadata.get(installMetadataKey(target));
|
||||
if (recorded == null || !recorded.equals(metadataDigest(target))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
IrisLogging.debug("Managed datapack '" + entry.id
|
||||
+ "' requires full verification: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRecordedManagedDirectory(File directory, Entry entry) throws IOException {
|
||||
Path path = directory.toPath();
|
||||
if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(path)) {
|
||||
return false;
|
||||
}
|
||||
if (!new File(directory, "pack.mcmeta").isFile()) {
|
||||
return false;
|
||||
}
|
||||
Ownership ownership = readOwnershipOrNull(directory);
|
||||
return ownership != null
|
||||
&& ownershipSourceMatches(ownership, entry)
|
||||
&& Objects.equals(ownership.versionId, entry.versionId)
|
||||
&& Objects.equals(ownership.versionNumber, entry.versionNumber)
|
||||
&& Objects.equals(ownership.sha1, entry.sha1);
|
||||
}
|
||||
|
||||
private static void recordInstallMetadata(File stagedDir, KList<File> worldFolders, Entry entry) {
|
||||
try {
|
||||
Map<String, String> recorded = new HashMap<>();
|
||||
for (File worldFolder : worldFolders) {
|
||||
File target = new File(worldFolder, entry.id);
|
||||
recorded.put(installMetadataKey(target), metadataDigest(target));
|
||||
}
|
||||
entry.stagingMetadata = metadataDigest(stagedDir);
|
||||
entry.installMetadata = recorded;
|
||||
} catch (IOException e) {
|
||||
IrisLogging.debug("Unable to record managed datapack metadata for '" + entry.id
|
||||
+ "': " + e.getMessage());
|
||||
forgetInstallMetadata(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static void forgetInstallMetadata(Entry entry) {
|
||||
entry.stagingMetadata = "";
|
||||
entry.installMetadata = new HashMap<>();
|
||||
}
|
||||
|
||||
private static String installMetadataKey(File target) {
|
||||
return target.toPath().toAbsolutePath().normalize().toString();
|
||||
}
|
||||
|
||||
private static String metadataDigest(File root) throws IOException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
Path rootPath = root.toPath().toAbsolutePath().normalize();
|
||||
List<Path> entries = new ArrayList<>();
|
||||
try (Stream<Path> paths = Files.walk(rootPath)) {
|
||||
Iterator<Path> iterator = paths.iterator();
|
||||
int pathCount = 0;
|
||||
while (iterator.hasNext()) {
|
||||
Path path = iterator.next();
|
||||
if (path.equals(rootPath) || isFinderMetadata(path)) {
|
||||
continue;
|
||||
}
|
||||
pathCount++;
|
||||
if (pathCount > MAX_MANAGED_PATHS) {
|
||||
throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths");
|
||||
}
|
||||
if (Files.isSymbolicLink(path)) {
|
||||
throw new IOException("Datapack contains a symbolic link: " + path);
|
||||
}
|
||||
entries.add(path);
|
||||
}
|
||||
}
|
||||
entries.sort(Comparator.comparing(path -> rootPath.relativize(path).toString()));
|
||||
for (Path entry : entries) {
|
||||
String relative = rootPath.relativize(entry).toString().replace(File.separatorChar, '/');
|
||||
byte[] relativeBytes = relative.getBytes(StandardCharsets.UTF_8);
|
||||
BasicFileAttributes attributes = Files.readAttributes(
|
||||
entry, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (!attributes.isDirectory() && !attributes.isRegularFile()) {
|
||||
throw new IOException("Datapack contains an unsupported filesystem entry: " + entry);
|
||||
}
|
||||
digest.update((byte) (attributes.isDirectory() ? 1 : 2));
|
||||
updateDigestInt(digest, relativeBytes.length);
|
||||
digest.update(relativeBytes);
|
||||
if (!attributes.isDirectory()) {
|
||||
updateDigestLong(digest, attributes.size());
|
||||
updateDigestLong(digest, attributes.lastModifiedTime().toMillis());
|
||||
}
|
||||
}
|
||||
return hex(digest.digest());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException("SHA-256 algorithm unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean recoverBeforeReapply(File root, List<File> worldFolders) {
|
||||
try {
|
||||
recoverTransactions(root, worldFolders);
|
||||
@@ -1166,6 +1293,7 @@ public final class DatapackIngestService {
|
||||
}
|
||||
|
||||
private static void recordInstallResult(VolmitSender sender, Report report, Entry entry, InstallResult result, String versionNumber) {
|
||||
forgetInstallMetadata(entry);
|
||||
if (result.changed()) {
|
||||
report.updated.add(entry.id + " (" + safe(versionNumber) + ")");
|
||||
report.requiresRestart = true;
|
||||
@@ -2805,8 +2933,11 @@ public final class DatapackIngestService {
|
||||
copy.lastModified = resolved.lastModified;
|
||||
copy.installedEpoch = resolved.installedEpoch;
|
||||
copy.structuresImported = resolved.structuresImported;
|
||||
copy.stagingMetadata = resolved.stagingMetadata;
|
||||
copy.structureKeys = new ArrayList<>(copyList(resolved.structureKeys));
|
||||
copy.templateKeys = new ArrayList<>(copyList(resolved.templateKeys));
|
||||
copy.installMetadata = new HashMap<>(Objects.requireNonNullElseGet(
|
||||
resolved.installMetadata, Map::of));
|
||||
copy.importedTargets = new HashMap<>(Objects.requireNonNullElseGet(
|
||||
resolved.importedTargets, Map::of));
|
||||
copy.importedBundles = new HashMap<>();
|
||||
@@ -2915,6 +3046,8 @@ public final class DatapackIngestService {
|
||||
}
|
||||
entry.structureKeys = normalizeKeys(entry.structureKeys);
|
||||
entry.templateKeys = normalizeKeys(entry.templateKeys);
|
||||
entry.stagingMetadata = entry.stagingMetadata == null ? "" : entry.stagingMetadata.trim();
|
||||
entry.installMetadata = normalizeImportedTargets(entry.installMetadata);
|
||||
entry.importedTargets = normalizeImportedTargets(entry.importedTargets);
|
||||
entry.importedBundles = normalizeImportedBundles(entry.importedBundles);
|
||||
if (!urls.add(entry.url) || !ids.add(entry.id)) {
|
||||
@@ -4472,8 +4605,10 @@ public final class DatapackIngestService {
|
||||
public String lastModified;
|
||||
public long installedEpoch;
|
||||
public boolean structuresImported;
|
||||
public String stagingMetadata = "";
|
||||
public List<String> structureKeys = new ArrayList<>();
|
||||
public List<String> templateKeys = new ArrayList<>();
|
||||
public Map<String, String> installMetadata = new HashMap<>();
|
||||
public Map<String, String> importedTargets = new HashMap<>();
|
||||
public Map<String, Map<String, String>> importedBundles = new HashMap<>();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
||||
@@ -143,6 +144,14 @@ public interface INMSBinding {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* World-space piece bounds of every native structure that will generate inside the given XZ rect. Bindings
|
||||
* without native structure support answer with no volumes, which leaves the object veto inert.
|
||||
*/
|
||||
default KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
|
||||
int getBiomeId(Biome biome);
|
||||
|
||||
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
|
||||
|
||||
@@ -35,7 +35,6 @@ import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.dom4j.Document;
|
||||
import org.dom4j.Element;
|
||||
|
||||
@@ -43,7 +42,11 @@ import java.awt.Desktop;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
@@ -129,10 +132,7 @@ public class IrisCodeWorkspace {
|
||||
File ws = getCodeWorkspaceFile();
|
||||
|
||||
try {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
JSONObject j = createCodeWorkspaceConfig();
|
||||
IO.writeAll(ws, j.toString(4));
|
||||
p.end();
|
||||
writeIfChanged(ws, createCodeWorkspaceConfig().toString(4));
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
@@ -153,6 +153,13 @@ public class IrisCodeWorkspace {
|
||||
return createCodeWorkspaceConfig(true);
|
||||
}
|
||||
|
||||
private static void writeIfChanged(File target, String rendered) throws IOException {
|
||||
if (target.isFile() && (rendered + "\n").equals(IO.readAll(target))) {
|
||||
return;
|
||||
}
|
||||
IO.writeAll(target, rendered);
|
||||
}
|
||||
|
||||
private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) {
|
||||
JSONObject ws = new JSONObject();
|
||||
JSONArray folders = new JSONArray();
|
||||
@@ -184,16 +191,17 @@ public class IrisCodeWorkspace {
|
||||
settings.put("[json]", jc);
|
||||
settings.put("json.maxItemsComputed", 30000);
|
||||
JSONArray schemas = new JSONArray();
|
||||
List<JSONObject> schemaEntries = new ArrayList<>();
|
||||
IrisData dm = null;
|
||||
if (includeSchemas) {
|
||||
dm = IrisData.get(project.getPath());
|
||||
for (ResourceLoader<?> r : dm.getLoaders().v()) {
|
||||
if (r.supportsSchemas()) {
|
||||
schemas.put(r.buildSchema());
|
||||
schemaEntries.add(r.buildSchema());
|
||||
}
|
||||
}
|
||||
|
||||
for (Class<?> i : dm.resolveSnippets()) {
|
||||
for (Class<?> i : sortedSnippets(dm.resolveSnippets())) {
|
||||
try {
|
||||
String snipType = i.getDeclaredAnnotation(Snippet.class).value();
|
||||
JSONObject o = new JSONObject();
|
||||
@@ -205,7 +213,7 @@ public class IrisCodeWorkspace {
|
||||
|
||||
o.put("fileMatch", new JSONArray(fm.toArray()));
|
||||
o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json");
|
||||
schemas.put(o);
|
||||
schemaEntries.add(o);
|
||||
IrisData snippetData = dm;
|
||||
File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json");
|
||||
J.attemptAsync(() -> {
|
||||
@@ -219,6 +227,11 @@ public class IrisCodeWorkspace {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
schemaEntries.sort(Comparator.comparing(entry -> entry.getString("url")));
|
||||
for (JSONObject entry : schemaEntries) {
|
||||
schemas.put(entry);
|
||||
}
|
||||
}
|
||||
|
||||
settings.put("json.schemas", schemas);
|
||||
@@ -295,4 +308,10 @@ public class IrisCodeWorkspace {
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
private static List<Class<?>> sortedSnippets(Set<Class<?>> snippets) {
|
||||
List<Class<?>> sorted = new ArrayList<>(snippets);
|
||||
sorted.sort(Comparator.comparing(Class::getName));
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ import art.arcane.iris.engine.framework.EngineEffects;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||
import art.arcane.iris.engine.framework.NativeStructureVolumeMemo;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
@@ -116,6 +119,9 @@ public class IrisEngine implements Engine {
|
||||
private final SeedManager seedManager;
|
||||
private final GenerationSessionManager generationSessions;
|
||||
private final EnginePlatformHooks platformHooks;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final NativeStructureVolumeMemo nativeStructureVolumeMemo = new NativeStructureVolumeMemo();
|
||||
private final AtomicBoolean closing;
|
||||
@Setter(AccessLevel.NONE)
|
||||
volatile IrisEngineData engineData;
|
||||
@@ -316,9 +322,15 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
|
||||
public void hotloadSilently() {
|
||||
nativeStructureVolumeMemo.clear();
|
||||
hotloader.hotloadSilently();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
||||
return nativeStructureVolumeMemo.volumes(this, platformHooks, minX, minZ, maxX, maxZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisEngineData getEngineData() {
|
||||
return engineDataStore.getEngineData();
|
||||
|
||||
@@ -92,6 +92,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
EnginePlatformHooks getPlatformHooks();
|
||||
|
||||
/**
|
||||
* World-space native structure piece bounds overlapping the given XZ rect. The answer is a pure function of the
|
||||
* seed, the registries and this pack's structure policy, so it never depends on generation order.
|
||||
*/
|
||||
default KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
||||
EnginePlatformHooks hooks = getPlatformHooks();
|
||||
return hooks == null ? NativeStructureVolume.NONE : hooks.nativeStructureVolumes(this, minX, minZ, maxX, maxZ);
|
||||
}
|
||||
|
||||
int getBlockUpdatesPerSecond();
|
||||
|
||||
void printMetrics(VolmitSender sender);
|
||||
|
||||
@@ -19,8 +19,17 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
public interface EnginePlatformHooks {
|
||||
/**
|
||||
* World-space piece bounds of every native structure that will generate inside the given XZ rect. Platforms
|
||||
* without native structures return no volumes, which keeps the object veto free on those platforms.
|
||||
*/
|
||||
default KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
|
||||
default void refreshWorkspace(Engine engine) {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 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.framework;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
/**
|
||||
* World-space axis-aligned bounds of one native structure piece. Volumes are resolved from seed, registry and
|
||||
* pack policy alone so the same query answers identically regardless of which chunks exist.
|
||||
*/
|
||||
public record NativeStructureVolume(
|
||||
String structure,
|
||||
int minX,
|
||||
int minY,
|
||||
int minZ,
|
||||
int maxX,
|
||||
int maxY,
|
||||
int maxZ
|
||||
) {
|
||||
public static final KList<NativeStructureVolume> NONE = new KList<>();
|
||||
|
||||
public static NativeStructureVolume of(String structure, int aX, int aY, int aZ, int bX, int bY, int bZ) {
|
||||
return new NativeStructureVolume(
|
||||
structure,
|
||||
Math.min(aX, bX),
|
||||
Math.min(aY, bY),
|
||||
Math.min(aZ, bZ),
|
||||
Math.max(aX, bX),
|
||||
Math.max(aY, bY),
|
||||
Math.max(aZ, bZ));
|
||||
}
|
||||
|
||||
public boolean intersectsRect(int rectMinX, int rectMinZ, int rectMaxX, int rectMaxZ) {
|
||||
return maxX >= rectMinX && minX <= rectMaxX && maxZ >= rectMinZ && minZ <= rectMaxZ;
|
||||
}
|
||||
|
||||
public boolean intersects(int boxMinX, int boxMinY, int boxMinZ, int boxMaxX, int boxMaxY, int boxMaxZ) {
|
||||
return maxX >= boxMinX && minX <= boxMaxX
|
||||
&& maxY >= boxMinY && minY <= boxMaxY
|
||||
&& maxZ >= boxMinZ && minZ <= boxMaxZ;
|
||||
}
|
||||
|
||||
public boolean contains(int x, int y, int z) {
|
||||
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
|
||||
}
|
||||
|
||||
public boolean containsWithin(int x, int y, int z, int margin) {
|
||||
return x >= minX - margin && x <= maxX + margin
|
||||
&& y >= minY - margin && y <= maxY + margin
|
||||
&& z >= minZ - margin && z <= maxZ + margin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 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.framework;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
/**
|
||||
* Chunk keyed memo in front of the platform volume hook. One chunk's object pass fires the same rect query dozens of
|
||||
* times, so the hook is asked once per chunk column and every later object reuses that answer. Long keyed so the
|
||||
* overwhelmingly common "no native structure anywhere near" lookup allocates nothing at all.
|
||||
*/
|
||||
public final class NativeStructureVolumeMemo {
|
||||
private static final int MAX_CACHED_CHUNKS = 2_048;
|
||||
|
||||
private final Long2ObjectLinkedOpenHashMap<KList<NativeStructureVolume>> chunks =
|
||||
new Long2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
public KList<NativeStructureVolume> volumes(Engine engine, EnginePlatformHooks hooks,
|
||||
int minX, int minZ, int maxX, int maxZ) {
|
||||
if (hooks == null) {
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
|
||||
int fromChunkX = Math.min(minX, maxX) >> 4;
|
||||
int toChunkX = Math.max(minX, maxX) >> 4;
|
||||
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
|
||||
int toChunkZ = Math.max(minZ, maxZ) >> 4;
|
||||
KList<NativeStructureVolume> matches = null;
|
||||
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
||||
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
||||
for (NativeStructureVolume volume : chunkVolumes(engine, hooks, chunkX, chunkZ)) {
|
||||
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||
continue;
|
||||
}
|
||||
if (matches == null) {
|
||||
matches = new KList<>();
|
||||
}
|
||||
if (!matches.contains(volume)) {
|
||||
matches.add(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches == null ? NativeStructureVolume.NONE : matches;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (chunks) {
|
||||
chunks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private KList<NativeStructureVolume> chunkVolumes(Engine engine, EnginePlatformHooks hooks,
|
||||
int chunkX, int chunkZ) {
|
||||
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||
synchronized (chunks) {
|
||||
KList<NativeStructureVolume> hit = chunks.getAndMoveToFirst(key);
|
||||
if (hit != null) {
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
|
||||
int minX = chunkX << 4;
|
||||
int minZ = chunkZ << 4;
|
||||
KList<NativeStructureVolume> resolved = hooks.nativeStructureVolumes(engine, minX, minZ, minX + 15, minZ + 15);
|
||||
if (resolved == null) {
|
||||
resolved = NativeStructureVolume.NONE;
|
||||
}
|
||||
synchronized (chunks) {
|
||||
chunks.putAndMoveToFirst(key, resolved);
|
||||
while (chunks.size() > MAX_CACHED_CHUNKS) {
|
||||
chunks.removeLast();
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.object;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||
import art.arcane.iris.engine.framework.PlacedObject;
|
||||
import art.arcane.iris.engine.framework.placer.HeightmapObjectPlacer;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -343,6 +344,12 @@ final class IrisObjectPlacementRunner {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int warpMargin = warped ? (int) Math.ceil(Math.abs(config.getWarp().getMultiplier()) / 2D) : 0;
|
||||
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang,
|
||||
yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
|
||||
Engine engine = rdata.getEngine();
|
||||
IrisBlockVector offset = new IrisBlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
|
||||
@@ -841,6 +848,87 @@ final class IrisObjectPlacementRunner {
|
||||
return !wouldReplace && (rawStructurePiece || !air);
|
||||
}
|
||||
|
||||
/**
|
||||
* Objects may never intersect a native structure piece. The rect query is the fast path: no native structures
|
||||
* near this placement means no per block work at all. Only when the placement envelope meets a piece does the
|
||||
* precise pass run, and the first solid block inside a piece rejects the whole object before any write.
|
||||
*/
|
||||
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin,
|
||||
boolean translating, IrisBlockVector translateOffset, boolean ceilingHang,
|
||||
boolean paint, int warpMargin, int x, int y, int z) {
|
||||
Engine engine = placer.getEngine();
|
||||
if (engine == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int margin = (Math.max(self.getW(), Math.max(self.getH(), self.getD())) / 2) + 1 + warpMargin;
|
||||
if (translating) {
|
||||
margin += Math.max(Math.abs(translateOffset.getBlockX()),
|
||||
Math.max(Math.abs(translateOffset.getBlockY()), Math.abs(translateOffset.getBlockZ())));
|
||||
}
|
||||
|
||||
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(x - margin, z - margin, x + margin, z + margin);
|
||||
if (volumes == null || volumes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int envelopeMinY = paint ? Integer.MIN_VALUE : y - margin;
|
||||
int envelopeMaxY = paint ? Integer.MAX_VALUE : y + margin;
|
||||
boolean envelopeMeetsPiece = false;
|
||||
for (NativeStructureVolume volume : volumes) {
|
||||
if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) {
|
||||
envelopeMeetsPiece = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!envelopeMeetsPiece) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.readLock.lock();
|
||||
try {
|
||||
VectorMap<PlatformBlockState>.Cursor cursor = self.blocks.cursor();
|
||||
while (cursor.next()) {
|
||||
PlatformBlockState state = cursor.value();
|
||||
if (state == null || isAirBlock(state)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisBlockVector i = cursor.key().clone();
|
||||
spin.rotate(i);
|
||||
if (ceilingHang) {
|
||||
i.setY(-i.getBlockY());
|
||||
}
|
||||
if (translating) {
|
||||
i.add(translateOffset);
|
||||
}
|
||||
|
||||
int xx = x + (int) Math.round(i.getX());
|
||||
int zz = z + (int) Math.round(i.getZ());
|
||||
int yy = paint
|
||||
? (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2)
|
||||
+ placer.getHighest(xx, zz, self.getLoader(), config.isUnderwater())
|
||||
: y + (int) Math.round(i.getY());
|
||||
|
||||
for (NativeStructureVolume volume : volumes) {
|
||||
if (volume.containsWithin(xx, yy, zz, warpMargin)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isAirBlock(PlatformBlockState state) {
|
||||
String material = IrisObjectShaping.materialKey(state);
|
||||
return material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||
}
|
||||
|
||||
private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) {
|
||||
String key = self.getLoadKey();
|
||||
String fingerprint = (key == null ? "<null>" : key) + "|" + config.getMode();
|
||||
|
||||
Reference in new issue
Block a user