From 482fa9b11ecd77d7d260d42bc9cc1ab3be9591d9 Mon Sep 17 00:00:00 2001 From: Julian Krings Date: Mon, 29 Jul 2024 18:29:28 +0200 Subject: [PATCH] add missing methods to 1.21 bindings --- .../iris/core/nms/v1_21_R1/NMSBinding.java | 240 +++++++++++++++++- 1 file changed, 229 insertions(+), 11 deletions(-) diff --git a/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java index 0e19dae15..bd0d1dc03 100644 --- a/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java +++ b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java @@ -1,26 +1,48 @@ package com.volmit.iris.core.nms.v1_21_R1; import java.awt.Color; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; +import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; +import java.util.*; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; +import com.google.common.base.Preconditions; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.datapack.DataVersion; +import com.volmit.iris.engine.object.IrisBiomeCustom; +import com.volmit.iris.engine.object.IrisDimension; +import com.volmit.iris.util.format.C; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; +import net.bytebuddy.matcher.ElementMatchers; +import net.minecraft.core.*; +import net.minecraft.core.Registry; import net.minecraft.core.component.DataComponents; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.RandomSequences; import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.status.WorldGenContext; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.PrimaryLevelData; import org.bukkit.*; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; @@ -34,6 +56,7 @@ import org.bukkit.craftbukkit.v1_21_R1.util.CraftNamespacedKey; import org.bukkit.entity.Dolphin; import org.bukkit.entity.Entity; import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.ChunkGenerator; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -56,10 +79,6 @@ import com.volmit.iris.util.nbt.mca.palette.*; import com.volmit.iris.util.nbt.tag.CompoundTag; import it.unimi.dsi.fastutil.objects.Object2IntMap; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Holder; -import net.minecraft.core.Registry; -import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.TagParser; @@ -581,4 +600,203 @@ public class NMSBinding implements INMSBinding { public DataVersion getDataVersion() { return DataVersion.V1205; } + + @Override + public boolean registerDimension(String name, IrisDimension dimension) { + var registry = registry(Registries.DIMENSION_TYPE); + var baseLocation = switch (dimension.getEnvironment()) { + case NORMAL -> ResourceLocation.fromNamespaceAndPath("minecraft", "overworld"); + case NETHER -> ResourceLocation.fromNamespaceAndPath("minecraft", "the_nether"); + case THE_END -> ResourceLocation.fromNamespaceAndPath("minecraft", "the_end"); + case CUSTOM -> throw new IllegalArgumentException("Cannot register custom dimension"); + }; + var base = registry.getHolder(ResourceKey.create(Registries.DIMENSION_TYPE, baseLocation)).orElse(null); + if (base == null) return false; + var json = encode(DimensionType.CODEC, base).orElse(null); + if (json == null) return false; + var object = json.getAsJsonObject(); + var height = dimension.getDimensionHeight(); + object.addProperty("min_y", height.getMin()); + object.addProperty("height", height.getMax() - height.getMin()); + object.addProperty("logical_height", dimension.getLogicalHeight()); + var value = decode(DimensionType.CODEC, object.toString()).map(Holder::value).orElse(null); + if (value == null) return false; + return register(Registries.DIMENSION_TYPE, ResourceLocation.fromNamespaceAndPath("iris", name), value, true); + } + + @Override + public boolean registerBiome(String dimensionId, IrisBiomeCustom biome, boolean replace) { + var biomeBase = decode(net.minecraft.world.level.biome.Biome.CODEC, biome.generateJson()).map(Holder::value).orElse(null); + if (biomeBase == null) return false; + return register(Registries.BIOME, ResourceLocation.fromNamespaceAndPath(dimensionId, biome.getId()), biomeBase, replace); + } + + private Optional decode(Codec codec, String json) { + return codec.decode(JsonOps.INSTANCE, GsonHelper.parse(json)).result().map(Pair::getFirst); + } + + private Optional encode(Codec codec, T value) { + return codec.encode(value, JsonOps.INSTANCE, new JsonObject()).result(); + } + + private boolean register(ResourceKey> registryKey, ResourceLocation location, T value, boolean replace) { + Preconditions.checkArgument(registryKey != null, "The registry cannot be null!"); + Preconditions.checkArgument(location != null, "The location cannot be null!"); + Preconditions.checkArgument(value != null, "The value cannot be null!"); + var registry = registry(registryKey); + var key = ResourceKey.create(registryKey, location); + try { + if (registry.containsKey(key)) { + if (!replace) return false; + return replace(registryKey, location, value); + } + Field field = getField(MappedRegistry.class, boolean.class); + field.setAccessible(true); + boolean frozen = field.getBoolean(registry); + field.setBoolean(registry, false); + Field valueField = getField(Holder.Reference.class, "T"); + valueField.setAccessible(true); + + try { + var holder = registry.register(key, value, RegistrationInfo.BUILT_IN); + if (frozen) valueField.set(holder, value); + return true; + } finally { + field.setBoolean(registry, frozen); + } + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + @SuppressWarnings("unchecked") + private boolean replace(ResourceKey> registryKey, ResourceLocation location, T value) { + Preconditions.checkArgument(registryKey != null, "The registryKey cannot be null!"); + Preconditions.checkArgument(location != null, "The location cannot be null!"); + Preconditions.checkArgument(value != null, "The value cannot be null!"); + var registry = registry(registryKey); + var key = ResourceKey.create(registryKey, location); + try { + var holder = registry.getHolder(key).orElse(null); + if (holder == null) return false; + var oldValue = holder.value(); + Field valueField = getField(Holder.Reference.class, "T"); + valueField.setAccessible(true); + Field toIdField = getField(MappedRegistry.class, buildType(Reference2IntMap.class, "T")); + toIdField.setAccessible(true); + Field byValueField = getField(MappedRegistry.class, buildType(Map.class, "T", buildType(Holder.Reference.class, "T"))); + byValueField.setAccessible(true); + var toId = (Reference2IntMap) toIdField.get(registry); + var byValue = (Map>) byValueField.get(registry); + + valueField.set(holder, value); + toId.put(value, toId.removeInt(oldValue)); + byValue.put(value, byValue.remove(oldValue)); + return true; + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + private MappedRegistry registry(ResourceKey> registryKey) { + var rawRegistry = registry().registry(registryKey).orElse(null); + if (!(rawRegistry instanceof MappedRegistry registry)) + throw new IllegalStateException("The Registry is not a mapped Registry!"); + return registry; + } + + private static String buildType(Class clazz, String... parameterTypes) { + if (parameterTypes.length == 0) return clazz.getName(); + var builder = new StringBuilder(clazz.getName()) + .append("<"); + for (int i = 0; i < parameterTypes.length; i++) { + builder.append(parameterTypes[i]).append(parameterTypes.length - 1 == i ? ">" : ", "); + } + return builder.toString(); + } + + private static Field getField(Class clazz, String type) throws NoSuchFieldException { + try { + for (Field f : clazz.getDeclaredFields()) { + if (f.getGenericType().getTypeName().equals(type)) + return f; + } + throw new NoSuchFieldException(type); + } catch (NoSuchFieldException e) { + Class superClass = clazz.getSuperclass(); + if (superClass == null) throw e; + return getField(superClass, type); + } + } + + public void injectBukkit() { + try { + Iris.info("Injecting Bukkit"); + new ByteBuddy() + .redefine(CraftServer.class) + .visit(Advice.to(CraftServerAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(WorldCreator.class)))) + .make() + .load(CraftServer.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); + new ByteBuddy() + .redefine(ServerLevel.class) + .visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor().and(ElementMatchers.takesArguments(MinecraftServer.class, Executor.class, LevelStorageSource.LevelStorageAccess.class, + PrimaryLevelData.class, ResourceKey.class, LevelStem.class, ChunkProgressListener.class, boolean.class, long.class, + List.class, boolean.class, RandomSequences.class, World.Environment.class, ChunkGenerator.class, BiomeProvider.class)))) + .make() + .load(ServerLevel.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); + Iris.info("Injected Bukkit Successfully!"); + } catch (Exception e) { + Iris.info(C.RED + "Failed to Inject Bukkit!"); + e.printStackTrace(); + Iris.reportError(e); + } + + } + + private static class ServerLevelAdvice { + @Advice.OnMethodEnter + static void enter(@Advice.Argument(0) MinecraftServer server, @Advice.Argument(2) LevelStorageSource.LevelStorageAccess access, @Advice.Argument(4) ResourceKey key, @Advice.Argument(value = 5, readOnly = false) LevelStem levelStem) { + File iris = new File(access.levelDirectory.path().toFile(), "iris"); + if (!iris.exists() && !key.location().getPath().startsWith("iris/")) return; + ResourceKey typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, ResourceLocation.fromNamespaceAndPath("iris", key.location().getPath())); + RegistryAccess registryAccess = server.registryAccess(); + Registry registry = registryAccess.registry(Registries.DIMENSION_TYPE).orElse(null); + if (registry == null) throw new IllegalStateException("Unable to find registry for dimension type " + typeKey); + Holder holder = registry.getHolder(typeKey).orElse(null); + if (holder == null) throw new IllegalStateException("Unable to find dimension type " + typeKey); + levelStem = new LevelStem(holder, levelStem.generator()); + } + } + + private static class CraftServerAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + static boolean enter(@Advice.This CraftServer self, @Advice.Argument(0) WorldCreator creator) { + File isIrisWorld = new File(self.getWorldContainer(), creator.name() + "/iris"); + boolean isFromIris = false; + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + for (StackTraceElement stack : stackTrace) { + if (stack.getClassName().contains("Iris")) { + isFromIris = true; + break; + } + } + if (isIrisWorld.exists() && !isFromIris) { + var logger = Logger.getLogger("Iris"); + logger.warning("detected another Plugin trying to load " + creator.name() + ". This is not supported and will be ignored."); + + if (System.getProperty("iris.debug", "false").equals("true")) { + new RuntimeException().printStackTrace(); + } + return true; + } + return false; + } + + @Advice.OnMethodExit + static void exit(@Advice.Enter boolean bool, @Advice.Return(readOnly = false) World returned) { + if (bool) { + returned = null; + } + } + } }