This commit is contained in:
Brian Neumann-Fopiano
2026-02-19 04:09:06 -05:00
parent b491d9efd0
commit 415c2f5837
11 changed files with 392 additions and 843 deletions

View File

@@ -215,9 +215,6 @@ public class IrisSettings {
public boolean commandSounds = true;
public boolean debug = false;
public boolean dumpMantleOnError = false;
public boolean validatePacksOnStartup = true;
public boolean stopStartupOnPackValidationFailure = false;
public int maxPackValidationErrorsPerPack = 200;
public boolean disableNMS = false;
public boolean pluginMetrics = true;
public boolean splashLogoStartup = true;

View File

@@ -24,7 +24,6 @@ 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.service.ConversionSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisNoisemapPrebakePipeline;
@@ -268,12 +267,6 @@ public class CommandStudio implements DirectorExecutor {
}
}
@Director(description = "Convert objects in the \"convert\" folder")
public void convert() {
Iris.service(ConversionSVC.class).check(sender());
//IrisConverter.convertSchematics(sender());
}
@Director(description = "Execute a script", aliases = "run", origin = DirectorOrigin.PLAYER)
public void execute(
@Param(description = "The script to run")

View File

@@ -151,6 +151,10 @@ public interface INMSBinding {
KList<String> getStructureKeys();
default KMap<String, KList<String>> getVanillaStructureBiomeTags() {
return new KMap<>();
}
boolean missingDimensionTypes(String... keys);
default boolean injectBukkit() {

View File

@@ -21,7 +21,6 @@ package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorContextHandler;
@@ -48,8 +47,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -83,16 +80,6 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
}
@EventHandler
public void on(PlayerCommandPreprocessEvent e) {
String msg = e.getMessage().startsWith("/") ? e.getMessage().substring(1) : e.getMessage();
if ((msg.startsWith("locate ") || msg.startsWith("locatebiome ")) && IrisToolbelt.isIrisWorld(e.getPlayer().getWorld())) {
new VolmitSender(e.getPlayer()).sendMessage(C.RED + "Locating biomes & objects is disabled in Iris Worlds. Use /iris studio goto <biome>");
e.setCancelled(true);
}
}
public DirectorRuntimeEngine getDirector() {
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
new CommandIris(),

View File

@@ -1,160 +0,0 @@
/*
* 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.nms.INMS;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.Converter;
import art.arcane.volmlib.util.io.IO;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
public class ConversionSVC implements IrisService {
private KList<Converter> converters;
private File folder;
@Override
public void onEnable() {
folder = Iris.instance.getDataFolder("convert");
converters = new KList<>();
J.s(() ->
J.attemptAsync(() ->
{
}), 5);
}
@Override
public void onDisable() {
}
private void findAllNBT(File path, KList<File> found) {
if (path == null) {
return;
}
if (path.isFile() && path.getName().endsWith(".nbt")) {
found.add(path);
return;
}
File[] children = path.listFiles();
if (children == null) {
return;
}
for (File i : children) {
if (i.isDirectory()) {
findAllNBT(i, found);
} else if (i.isFile() && i.getName().endsWith(".nbt")) {
found.add(i);
}
}
}
private void ensurePackMetadata(File datapackRoot) throws IOException {
File mcmeta = new File(datapackRoot, "pack.mcmeta");
if (mcmeta.exists()) {
return;
}
int format = INMS.get().getDataVersion().getPackFormat();
String meta = """
{
"pack": {
"description": "Iris loose structure ingestion pack",
"pack_format": {}
}
}
""".replace("{}", String.valueOf(format));
IO.writeAll(mcmeta, meta);
}
private int ingestLooseNbtStructures(File source, File datapackRoot) {
KList<File> nbtFiles = new KList<>();
findAllNBT(source, nbtFiles);
if (nbtFiles.isEmpty()) {
return 0;
}
File structureRoot = new File(datapackRoot, "data/iris_loose/structures");
structureRoot.mkdirs();
int copied = 0;
try {
ensurePackMetadata(datapackRoot);
for (File nbt : nbtFiles) {
String relative = source.toURI().relativize(nbt.toURI()).getPath();
if (relative == null || relative.isEmpty()) {
continue;
}
File output = new File(structureRoot, relative);
File parent = output.getParentFile();
if (parent != null) {
parent.mkdirs();
}
Files.copy(nbt.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING);
copied++;
}
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
}
return copied;
}
public void check(VolmitSender s) {
int m = 0;
Iris.instance.getDataFolder("convert");
File[] files = folder.listFiles();
if (files == null) {
s.sendMessage("Converted 0 Files");
return;
}
for (File i : files) {
for (Converter j : converters) {
if (i.getName().endsWith("." + j.getInExtension())) {
File out = new File(folder, i.getName().replaceAll("\\Q." + j.getInExtension() + "\\E", "." + j.getOutExtension()));
m++;
j.convert(i, out);
s.sendMessage("Converted " + i.getName() + " -> " + out.getName());
}
}
}
File loose = new File(folder, "structures");
if (loose.isDirectory()) {
File datapackRoot = Iris.instance.getDataFolder("datapacks", "iris-loose-structures");
int ingested = ingestLooseNbtStructures(loose, datapackRoot);
s.sendMessage("Ingested " + ingested + " loose NBT structure" + (ingested == 1 ? "" : "s") + " into " + datapackRoot.getName());
}
s.sendMessage("Converted " + m + " File" + (m == 1 ? "" : "s"));
}
}

View File

@@ -1,576 +0,0 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.util.common.data.registry.KeyedRegistry;
import art.arcane.iris.util.common.data.registry.RegistryUtil;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.bukkit.NamespacedKey;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class PackValidationSVC implements IrisService {
private final Map<Class<?>, Map<String, Field>> fieldCache = new ConcurrentHashMap<>();
private final Map<Class<?>, Boolean> keyedTypeCache = new ConcurrentHashMap<>();
@Override
public void onEnable() {
IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral();
if (!general.isValidatePacksOnStartup()) {
return;
}
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packFolders = packsFolder.listFiles(File::isDirectory);
if (packFolders == null || packFolders.length == 0) {
Iris.info("Startup pack validation skipped: no pack folders found.");
return;
}
int maxLoggedIssues = Math.max(1, general.getMaxPackValidationErrorsPerPack());
int totalPacks = 0;
int totalFiles = 0;
int totalErrors = 0;
int packsWithErrors = 0;
long started = System.currentTimeMillis();
Iris.info("Startup pack validation started for %d pack(s).", packFolders.length);
for (File packFolder : packFolders) {
totalPacks++;
PackValidationReport report = validatePack(packFolder, maxLoggedIssues);
totalFiles += report.filesChecked;
totalErrors += report.errorCount;
if (report.errorCount > 0) {
packsWithErrors++;
Iris.error("Pack \"%s\" has %d validation issue(s) across %d file(s).", report.packName, report.errorCount, report.filesChecked);
for (String issue : report.sampleIssues) {
Iris.error(" - %s", issue);
}
int hiddenIssues = report.errorCount - report.sampleIssues.size();
if (hiddenIssues > 0) {
Iris.error(" - ... %d additional issue(s) not shown", hiddenIssues);
}
}
}
long elapsed = System.currentTimeMillis() - started;
if (totalErrors == 0) {
Iris.info("Startup pack validation finished: %d pack(s), %d file(s), no issues (%dms).", totalPacks, totalFiles, elapsed);
return;
}
Iris.error("Startup pack validation finished: %d issue(s) in %d pack(s), %d file(s) checked (%dms).", totalErrors, packsWithErrors, totalFiles, elapsed);
if (general.isStopStartupOnPackValidationFailure()) {
throw new IllegalStateException("Pack validation failed with " + totalErrors + " issue(s).");
}
}
@Override
public void onDisable() {
}
private PackValidationReport validatePack(File packFolder, int maxLoggedIssues) {
PackValidationReport report = new PackValidationReport(packFolder.getName(), maxLoggedIssues);
IrisData data = IrisData.get(packFolder);
Collection<ResourceLoader<? extends IrisRegistrant>> loaders = data.getLoaders().values();
for (ResourceLoader<? extends IrisRegistrant> loader : loaders) {
Class<?> rootType = loader.getObjectClass();
if (rootType == null) {
continue;
}
List<File> folders = loader.getFolders();
for (File folder : folders) {
validateFolder(data, packFolder, folder, rootType, report);
}
}
validateSnippetFolders(data, packFolder, report);
return report;
}
private void validateSnippetFolders(IrisData data, File packFolder, PackValidationReport report) {
File snippetRoot = new File(packFolder, "snippet");
if (!snippetRoot.isDirectory()) {
return;
}
Map<String, Class<?>> snippetTypes = new HashMap<>();
for (Class<?> snippetType : data.resolveSnippets()) {
Snippet snippet = snippetType.getDeclaredAnnotation(Snippet.class);
if (snippet == null) {
continue;
}
snippetTypes.put(snippet.value(), snippetType);
}
for (Map.Entry<String, Class<?>> entry : snippetTypes.entrySet()) {
File typeFolder = new File(snippetRoot, entry.getKey());
if (!typeFolder.isDirectory()) {
continue;
}
validateFolder(data, packFolder, typeFolder, entry.getValue(), report);
}
}
private void validateFolder(IrisData data, File packFolder, File folder, Class<?> rootType, PackValidationReport report) {
List<File> jsonFiles = new ArrayList<>();
collectJsonFiles(folder, jsonFiles);
for (File jsonFile : jsonFiles) {
report.filesChecked++;
validateJsonFile(data, packFolder, jsonFile, rootType, report);
}
}
private void collectJsonFiles(File folder, List<File> output) {
File[] files = folder.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
collectJsonFiles(file, output);
continue;
}
if (file.isFile() && file.getName().endsWith(".json")) {
output.add(file);
}
}
}
private void validateJsonFile(IrisData data, File packFolder, File file, Class<?> rootType, PackValidationReport report) {
String content;
try {
content = IO.readAll(file);
} catch (Throwable e) {
report.addIssue(packFolder, file, "$", "Unable to read file: " + simpleMessage(e));
return;
}
JSONObject object;
try {
object = new JSONObject(content);
} catch (Throwable e) {
report.addIssue(packFolder, file, "$", "Invalid JSON syntax: " + simpleMessage(e));
return;
}
try {
Object decoded = data.getGson().fromJson(content, rootType);
if (decoded == null) {
report.addIssue(packFolder, file, "$", "Decoded value is null for root type " + rootType.getSimpleName());
}
} catch (Throwable e) {
report.addIssue(packFolder, file, "$", "Deserializer rejected file for " + rootType.getSimpleName() + ": " + simpleMessage(e));
}
validateObject(packFolder, file, "$", object, rootType, report);
}
private void validateObject(File packFolder, File file, String path, JSONObject object, Class<?> type, PackValidationReport report) {
if (type == null) {
return;
}
Map<String, Field> fields = getSerializableFields(type);
for (String key : object.keySet()) {
Field field = fields.get(key);
String keyPath = path + "." + key;
if (field == null) {
report.addIssue(packFolder, file, keyPath, "Unknown or misplaced key for type " + type.getSimpleName());
continue;
}
Object value = object.get(key);
validateValue(packFolder, file, keyPath, value, field.getType(), field.getGenericType(), field, report);
}
}
private void validateValue(File packFolder, File file, String path, Object value, Class<?> expectedType, Type genericType, Field sourceField, PackValidationReport report) {
Class<?> normalizedType = normalizeType(expectedType);
if (value == JSONObject.NULL) {
if (normalizedType.isPrimitive()) {
report.addIssue(packFolder, file, path, "Null value is not allowed for primitive type " + normalizedType.getSimpleName());
}
return;
}
if (Collection.class.isAssignableFrom(normalizedType)) {
validateCollection(packFolder, file, path, value, genericType, sourceField, report);
return;
}
if (normalizedType.isArray()) {
validateArray(packFolder, file, path, value, normalizedType, report);
return;
}
if (Map.class.isAssignableFrom(normalizedType)) {
validateMap(packFolder, file, path, value, genericType, report);
return;
}
if (normalizedType.isEnum()) {
validateEnum(packFolder, file, path, value, normalizedType, report);
return;
}
if (isKeyedType(normalizedType)) {
validateKeyed(packFolder, file, path, value, normalizedType, report);
return;
}
if (normalizedType == String.class) {
if (!(value instanceof String)) {
report.addIssue(packFolder, file, path, "Expected string value");
}
return;
}
if (normalizedType == Boolean.class) {
if (!(value instanceof Boolean)) {
report.addIssue(packFolder, file, path, "Expected boolean value");
}
return;
}
if (Number.class.isAssignableFrom(normalizedType)) {
if (!(value instanceof Number)) {
report.addIssue(packFolder, file, path, "Expected numeric value");
}
return;
}
if (normalizedType == Character.class) {
if (!(value instanceof String text) || text.length() != 1) {
report.addIssue(packFolder, file, path, "Expected single-character string value");
}
return;
}
Snippet snippet = normalizedType.getDeclaredAnnotation(Snippet.class);
if (snippet != null) {
if (value instanceof String reference) {
if (!reference.startsWith("snippet/")) {
report.addIssue(packFolder, file, path, "Snippet reference must start with snippet/");
}
return;
}
if (value instanceof JSONObject jsonObject) {
validateObject(packFolder, file, path, jsonObject, normalizedType, report);
return;
}
report.addIssue(packFolder, file, path, "Snippet value must be an object or snippet reference string");
return;
}
if (value instanceof JSONObject jsonObject) {
if (shouldValidateNestedObject(normalizedType)) {
validateObject(packFolder, file, path, jsonObject, normalizedType, report);
}
return;
}
if (value instanceof JSONArray) {
report.addIssue(packFolder, file, path, "Unexpected array value for type " + normalizedType.getSimpleName());
return;
}
if (shouldValidateNestedObject(normalizedType)) {
report.addIssue(packFolder, file, path, "Expected object value for type " + normalizedType.getSimpleName());
}
}
private void validateCollection(File packFolder, File file, String path, Object value, Type genericType, Field sourceField, PackValidationReport report) {
if (!(value instanceof JSONArray array)) {
report.addIssue(packFolder, file, path, "Expected array value");
return;
}
Class<?> elementType = resolveCollectionElementType(genericType, sourceField);
if (elementType == null || elementType == Object.class) {
return;
}
for (int i = 0; i < array.length(); i++) {
Object element = array.get(i);
validateValue(packFolder, file, path + "[" + i + "]", element, elementType, null, null, report);
}
}
private void validateArray(File packFolder, File file, String path, Object value, Class<?> expectedType, PackValidationReport report) {
if (!(value instanceof JSONArray array)) {
report.addIssue(packFolder, file, path, "Expected array value");
return;
}
Class<?> componentType = normalizeType(expectedType.getComponentType());
for (int i = 0; i < array.length(); i++) {
Object element = array.get(i);
validateValue(packFolder, file, path + "[" + i + "]", element, componentType, null, null, report);
}
}
private void validateMap(File packFolder, File file, String path, Object value, Type genericType, PackValidationReport report) {
if (!(value instanceof JSONObject object)) {
report.addIssue(packFolder, file, path, "Expected object value");
return;
}
Class<?> valueType = resolveMapValueType(genericType);
if (valueType == null || valueType == Object.class) {
return;
}
for (String key : object.keySet()) {
Object child = object.get(key);
validateValue(packFolder, file, path + "." + key, child, valueType, null, null, report);
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void validateEnum(File packFolder, File file, String path, Object value, Class<?> expectedType, PackValidationReport report) {
if (!(value instanceof String text)) {
report.addIssue(packFolder, file, path, "Expected enum string for " + expectedType.getSimpleName());
return;
}
try {
Enum.valueOf((Class<? extends Enum>) expectedType, text);
} catch (Throwable e) {
report.addIssue(packFolder, file, path, "Unknown enum value \"" + text + "\" for " + expectedType.getSimpleName());
}
}
@SuppressWarnings("unchecked")
private void validateKeyed(File packFolder, File file, String path, Object value, Class<?> expectedType, PackValidationReport report) {
if (!(value instanceof String text)) {
report.addIssue(packFolder, file, path, "Expected namespaced key string for " + expectedType.getSimpleName());
return;
}
NamespacedKey key = NamespacedKey.fromString(text);
if (key == null) {
report.addIssue(packFolder, file, path, "Invalid namespaced key format \"" + text + "\"");
return;
}
KeyedRegistry<Object> registry;
try {
registry = RegistryUtil.lookup((Class<Object>) expectedType);
} catch (Throwable e) {
report.addIssue(packFolder, file, path, "Unable to resolve keyed registry for " + expectedType.getSimpleName() + ": " + simpleMessage(e));
return;
}
if (registry.isEmpty()) {
return;
}
if (registry.get(key) == null) {
report.addIssue(packFolder, file, path, "Unknown registry key \"" + text + "\" for " + expectedType.getSimpleName());
}
}
private Map<String, Field> getSerializableFields(Class<?> type) {
return fieldCache.computeIfAbsent(type, this::buildSerializableFields);
}
private Map<String, Field> buildSerializableFields(Class<?> type) {
Map<String, Field> fields = new LinkedHashMap<>();
Class<?> cursor = type;
while (cursor != null && cursor != Object.class) {
Field[] declared = cursor.getDeclaredFields();
for (Field field : declared) {
int modifiers = field.getModifiers();
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) || field.isSynthetic()) {
continue;
}
fields.putIfAbsent(field.getName(), field);
}
cursor = cursor.getSuperclass();
}
return fields;
}
private Class<?> resolveCollectionElementType(Type genericType, Field sourceField) {
if (sourceField != null) {
ArrayType arrayType = sourceField.getDeclaredAnnotation(ArrayType.class);
if (arrayType != null && arrayType.type() != Object.class) {
return arrayType.type();
}
}
if (genericType instanceof ParameterizedType parameterizedType) {
Type[] arguments = parameterizedType.getActualTypeArguments();
if (arguments.length > 0) {
Class<?> resolved = resolveType(arguments[0]);
if (resolved != null) {
return resolved;
}
}
}
return Object.class;
}
private Class<?> resolveMapValueType(Type genericType) {
if (genericType instanceof ParameterizedType parameterizedType) {
Type[] arguments = parameterizedType.getActualTypeArguments();
if (arguments.length > 1) {
Class<?> resolved = resolveType(arguments[1]);
if (resolved != null) {
return resolved;
}
}
}
return Object.class;
}
private Class<?> resolveType(Type type) {
if (type instanceof Class<?> clazz) {
return normalizeType(clazz);
}
if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class<?> clazz) {
return normalizeType(clazz);
}
if (type instanceof WildcardType wildcardType) {
Type[] upperBounds = wildcardType.getUpperBounds();
if (upperBounds.length > 0) {
return resolveType(upperBounds[0]);
}
}
if (type instanceof GenericArrayType genericArrayType) {
Class<?> component = resolveType(genericArrayType.getGenericComponentType());
if (component != null) {
return normalizeType(Array.newInstance(component, 0).getClass());
}
}
return null;
}
private Class<?> normalizeType(Class<?> type) {
if (type == null) {
return Object.class;
}
if (!type.isPrimitive()) {
return type;
}
if (type == int.class) return Integer.class;
if (type == long.class) return Long.class;
if (type == double.class) return Double.class;
if (type == float.class) return Float.class;
if (type == short.class) return Short.class;
if (type == byte.class) return Byte.class;
if (type == boolean.class) return Boolean.class;
if (type == char.class) return Character.class;
return type;
}
private boolean shouldValidateNestedObject(Class<?> type) {
String name = type.getName();
return name.startsWith("art.arcane.iris.");
}
private boolean isKeyedType(Class<?> type) {
return keyedTypeCache.computeIfAbsent(type, this::resolveKeyedType);
}
@SuppressWarnings("unchecked")
private boolean resolveKeyedType(Class<?> type) {
try {
KeyedRegistry<Object> registry = RegistryUtil.lookup((Class<Object>) type);
return !registry.isEmpty();
} catch (Throwable ignored) {
return false;
}
}
private String simpleMessage(Throwable throwable) {
String message = throwable.getMessage();
if (message == null || message.trim().isEmpty()) {
return throwable.getClass().getSimpleName();
}
return throwable.getClass().getSimpleName() + ": " + message;
}
private String relativePath(File packFolder, File file) {
try {
Path packPath = packFolder.toPath();
Path filePath = file.toPath();
return packPath.relativize(filePath).toString().replace(File.separatorChar, '/');
} catch (Throwable ignored) {
return file.getPath();
}
}
private final class PackValidationReport {
private final String packName;
private final int maxIssues;
private final List<String> sampleIssues;
private int filesChecked;
private int errorCount;
private PackValidationReport(String packName, int maxIssues) {
this.packName = packName;
this.maxIssues = maxIssues;
this.sampleIssues = new ArrayList<>();
}
private void addIssue(File packFolder, File file, String path, String message) {
errorCount++;
if (sampleIssues.size() >= maxIssues) {
return;
}
String relative = relativePath(packFolder, file);
sampleIssues.add(relative + " " + path + " -> " + message);
}
}
}

View File

@@ -29,27 +29,36 @@ import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.engine.object.annotations.functions.ComponentFlagFunction;
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.io.IO;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.plugin.VolmitSender;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Material;
import org.bukkit.World.Environment;
import org.bukkit.block.data.BlockData;
import java.io.*;
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.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.plugin.VolmitSender;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World.Environment;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.io.*;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
import java.util.Map;
@Accessors(chain = true)
@AllArgsConstructor
@@ -379,36 +388,170 @@ public class IrisDimension extends IrisRegistrant {
return landBiomeStyle;
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> folders, KSet<String> biomes) {
getAllBiomes(data)
.stream()
.filter(IrisBiome::isCustom)
.map(IrisBiome::getCustomDerivitives)
.flatMap(KList::stream)
.parallel()
.forEach(j -> {
String json = j.generateJson(fixer);
synchronized (biomes) {
if (!biomes.add(j.getId())) {
Iris.verbose("Duplicate Data Pack Biome: " + getLoadKey() + "/" + j.getId());
return;
}
}
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/" + getLoadKey().toLowerCase() + "/worldgen/biome/" + j.getId() + ".json");
Iris.verbose(" Installing Data Pack Biome: " + output.getPath());
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
});
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> folders, KSet<String> biomes) {
KMap<String, String> customBiomeToVanillaBiome = new KMap<>();
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : getAllBiomes(data)) {
if (!irisBiome.isCustom()) {
continue;
}
Biome vanillaDerivative = irisBiome.getVanillaDerivative();
NamespacedKey vanillaDerivativeKey = vanillaDerivative == null ? null : vanillaDerivative.getKey();
String vanillaBiomeKey = vanillaDerivativeKey == null ? null : vanillaDerivativeKey.toString();
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customBiomeId = customBiome.getId();
String customBiomeKey = namespace + ":" + customBiomeId.toLowerCase(Locale.ROOT);
String json = customBiome.generateJson(fixer);
synchronized (biomes) {
if (!biomes.add(customBiomeId)) {
Iris.verbose("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
continue;
}
}
if (vanillaBiomeKey != null) {
customBiomeToVanillaBiome.put(customBiomeKey, vanillaBiomeKey);
}
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/" + namespace + "/worldgen/biome/" + customBiomeId + ".json");
Iris.verbose(" Installing Data Pack Biome: " + output.getPath());
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
}
}
installStructureBiomeTags(folders, customBiomeToVanillaBiome);
}
private void installStructureBiomeTags(KList<File> folders, KMap<String, String> customBiomeToVanillaBiome) {
if (customBiomeToVanillaBiome.isEmpty()) {
return;
}
KMap<String, KList<String>> vanillaTags = INMS.get().getVanillaStructureBiomeTags();
if (vanillaTags == null || vanillaTags.isEmpty()) {
return;
}
KMap<String, KSet<String>> customTagValues = new KMap<>();
for (Map.Entry<String, String> customBiomeEntry : customBiomeToVanillaBiome.entrySet()) {
String customBiomeKey = customBiomeEntry.getKey();
String vanillaBiomeKey = customBiomeEntry.getValue();
if (vanillaBiomeKey == null) {
continue;
}
for (Map.Entry<String, KList<String>> tagEntry : vanillaTags.entrySet()) {
KList<String> values = tagEntry.getValue();
if (values == null || !values.contains(vanillaBiomeKey)) {
continue;
}
customTagValues.computeIfAbsent(tagEntry.getKey(), key -> new KSet<>()).add(customBiomeKey);
}
}
if (customTagValues.isEmpty()) {
return;
}
for (File datapacks : folders) {
for (Map.Entry<String, KSet<String>> tagEntry : customTagValues.entrySet()) {
String tagPath = tagEntry.getKey();
KSet<String> customValues = tagEntry.getValue();
if (customValues == null || customValues.isEmpty()) {
continue;
}
File output = new File(datapacks, "iris/data/minecraft/tags/worldgen/biome/" + tagPath + ".json");
try {
writeMergedStructureBiomeTag(output, customValues);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
}
}
private void writeMergedStructureBiomeTag(File output, KSet<String> customValues) throws IOException {
synchronized (IrisDimension.class) {
KSet<String> mergedValues = readExistingStructureBiomeTagValues(output);
mergedValues.addAll(customValues);
JSONArray values = new JSONArray();
KList<String> sortedValues = new KList<>(mergedValues).sort();
for (String value : sortedValues) {
values.put(value);
}
JSONObject json = new JSONObject();
json.put("replace", false);
json.put("values", values);
writeAtomicFile(output, json.toString(4));
}
}
private KSet<String> readExistingStructureBiomeTagValues(File output) {
KSet<String> values = new KSet<>();
if (output == null || !output.exists()) {
return values;
}
try {
JSONObject json = new JSONObject(IO.readAll(output));
if (!json.has("values")) {
return values;
}
JSONArray existingValues = json.getJSONArray("values");
for (int index = 0; index < existingValues.length(); index++) {
Object rawValue = existingValues.get(index);
if (rawValue == null) {
continue;
}
String value = String.valueOf(rawValue).trim();
if (!value.isEmpty()) {
values.add(value);
}
}
} catch (Throwable e) {
Iris.warn("Skipping malformed existing structure biome tag file: " + output.getPath());
}
return values;
}
private void writeAtomicFile(File output, String contents) throws IOException {
File parent = output.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
File temp = new File(parent, output.getName() + ".tmp-" + System.nanoTime());
IO.writeAll(temp, contents);
Path tempPath = temp.toPath();
Path outputPath = output.toPath();
try {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING);
}
}
public Dimension getBaseDimension() {
return switch (getEnvironment()) {

View File

@@ -508,7 +508,17 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
return 4;
Engine currentEngine = engine;
if (currentEngine == null || !setup.get()) {
currentEngine = getEngine(worldInfo);
}
boolean ignoreFluid = switch (heightMap) {
case OCEAN_FLOOR, OCEAN_FLOOR_WG -> true;
default -> false;
};
return currentEngine.getMinHeight() + currentEngine.getHeight(x, z, ignoreFluid) + 1;
}
private void computeStudioGenerator() {
@@ -541,7 +551,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public boolean shouldGenerateStructures() {
return false;
return IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures();
}
@Override

View File

@@ -25,28 +25,29 @@ import org.bukkit.craftbukkit.v1_21_R7.CraftWorld;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class CustomBiomeSource extends BiomeSource {
private static final int NOISE_BIOME_CACHE_MAX = 262144;
private final long seed;
private final Engine engine;
private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final RNG rng;
private final KMap<String, Holder<Biome>> customBiomes;
private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine;
this.seed = seed;
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
this.rng = new RNG(engine.getSeedManager().getBiome());
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
}
@@ -172,31 +173,85 @@ public class CustomBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
int m = (y - engine.getMinHeight()) << 2;
IrisBiome ib = engine.getComplex().getTrueBiomeStream().get(x << 2, z << 2);
if (ib == null) {
return resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
long cacheKey = packNoiseKey(x, y, z);
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
return cachedHolder;
}
if (ib.isCustom()) {
IrisBiomeCustom custom = ib.getCustomBiome(rng, x << 2, m, z << 2);
if (custom != null) {
Holder<Biome> holder = customBiomes.get(custom.getId());
Holder<Biome> resolvedHolder = resolveNoiseBiomeHolder(x, y, z);
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
return resolvedHolder;
}
private Holder<Biome> resolveNoiseBiomeHolder(int x, int y, int z) {
if (engine == null || engine.isClosed()) {
return getFallbackBiome();
}
if (engine.getComplex() == null) {
return getFallbackBiome();
}
int blockX = x << 2;
int blockZ = z << 2;
int blockY = (y - engine.getMinHeight()) << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
return getFallbackBiome();
}
RNG noiseRng = new RNG(seed
^ (((long) blockX) * 341873128712L)
^ (((long) blockY) * 132897987541L)
^ (((long) blockZ) * 42317861L));
if (irisBiome.isCustom()) {
IrisBiomeCustom customBiome = irisBiome.getCustomBiome(noiseRng, blockX, blockY, blockZ);
if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiome.getId());
if (holder != null) {
return holder;
}
}
return resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
return getFallbackBiome();
}
org.bukkit.block.Biome v = ib.getSkyBiome(rng, x << 2, m, z << 2);
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, v);
org.bukkit.block.Biome vanillaBiome = irisBiome.getSkyBiome(noiseRng, blockX, blockY, blockZ);
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
if (holder != null) {
return holder;
}
return resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
return getFallbackBiome();
}
private Holder<Biome> getFallbackBiome() {
if (fallbackBiome != null) {
return fallbackBiome;
}
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
if (holder != null) {
return holder;
}
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
| ((long) y & 4095L);
}
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {

View File

@@ -30,6 +30,7 @@ import net.minecraft.world.level.levelgen.*;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.bukkit.Bukkit;
@@ -50,6 +51,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private final ChunkGenerator delegate;
private final Engine engine;
private volatile Registry<Structure> cachedStructureRegistry;
private volatile Map<Structure, Integer> cachedStructureOrder;
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null);
@@ -152,20 +155,21 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures())
return;
if (!IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures()) {
return;
}
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long i = random.setDecorationSeed(level.getSeed(), blockPos.getX(), blockPos.getZ());
var structures = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var list = structures.stream()
.sorted(Comparator.comparingInt(s -> s.step().ordinal()))
.toList();
Registry<Structure> structureRegistry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Map<Structure, Integer> structureOrder = getStructureOrder(structureRegistry);
var surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
var ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
var motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
var motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
@@ -174,21 +178,42 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
int noAir = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
int noFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
SET_HEIGHT.invoke(ocean, x, z, Math.min(noFluid, ocean.getFirstAvailable(x, z)));
SET_HEIGHT.invoke(surface, x, z, Math.min(noAir, surface.getFirstAvailable(x, z)));
SET_HEIGHT.invoke(motion, x, z, Math.min(noAir, motion.getFirstAvailable(x, z)));
SET_HEIGHT.invoke(motionNoLeaves, x, z, Math.min(noAir, motionNoLeaves.getFirstAvailable(x, z)));
int oceanHeight = ocean.getFirstAvailable(x, z);
int surfaceHeight = surface.getFirstAvailable(x, z);
int motionHeight = motion.getFirstAvailable(x, z);
int motionNoLeavesHeight = motionNoLeaves.getFirstAvailable(x, z);
if (noFluid > oceanHeight) {
SET_HEIGHT.invoke(ocean, x, z, noFluid);
}
if (noAir > surfaceHeight) {
SET_HEIGHT.invoke(surface, x, z, noAir);
}
if (noAir > motionHeight) {
SET_HEIGHT.invoke(motion, x, z, noAir);
}
if (noAir > motionNoLeavesHeight) {
SET_HEIGHT.invoke(motionNoLeaves, x, z, noAir);
}
}
}
for (int j = 0; j < list.size(); j++) {
Structure structure = list.get(j);
random.setFeatureSeed(i, j, structure.step().ordinal());
Supplier<String> supplier = () -> structures.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString);
List<StructureStart> starts = new ArrayList<>(structureManager.startsForStructure(chunkAccess.getPos(), structure -> true));
starts.sort(Comparator.comparingInt(start -> structureOrder.getOrDefault(start.getStructure(), Integer.MAX_VALUE)));
int seededStructureIndex = Integer.MIN_VALUE;
for (int j = 0; j < starts.size(); j++) {
StructureStart start = starts.get(j);
Structure structure = start.getStructure();
int structureIndex = structureOrder.getOrDefault(structure, j);
if (structureIndex != seededStructureIndex) {
random.setFeatureSeed(i, structureIndex, structure.step().ordinal());
seededStructureIndex = structureIndex;
}
Supplier<String> supplier = () -> structureRegistry.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString);
try {
level.setCurrentlyGenerating(supplier);
structureManager.startsForStructure(sectionPos, structure).forEach((start) -> start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos()));
start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos());
} catch (Exception exception) {
CrashReport crashReport = CrashReport.forThrowable(exception, "Feature placement");
CrashReportCategory category = crashReport.addCategory("Feature");
@@ -210,6 +235,35 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15);
}
private Map<Structure, Integer> getStructureOrder(Registry<Structure> structureRegistry) {
Map<Structure, Integer> localOrder = cachedStructureOrder;
Registry<Structure> localRegistry = cachedStructureRegistry;
if (localRegistry == structureRegistry && localOrder != null) {
return localOrder;
}
synchronized (this) {
Map<Structure, Integer> synchronizedOrder = cachedStructureOrder;
Registry<Structure> synchronizedRegistry = cachedStructureRegistry;
if (synchronizedRegistry == structureRegistry && synchronizedOrder != null) {
return synchronizedOrder;
}
List<Structure> sortedStructures = structureRegistry.stream()
.sorted(Comparator.comparingInt(structure -> structure.step().ordinal()))
.toList();
Map<Structure, Integer> builtOrder = new IdentityHashMap<>(sortedStructures.size());
for (int index = 0; index < sortedStructures.size(); index++) {
Structure structure = sortedStructures.get(index);
builtOrder.put(structure, index);
}
cachedStructureRegistry = structureRegistry;
cachedStructureOrder = builtOrder;
return builtOrder;
}
}
@Override
public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) {
delegate.spawnOriginalMobs(regionlimitedworldaccess);

View File

@@ -727,6 +727,48 @@ public class NMSBinding implements INMSBinding {
return keys;
}
@Override
public KMap<String, KList<String>> getVanillaStructureBiomeTags() {
KMap<String, KList<String>> tags = new KMap<>();
Registry<net.minecraft.world.level.biome.Biome> registry = registry().lookup(Registries.BIOME).orElse(null);
if (registry == null) {
return tags;
}
registry.getTags().forEach(named -> {
TagKey<net.minecraft.world.level.biome.Biome> tagKey = named.key();
Identifier location = tagKey.location();
if (!"minecraft".equals(location.getNamespace())) {
return;
}
String path = location.getPath();
if (!path.startsWith("has_structure/")) {
return;
}
KList<String> values = new KList<>();
named.stream().forEach(holder -> {
net.minecraft.world.level.biome.Biome biome = holder.value();
Identifier biomeLocation = registry.getKey(biome);
if (biomeLocation == null) {
return;
}
if ("minecraft".equals(biomeLocation.getNamespace())) {
values.add(biomeLocation.toString());
}
});
KList<String> uniqueValues = values.removeDuplicates();
if (!uniqueValues.isEmpty()) {
tags.put(path, uniqueValues);
}
});
return tags;
}
@Override
public boolean missingDimensionTypes(String... keys) {
var type = registry().lookupOrThrow(Registries.DIMENSION_TYPE);