From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> Date: Mon, 26 Dec 2022 11:25:35 -0500 Subject: [PATCH] AdvancedSlimePaper Server Changes AdvancedSlimePaper Copyright (C) 2023 InfernalSuite 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 . diff --git a/build.gradle.kts b/build.gradle.kts index fb98936bb8a5488db75d676c5bcb4060597fbbf8..2143180a92ec6d0c0eba5559dd5497291348fdfa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ configurations.named(log4jPlugins.compileClasspathConfigurationName) { val alsoShade: Configuration by configurations.creating dependencies { + implementation(project(":aswm-core")) implementation(project(":paper-api")) implementation(project(":paper-mojangapi")) // Paper start diff --git a/src/main/java/com/infernalsuite/aswm/Converter.java b/src/main/java/com/infernalsuite/aswm/Converter.java new file mode 100644 index 0000000000000000000000000000000000000000..7d1753c0b7e89bbf0c245a0231b62773eca2779e --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/Converter.java @@ -0,0 +1,118 @@ +package com.infernalsuite.aswm; + +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.TagType; +import com.infernalsuite.aswm.api.utils.NibbleArray; +import net.minecraft.nbt.*; +import net.minecraft.world.level.chunk.DataLayer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +public class Converter { + + private static final Logger LOGGER = LogManager.getLogger("SWM Converter"); + + static DataLayer convertArray(NibbleArray array) { + return new DataLayer(array.getBacking()); + } + + public static NibbleArray convertArray(DataLayer array) { + if(array == null) { + return null; + } + + return new NibbleArray(array.getData()); + } + + public static Tag convertTag(com.flowpowered.nbt.Tag tag) { + try { + switch(tag.getType()) { + case TAG_BYTE: + return ByteTag.valueOf(((com.flowpowered.nbt.ByteTag) tag).getValue()); + case TAG_SHORT: + return ShortTag.valueOf(((com.flowpowered.nbt.ShortTag) tag).getValue()); + case TAG_INT: + return IntTag.valueOf(((com.flowpowered.nbt.IntTag) tag).getValue()); + case TAG_LONG: + return LongTag.valueOf(((com.flowpowered.nbt.LongTag) tag).getValue()); + case TAG_FLOAT: + return FloatTag.valueOf(((com.flowpowered.nbt.FloatTag) tag).getValue()); + case TAG_DOUBLE: + return DoubleTag.valueOf(((com.flowpowered.nbt.DoubleTag) tag).getValue()); + case TAG_BYTE_ARRAY: + return new ByteArrayTag(((com.flowpowered.nbt.ByteArrayTag) tag).getValue()); + case TAG_STRING: + return StringTag.valueOf(((com.flowpowered.nbt.StringTag) tag).getValue()); + case TAG_LIST: + ListTag list = new ListTag(); + ((com.flowpowered.nbt.ListTag) tag).getValue().stream().map(Converter::convertTag).forEach(list::add); + + return list; + case TAG_COMPOUND: + CompoundTag compound = new CompoundTag(); + + ((com.flowpowered.nbt.CompoundTag) tag).getValue().forEach((key, value) -> compound.put(key, convertTag(value))); + return compound; + case TAG_INT_ARRAY: + return new IntArrayTag(((com.flowpowered.nbt.IntArrayTag) tag).getValue()); + case TAG_LONG_ARRAY: + return new LongArrayTag(((com.flowpowered.nbt.LongArrayTag) tag).getValue()); + default: + throw new IllegalArgumentException("Invalid tag type " + tag.getType().name()); + } + } catch(Exception ex) { + LOGGER.error("Failed to convert NBT object:"); + LOGGER.error(tag.toString()); + + throw ex; + } + } + + public static com.flowpowered.nbt.Tag convertTag(String name, Tag base) { + switch(base.getId()) { + case Tag.TAG_BYTE: + return new com.flowpowered.nbt.ByteTag(name, ((ByteTag) base).getAsByte()); + case Tag.TAG_SHORT: + return new com.flowpowered.nbt.ShortTag(name, ((ShortTag) base).getAsShort()); + case Tag.TAG_INT: + return new com.flowpowered.nbt.IntTag(name, ((IntTag) base).getAsInt()); + case Tag.TAG_LONG: + return new com.flowpowered.nbt.LongTag(name, ((LongTag) base).getAsLong()); + case Tag.TAG_FLOAT: + return new com.flowpowered.nbt.FloatTag(name, ((FloatTag) base).getAsFloat()); + case Tag.TAG_DOUBLE: + return new com.flowpowered.nbt.DoubleTag(name, ((DoubleTag) base).getAsDouble()); + case Tag.TAG_BYTE_ARRAY: + return new com.flowpowered.nbt.ByteArrayTag(name, ((ByteArrayTag) base).getAsByteArray()); + case Tag.TAG_STRING: + return new com.flowpowered.nbt.StringTag(name, ((StringTag) base).getAsString()); + case Tag.TAG_LIST: + List list = new ArrayList<>(); + ListTag originalList = ((ListTag) base); + + for(Tag entry : originalList) { + list.add(convertTag("", entry)); + } + + return new com.flowpowered.nbt.ListTag(name, TagType.getById(originalList.getElementType()), list); + case Tag.TAG_COMPOUND: + CompoundTag originalCompound = ((CompoundTag) base); + com.flowpowered.nbt.CompoundTag compound = new com.flowpowered.nbt.CompoundTag(name, new CompoundMap()); + + for(String key : originalCompound.getAllKeys()) { + compound.getValue().put(key, convertTag(key, originalCompound.get(key))); + } + + return compound; + case Tag.TAG_INT_ARRAY: + return new com.flowpowered.nbt.IntArrayTag(name, ((IntArrayTag) base).getAsIntArray()); + case Tag.TAG_LONG_ARRAY: + return new com.flowpowered.nbt.LongArrayTag(name, ((LongArrayTag) base).getAsLongArray()); + default: + throw new IllegalArgumentException("Invalid tag type " + base.getId()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/InternalPlugin.java b/src/main/java/com/infernalsuite/aswm/InternalPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..61518ab2b68e7a41500f3c8c8a5ec1230597f0e5 --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/InternalPlugin.java @@ -0,0 +1,28 @@ +package com.infernalsuite.aswm; + +import net.minecraft.server.MinecraftServer; +import org.bukkit.Server; +import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin; +import org.bukkit.plugin.PluginLogger; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.LogRecord; + +public class InternalPlugin extends MinecraftInternalPlugin { + + @Override + public @NotNull Server getServer() { + return MinecraftServer.getServer().server; + } + + @Override + public @NotNull PluginLogger getLogger() { + return new PluginLogger(new InternalPlugin()) { + @Override + public void log(@NotNull LogRecord logRecord) { + MinecraftServer.LOGGER.info(logRecord.getMessage()); + } + }; + } + +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/SimpleDataFixerConverter.java b/src/main/java/com/infernalsuite/aswm/SimpleDataFixerConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..1affe4c94b490a05184deccc9eb80530f67fd5ea --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/SimpleDataFixerConverter.java @@ -0,0 +1,101 @@ +package com.infernalsuite.aswm; + +import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; +import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; +import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; +import com.flowpowered.nbt.CompoundTag; +import com.infernalsuite.aswm.serialization.SlimeWorldReader; +import com.infernalsuite.aswm.skeleton.SkeletonSlimeWorld; +import com.infernalsuite.aswm.skeleton.SlimeChunkSectionSkeleton; +import com.infernalsuite.aswm.skeleton.SlimeChunkSkeleton; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.infernalsuite.aswm.api.world.SlimeChunkSection; +import com.infernalsuite.aswm.api.world.SlimeWorld; +import net.minecraft.SharedConstants; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +class SimpleDataFixerConverter implements SlimeWorldReader { + + @Override + public SlimeWorld readFromData(SlimeWorld data) { + int newVersion = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + int currentVersion = data.getDataVersion(); + // Already fixed + if (currentVersion == newVersion) { + return data; + } + + Map chunks = new HashMap<>(); + for (SlimeChunk chunk : data.getChunkStorage()) { + List entities = new ArrayList<>(); + List blockEntities = new ArrayList<>(); + for (CompoundTag upgradeEntity : chunk.getTileEntities()) { + blockEntities.add( + convertAndBack(upgradeEntity, (tag) -> MCTypeRegistry.TILE_ENTITY.convert(new NBTMapType(tag), currentVersion, newVersion)) + ); + } + for (CompoundTag upgradeEntity : chunk.getEntities()) { + entities.add( + convertAndBack(upgradeEntity, (tag) -> MCTypeRegistry.ENTITY.convert(new NBTMapType(tag), currentVersion, newVersion)) + ); + } + + SlimeChunkSection[] sections = new SlimeChunkSection[chunk.getSections().length]; + for (int i = 0; i < sections.length; i++) { + SlimeChunkSection dataSection = chunk.getSections()[i]; + + com.flowpowered.nbt.CompoundTag blockStateTag = blockStateTag = convertAndBack(dataSection.getBlockStatesTag(), (tag) -> { + WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, new NBTMapType(tag), "palette", currentVersion, newVersion); + }); + + com.flowpowered.nbt.CompoundTag biomeTag = convertAndBack(dataSection.getBiomeTag(), (tag) -> { + WalkerUtils.convertList(MCTypeRegistry.BIOME, new NBTMapType(tag), "palette", currentVersion, newVersion); + }); + + sections[i] = new SlimeChunkSectionSkeleton( + blockStateTag, + biomeTag, + dataSection.getBlockLight(), + dataSection.getSkyLight() + ); + + chunks.put(new ChunkPos(chunk.getX(), chunk.getZ()), new SlimeChunkSkeleton( + chunk.getX(), + chunk.getZ(), + sections, + chunk.getHeightMaps(), + blockEntities, + entities + )); + } + + } + + return new SkeletonSlimeWorld( + data.getName(), + data.getLoader(), + data.isReadOnly(), + chunks, + data.getExtraData(), + data.getPropertyMap(), + newVersion + ); + } + + + private static com.flowpowered.nbt.CompoundTag convertAndBack(com.flowpowered.nbt.CompoundTag value, Consumer acceptor) { + if (value == null) { + return null; + } + + net.minecraft.nbt.CompoundTag converted = (net.minecraft.nbt.CompoundTag) Converter.convertTag(value); + acceptor.accept(converted); + + return (com.flowpowered.nbt.CompoundTag) Converter.convertTag(value.getName(), converted); + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/SlimeNMSBridgeImpl.java b/src/main/java/com/infernalsuite/aswm/SlimeNMSBridgeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..20f16757ba8b8da525ff17d51d4f7eb660c4d22b --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/SlimeNMSBridgeImpl.java @@ -0,0 +1,206 @@ +package com.infernalsuite.aswm; + +import com.infernalsuite.aswm.api.SlimeNMSBridge; +import com.infernalsuite.aswm.api.world.SlimeWorld; +import com.infernalsuite.aswm.api.world.SlimeWorldInstance; +import com.infernalsuite.aswm.api.world.properties.SlimeProperties; +import com.infernalsuite.aswm.level.SlimeBootstrap; +import com.infernalsuite.aswm.level.SlimeInMemoryWorld; +import com.infernalsuite.aswm.level.SlimeLevelInstance; +import com.mojang.serialization.Lifecycle; +import net.kyori.adventure.util.Services; +import net.minecraft.SharedConstants; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.dedicated.DedicatedServerProperties; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelSettings; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.WorldOptions; +import net.minecraft.world.level.storage.CommandStorage; +import net.minecraft.world.level.storage.DimensionDataStorage; +import net.minecraft.world.level.storage.PrimaryLevelData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Locale; + +public class SlimeNMSBridgeImpl implements SlimeNMSBridge { + + private static final SimpleDataFixerConverter DATA_FIXER_CONVERTER = new SimpleDataFixerConverter(); + + private static final Logger LOGGER = LogManager.getLogger("SWM"); + + private SlimeWorld defaultWorld; + private SlimeWorld defaultNetherWorld; + private SlimeWorld defaultEndWorld; + + public static SlimeNMSBridgeImpl instance() { + return (SlimeNMSBridgeImpl) SlimeNMSBridge.instance(); + } + + @Override + public boolean loadOverworldOverride() { + if (defaultWorld == null) { + return false; + } + + // See MinecraftServer loading logic + // Some stuff is needed when loading overworld world + SlimeLevelInstance instance = ((SlimeInMemoryWorld) this.loadInstance(defaultWorld, Level.OVERWORLD)).getInstance(); + DimensionDataStorage worldpersistentdata = instance.getDataStorage(); + instance.getCraftServer().scoreboardManager = new org.bukkit.craftbukkit.scoreboard.CraftScoreboardManager(instance.getServer(), instance.getScoreboard()); + instance.getServer().commandStorage = new CommandStorage(worldpersistentdata); + + return true; + } + + @Override + public boolean loadNetherOverride() { + if (defaultNetherWorld == null) { + return false; + } + + this.loadInstance(defaultNetherWorld, Level.NETHER); + + return true; + } + + @Override + public boolean loadEndOverride() { + if (defaultEndWorld == null) { + return false; + } + + this.loadInstance(defaultEndWorld, Level.END); + + return true; + } + + @Override + public void setDefaultWorlds(SlimeWorld normalWorld, SlimeWorld netherWorld, SlimeWorld endWorld) { + if (normalWorld != null) { + normalWorld.getPropertyMap().setValue(SlimeProperties.ENVIRONMENT, World.Environment.NORMAL.toString().toLowerCase()); + defaultWorld = normalWorld; + } + + if (netherWorld != null) { + netherWorld.getPropertyMap().setValue(SlimeProperties.ENVIRONMENT, World.Environment.NETHER.toString().toLowerCase()); + defaultNetherWorld = netherWorld; + } + + if (endWorld != null) { + endWorld.getPropertyMap().setValue(SlimeProperties.ENVIRONMENT, World.Environment.THE_END.toString().toLowerCase()); + defaultEndWorld = endWorld; + } + + } + + @Override + public SlimeWorldInstance loadInstance(SlimeWorld slimeWorld) { + return this.loadInstance(slimeWorld, null); + } + + public SlimeWorldInstance loadInstance(SlimeWorld slimeWorld, @Nullable ResourceKey dimensionOverride) { + String worldName = slimeWorld.getName(); + + if (Bukkit.getWorld(worldName) != null) { + throw new IllegalArgumentException("World " + worldName + " already exists! Maybe it's an outdated SlimeWorld object?"); + } + + SlimeLevelInstance server = createCustomWorld(slimeWorld, dimensionOverride); + registerWorld(server); + return server.getSlimeInstance(); + } + + @Override + public SlimeWorldInstance getInstance(World world) { + CraftWorld craftWorld = (CraftWorld) world; + + if (!(craftWorld.getHandle() instanceof SlimeLevelInstance worldServer)) { + return null; + } + + return worldServer.getSlimeInstance(); + } + + @Override + public SlimeWorld applyDataFixers(SlimeWorld world) { + return DATA_FIXER_CONVERTER.readFromData(world); + } + + + @Override + public int getCurrentVersion() { + return SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + } + + public void registerWorld(SlimeLevelInstance server) { + MinecraftServer mcServer = MinecraftServer.getServer(); + mcServer.initWorld(server, server.serverLevelData, mcServer.getWorldData(), server.serverLevelData.worldGenOptions()); + + mcServer.addLevel(server); + } + + private SlimeLevelInstance createCustomWorld(SlimeWorld world, @Nullable ResourceKey dimensionOverride) { + SlimeBootstrap bootstrap = new SlimeBootstrap(world); + String worldName = world.getName(); + + PrimaryLevelData worldDataServer = createWorldData(world); + World.Environment environment = getEnvironment(world); + ResourceKey dimension = switch (environment) { + case NORMAL -> LevelStem.OVERWORLD; + case NETHER -> LevelStem.NETHER; + case THE_END -> LevelStem.END; + default -> throw new IllegalArgumentException("Unknown dimension supplied"); + }; + + ResourceKey worldKey = dimensionOverride == null ? ResourceKey.create(Registries.DIMENSION, new ResourceLocation(worldName.toLowerCase(Locale.ENGLISH))) : dimensionOverride; + LevelStem stem = MinecraftServer.getServer().registries().compositeAccess().registryOrThrow(Registries.LEVEL_STEM).get(dimension); + + SlimeLevelInstance level; + + try { + level = new SlimeLevelInstance(bootstrap, worldDataServer, worldKey, dimension, stem, environment); + } catch (IOException ex) { + throw new RuntimeException(ex); // TODO do something better with this? + } + + // level.setReady(true); + level.setSpawnSettings(world.getPropertyMap().getValue(SlimeProperties.ALLOW_MONSTERS), world.getPropertyMap().getValue(SlimeProperties.ALLOW_ANIMALS)); + + return level; + } + + private World.Environment getEnvironment(SlimeWorld world) { + return World.Environment.valueOf(world.getPropertyMap().getValue(SlimeProperties.ENVIRONMENT).toUpperCase()); + } + + private PrimaryLevelData createWorldData(SlimeWorld world) { + MinecraftServer mcServer = MinecraftServer.getServer(); + DedicatedServerProperties serverProps = ((DedicatedServer) mcServer).getProperties(); + String worldName = world.getName(); + + LevelSettings worldsettings = new LevelSettings(worldName, serverProps.gamemode, false, serverProps.difficulty, + true, new GameRules(), mcServer.worldLoader.dataConfiguration()); + + WorldOptions worldoptions = new WorldOptions(0, false, false); + + PrimaryLevelData data = new PrimaryLevelData(worldsettings, worldoptions, PrimaryLevelData.SpecialWorldProperty.FLAT, Lifecycle.stable()); + data.checkName(worldName); + data.setModdedInfo(mcServer.getServerModName(), mcServer.getModdedStatus().shouldReportAsModified()); + data.setInitialized(true); + + return data; + } + +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/ChunkDataLoadTask.java b/src/main/java/com/infernalsuite/aswm/level/ChunkDataLoadTask.java new file mode 100644 index 0000000000000000000000000000000000000000..41e652b568598926e838e81fdc338e51f8e97ef8 --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/ChunkDataLoadTask.java @@ -0,0 +1,121 @@ +package com.infernalsuite.aswm.level; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.mojang.logging.LogUtils; +import io.papermc.paper.chunk.system.poi.PoiChunk; +import io.papermc.paper.chunk.system.scheduling.ChunkLoadTask; +import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler; +import io.papermc.paper.chunk.system.scheduling.GenericDataLoadTask; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.ticks.LevelChunkTicks; +import org.slf4j.Logger; + +import java.util.function.Consumer; + +public final class ChunkDataLoadTask implements CommonLoadTask { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + private final ChunkTaskScheduler scheduler; + private final ServerLevel world; + private final int chunkX; + private final int chunkZ; + private Consumer> onRun; + + private PrioritisedExecutor.PrioritisedTask task; + + private final ChunkLoadTask chunkLoadTask; + + protected ChunkDataLoadTask(ChunkLoadTask chunkLoadTask, final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, + final int chunkZ, final PrioritisedExecutor.Priority priority, final Consumer> onRun) { + this.chunkLoadTask = chunkLoadTask; + this.scheduler = scheduler; + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.onRun = onRun; + + this.task = this.scheduler.createChunkTask(this.chunkX, this.chunkZ, () -> { + try { + SlimeChunk chunk = ((SlimeLevelInstance) this.world).slimeInstance.getChunk(this.chunkX, this.chunkZ); + this.onRun.accept(new GenericDataLoadTask.TaskResult<>(runOnMain(chunk), null)); + } catch (Throwable e) { + LOGGER.error("ERROR", e); + this.onRun.accept(new GenericDataLoadTask.TaskResult<>(null, e)); + } + }, priority); + } + + private ChunkAccess getEmptyChunk() { + LevelChunkTicks blockLevelChunkTicks = new LevelChunkTicks<>(); + LevelChunkTicks fluidLevelChunkTicks = new LevelChunkTicks<>(); + + return new ImposterProtoChunk(new LevelChunk(this.world, new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, blockLevelChunkTicks, fluidLevelChunkTicks, + 0L, null, null, null), true); + } + + protected ChunkAccess runOnMain(final SlimeChunk data) { + final PoiChunk poiChunk = this.chunkLoadTask.chunkHolder.getPoiChunk(); + if (poiChunk == null) { + LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); + } else { + poiChunk.load(); + } + + // have tasks to run (at this point, it's just the POI consistency checking) + try { + // if (data.tasks != null) { + // for (int i = 0, len = data.tasks.size(); i < len; i) { + // data.tasks.poll().run(); + // } + // } + + LevelChunk chunk = this.world.slimeInstance.promote(chunkX, chunkZ, data); + + return new ImposterProtoChunk(chunk, false); + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable thr2) { + LOGGER.error("Failed to parse main tasks for task " + this.toString() + ", chunk data will be lost", thr2); + return this.getEmptyChunk(); + } + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.task.getPriority(); + } + + @Override + public void setPriority(PrioritisedExecutor.Priority priority) { + this.task.setPriority(priority); + } + + @Override + public void raisePriority(PrioritisedExecutor.Priority priority) { + this.task.raisePriority(priority); + } + + @Override + public void lowerPriority(PrioritisedExecutor.Priority priority) { + this.task.lowerPriority(priority); + } + + @Override + public boolean cancel() { + return this.task.cancel(); + } + + public boolean schedule(boolean schedule) { + this.scheduler.scheduleChunkTask(chunkX, chunkZ, this.task::execute); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/CommonLoadTask.java b/src/main/java/com/infernalsuite/aswm/level/CommonLoadTask.java new file mode 100644 index 0000000000000000000000000000000000000000..fc6e46972bcc77134ed718c8c157ec3893d4bcdf --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/CommonLoadTask.java @@ -0,0 +1,18 @@ +package com.infernalsuite.aswm.level; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; + +public interface CommonLoadTask { + + boolean schedule(boolean schedule); + + PrioritisedExecutor.Priority getPriority(); + + boolean cancel(); + + void lowerPriority(PrioritisedExecutor.Priority priority); + + void raisePriority(PrioritisedExecutor.Priority priority); + + void setPriority(PrioritisedExecutor.Priority priority); +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/FastChunkPruner.java b/src/main/java/com/infernalsuite/aswm/level/FastChunkPruner.java new file mode 100644 index 0000000000000000000000000000000000000000..c0e47f25e9be33da374dc737c96d8d3c2bb1cd0f --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/FastChunkPruner.java @@ -0,0 +1,59 @@ +package com.infernalsuite.aswm.level; + +import com.infernalsuite.aswm.api.world.SlimeWorld; +import com.infernalsuite.aswm.api.world.properties.SlimeProperties; +import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap; +import io.papermc.paper.world.ChunkEntitySlices; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; + +public class FastChunkPruner { + + public static boolean canBePruned(SlimeWorld world, LevelChunk chunk) { + // Kenox + // It's not safe to assume that the chunk can be pruned + // if there isn't a loaded chunk there + if (chunk == null || chunk.getChunkHolder() == null) { + return false; + } + + SlimePropertyMap propertyMap = world.getPropertyMap(); + if (propertyMap.getValue(SlimeProperties.SHOULD_LIMIT_SAVE)) { + int minX = propertyMap.getValue(SlimeProperties.SAVE_MIN_X); + int maxX = propertyMap.getValue(SlimeProperties.SAVE_MAX_X); + + int minZ = propertyMap.getValue(SlimeProperties.SAVE_MIN_Z); + int maxZ = propertyMap.getValue(SlimeProperties.SAVE_MAX_Z); + + int chunkX = chunk.locX; + int chunkZ = chunk.locZ; + + if (chunkX < minX || chunkX > maxX) { + return true; + } + + if (chunkZ < minZ || chunkZ > maxZ) { + return true; + } + } + + String pruningSetting = world.getPropertyMap().getValue(SlimeProperties.CHUNK_PRUNING); + if (pruningSetting.equals("aggressive")) { + ChunkEntitySlices slices = chunk.getChunkHolder().getEntityChunk(); + + return chunk.blockEntities.isEmpty() && (slices == null || slices.isEmpty()) && areSectionsEmpty(chunk); + } + + return false; + } + + private static boolean areSectionsEmpty(LevelChunk chunk) { + for (LevelChunkSection section : chunk.getSections()) { + if (!section.hasOnlyAir()) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/NMSSlimeChunk.java b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeChunk.java new file mode 100644 index 0000000000000000000000000000000000000000..f1db2fe121bb3aabfad727a8133b645524b8f19a --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeChunk.java @@ -0,0 +1,203 @@ +package com.infernalsuite.aswm.level; + +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.LongArrayTag; +import com.google.common.collect.Lists; +import com.infernalsuite.aswm.Converter; +import com.infernalsuite.aswm.skeleton.SlimeChunkSectionSkeleton; +import com.infernalsuite.aswm.api.utils.NibbleArray; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.infernalsuite.aswm.api.world.SlimeChunkSection; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import io.papermc.paper.world.ChunkEntitySlices; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerRO; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class NMSSlimeChunk implements SlimeChunk { + private static final Logger LOGGER = LogUtils.getClassLogger(); + + private static final CompoundTag EMPTY_BLOCK_STATE_PALETTE; + private static final CompoundTag EMPTY_BIOME_PALETTE; + + // Optimized empty section serialization + static { + { + PalettedContainer empty = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null); + Tag tag = ChunkSerializer.BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, empty).getOrThrow(false, (error) -> { + throw new AssertionError(error); + }); + + EMPTY_BLOCK_STATE_PALETTE = (CompoundTag) Converter.convertTag("", tag); + } + { + Registry biomes = net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME); + PalettedContainer> empty = new PalettedContainer<>(biomes.asHolderIdMap(), biomes.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); + Tag tag = ChunkSerializer.makeBiomeCodec(biomes).encodeStart(NbtOps.INSTANCE, empty).getOrThrow(false, (error) -> { + throw new AssertionError(error); + }); + + EMPTY_BIOME_PALETTE = (CompoundTag) Converter.convertTag("", tag); + } + } + + private LevelChunk chunk; + + public NMSSlimeChunk(LevelChunk chunk) { + this.chunk = chunk; + } + + @Override + public int getX() { + return chunk.getPos().x; + } + + @Override + public int getZ() { + return chunk.getPos().z; + } + + @Override + public SlimeChunkSection[] getSections() { + SlimeChunkSection[] sections = new SlimeChunkSection[this.chunk.getSectionsCount()]; + LevelLightEngine lightEngine = chunk.getLevel().getChunkSource().getLightEngine(); + + Registry biomeRegistry = chunk.getLevel().registryAccess().registryOrThrow(Registries.BIOME); + + // Ignore deprecation, spigot only method + Codec>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS)); + + for (int sectionId = 0; sectionId < chunk.getSections().length; sectionId++) { + LevelChunkSection section = chunk.getSections()[sectionId]; + // Sections CANNOT be null in 1.18 + + // Block Light Nibble Array + NibbleArray blockLightArray = Converter.convertArray(lightEngine.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunk.getPos(), sectionId))); + + // Sky light Nibble Array + NibbleArray skyLightArray = Converter.convertArray(lightEngine.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunk.getPos(), sectionId))); + + // Tile/Entity Data + + // Block Data + CompoundTag blockStateTag; + if (section.hasOnlyAir()) { + blockStateTag = EMPTY_BLOCK_STATE_PALETTE; + } else { + Tag data = ChunkSerializer.BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, System.err::println); // todo error handling + blockStateTag = (CompoundTag) Converter.convertTag("", data); + } + + + CompoundTag biomeTag; + PalettedContainer> biomes = (PalettedContainer>) section.getBiomes(); + if (biomes.data.palette().getSize() == 1 && biomes.data.palette().maybeHas((h) -> h.is(Biomes.PLAINS))) { + biomeTag = EMPTY_BIOME_PALETTE; + } else { + Tag biomeData = codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, System.err::println); // todo error handling + biomeTag = (CompoundTag) Converter.convertTag("", biomeData); + } + + sections[sectionId] = new SlimeChunkSectionSkeleton(blockStateTag, biomeTag, blockLightArray, skyLightArray); + } + + return sections; + } + + @Override + public CompoundTag getHeightMaps() { + // HeightMap + CompoundMap heightMaps = new CompoundMap(); + + for (Map.Entry entry : chunk.heightmaps.entrySet()) { + if (!entry.getKey().keepAfterWorldgen()) { + continue; + } + + Heightmap.Types type = entry.getKey(); + Heightmap map = entry.getValue(); + + heightMaps.put(type.name(), new LongArrayTag(type.name(), map.getRawData())); + } + + return new CompoundTag("", heightMaps); + } + + @Override + public List getTileEntities() { + List tileEntities = new ArrayList<>(); + + for (BlockEntity entity : chunk.blockEntities.values()) { + net.minecraft.nbt.CompoundTag entityNbt = entity.saveWithFullMetadata(); + tileEntities.add(entityNbt); + } + + return Lists.transform(tileEntities, (compound) -> { + return (CompoundTag) Converter.convertTag("", compound); + }); + } + + @Override + public List getEntities() { + List entities = new ArrayList<>(); + + if(this.chunk == null || this.chunk.getChunkHolder() == null) { + return new ArrayList<>(); + } + + ChunkEntitySlices slices = this.chunk.getChunkHolder().getEntityChunk(); + if (slices == null) { + return new ArrayList<>(); + } + + // Work by + for (Entity entity : slices.entities) { + net.minecraft.nbt.CompoundTag entityNbt = new net.minecraft.nbt.CompoundTag(); + try { + if (entity.save(entityNbt)) { + entities.add(entityNbt); + } + } catch (Exception e) { + LOGGER.error("Could not save the entity = {}, exception = {}", entity, e); + } + } + + return Lists.transform(entities, (compound) -> { + return (CompoundTag) Converter.convertTag("", compound); + }); + } + + public LevelChunk getChunk() { + return chunk; + } + + public void setChunk(LevelChunk chunk) { + this.chunk = chunk; + } + +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/NMSSlimeWorld.java b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeWorld.java new file mode 100644 index 0000000000000000000000000000000000000000..9a27369c00345bbb94aa19f77687269dc94c0b0a --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeWorld.java @@ -0,0 +1,91 @@ +package com.infernalsuite.aswm.level; + +import com.flowpowered.nbt.CompoundTag; +import com.infernalsuite.aswm.api.exceptions.WorldAlreadyExistsException; +import com.infernalsuite.aswm.api.loaders.SlimeLoader; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.infernalsuite.aswm.api.world.SlimeWorld; +import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap; +import net.minecraft.SharedConstants; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.world.level.chunk.LevelChunk; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class NMSSlimeWorld implements SlimeWorld { + + private final SlimeInMemoryWorld memoryWorld; + private final SlimeLevelInstance instance; + + public NMSSlimeWorld(SlimeInMemoryWorld memoryWorld) { + this.instance = memoryWorld.getInstance(); + this.memoryWorld = memoryWorld; + } + + @Override + public String getName() { + return this.instance.getMinecraftWorld().serverLevelData.getLevelName(); + } + + @Override + public SlimeLoader getLoader() { + return this.instance.slimeInstance.getSaveStrategy(); + } + + @Override + public SlimeChunk getChunk(int x, int z) { + LevelChunk chunk = this.instance.getChunkIfLoaded(x, z); + if (chunk == null) { + return null; + } + + return new NMSSlimeChunk(chunk); + } + + @Override + public Collection getChunkStorage() { + List chunks = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.instance); // Paper + return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull) + .map(NMSSlimeChunk::new) + .collect(Collectors.toList()); + } + + @Override + public CompoundTag getExtraData() { + return this.instance.slimeInstance.getExtraData(); + } + + @Override + public Collection getWorldMaps() { + return List.of(); + } + + @Override + public SlimePropertyMap getPropertyMap() { + return this.instance.slimeInstance.getPropertyMap(); + } + + @Override + public boolean isReadOnly() { + return this.getLoader() == null; + } + + @Override + public SlimeWorld clone(String worldName) { + return this.memoryWorld.clone(worldName); + } + + @Override + public SlimeWorld clone(String worldName, SlimeLoader loader) throws WorldAlreadyExistsException, IOException { + return this.memoryWorld.clone(worldName, loader); + } + + @Override + public int getDataVersion() { + return SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/SafeNmsChunkWrapper.java b/src/main/java/com/infernalsuite/aswm/level/SafeNmsChunkWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b20a037679182e3c4a8bf31f084078f6d7e4ff46 --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/SafeNmsChunkWrapper.java @@ -0,0 +1,83 @@ +package com.infernalsuite.aswm.level; + +import com.flowpowered.nbt.CompoundTag; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.infernalsuite.aswm.api.world.SlimeChunkSection; + +import java.util.List; + +public class SafeNmsChunkWrapper implements SlimeChunk { + + private final NMSSlimeChunk wrapper; + private final SlimeChunk safety; + + public SafeNmsChunkWrapper(NMSSlimeChunk wrapper, SlimeChunk safety) { + this.wrapper = wrapper; + this.safety = safety; + } + + @Override + public int getX() { + return this.wrapper.getX(); + } + + @Override + public int getZ() { + return this.wrapper.getZ(); + } + + @Override + public SlimeChunkSection[] getSections() { + if (shouldDefaultBackToSlimeChunk()) { + return this.safety.getSections(); + } + + return this.wrapper.getSections(); + } + + @Override + public CompoundTag getHeightMaps() { + if (shouldDefaultBackToSlimeChunk()) { + return this.safety.getHeightMaps(); + } + + return this.wrapper.getHeightMaps(); + } + + @Override + public List getTileEntities() { + if (shouldDefaultBackToSlimeChunk()) { + return this.safety.getTileEntities(); + } + + return this.wrapper.getTileEntities(); + } + + @Override + public List getEntities() { + if (shouldDefaultBackToSlimeChunk()) { + return this.safety.getEntities(); + } + + return this.wrapper.getEntities(); + } + + /* +Slime chunks can still be requested but not actually loaded, this caused +some things to not properly save because they are not "loaded" into the chunk. +See ChunkMap#protoChunkToFullChunk +anything in the if statement will not be loaded and is stuck inside the runnable. +Inorder to possibly not corrupt the state, simply refer back to the slime saved object. +*/ + public boolean shouldDefaultBackToSlimeChunk() { + return this.safety != null && !this.wrapper.getChunk().loaded; + } + + public NMSSlimeChunk getWrapper() { + return wrapper; + } + + public SlimeChunk getSafety() { + return safety; + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeBootstrap.java b/src/main/java/com/infernalsuite/aswm/level/SlimeBootstrap.java new file mode 100644 index 0000000000000000000000000000000000000000..8853088c5c6306511716bbffac9bf73c633b61bb --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/SlimeBootstrap.java @@ -0,0 +1,8 @@ +package com.infernalsuite.aswm.level; + +import com.infernalsuite.aswm.api.world.SlimeWorld; + +public record SlimeBootstrap( + SlimeWorld initial +) { +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeChunkConverter.java b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..91a7f41db47c7df3ecc301e0827a1d07305f604e --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkConverter.java @@ -0,0 +1,164 @@ +package com.infernalsuite.aswm.level; + +import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.LongArrayTag; +import com.infernalsuite.aswm.Converter; +import com.infernalsuite.aswm.api.utils.NibbleArray; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.infernalsuite.aswm.api.world.SlimeChunkSection; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.NbtOps; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.ticks.LevelChunkTicks; + +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; + +public class SlimeChunkConverter { + + static SlimeChunkLevel deserializeSlimeChunk(SlimeLevelInstance instance, SlimeChunk chunk) { + int x = chunk.getX(); + int z = chunk.getZ(); + + ChunkPos pos = new ChunkPos(x, z); + + // Chunk sections + LevelChunkSection[] sections = new LevelChunkSection[instance.getSectionsCount()]; + + SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(instance); + SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(instance); + instance.getServer().scheduleOnMain(() -> { + instance.getLightEngine().retainData(pos, true); + }); + + Registry biomeRegistry = instance.registryAccess().registryOrThrow(Registries.BIOME); + // Ignore deprecated method + + Codec>> codec = PalettedContainer.codecRW(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS), null); + + for (int sectionId = 0; sectionId < chunk.getSections().length; sectionId++) { + SlimeChunkSection slimeSection = chunk.getSections()[sectionId]; + + if (slimeSection != null) { + NibbleArray blockLight = slimeSection.getBlockLight(); + if (blockLight != null) { + blockNibbles[sectionId] = new SWMRNibbleArray(blockLight.getBacking()); + } + + NibbleArray skyLight = slimeSection.getSkyLight(); + if (skyLight != null) { + skyNibbles[sectionId] = new SWMRNibbleArray(skyLight.getBacking()); + } + + PalettedContainer blockPalette; + if (slimeSection.getBlockStatesTag() != null) { + DataResult> dataresult = ChunkSerializer.BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, Converter.convertTag(slimeSection.getBlockStatesTag())).promotePartial((s) -> { + System.out.println("Recoverable error when parsing section " + x + "," + z + ": " + s); // todo proper logging + }); + blockPalette = dataresult.getOrThrow(false, System.err::println); // todo proper logging + } else { + blockPalette = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null); + } + + PalettedContainer> biomePalette; + + if (slimeSection.getBiomeTag() != null) { + DataResult>> dataresult = codec.parse(NbtOps.INSTANCE, Converter.convertTag(slimeSection.getBiomeTag())).promotePartial((s) -> { + System.out.println("Recoverable error when parsing section " + x + "," + z + ": " + s); // todo proper logging + }); + biomePalette = dataresult.getOrThrow(false, System.err::println); // todo proper logging + } else { + biomePalette = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); + } + + if (sectionId < sections.length) { + LevelChunkSection section = new LevelChunkSection(blockPalette, biomePalette); + sections[sectionId] = section; + } + } + } + + // Keep the chunk loaded at level 33 to avoid light glitches + // Such a high level will let the server not tick the chunk, + // but at the same time it won't be completely unloaded from memory + // getChunkProvider().addTicket(SWM_TICKET, pos, 33, Unit.INSTANCE); + + + LevelChunk.PostLoadProcessor loadEntities = (nmsChunk) -> { + + // TODO + // Load tile entities + List tileEntities = chunk.getTileEntities(); + + if (tileEntities != null) { + for (CompoundTag tag : tileEntities) { + Optional type = tag.getStringValue("id"); + + // Sometimes null tile entities are saved + if (type.isPresent()) { + BlockPos blockPosition = new BlockPos(tag.getIntValue("x").get(), tag.getIntValue("y").get(), tag.getIntValue("z").get()); + BlockState blockData = nmsChunk.getBlockState(blockPosition); + BlockEntity entity = BlockEntity.loadStatic(blockPosition, blockData, (net.minecraft.nbt.CompoundTag) Converter.convertTag(tag)); + + if (entity != null) { + nmsChunk.setBlockEntity(entity); + } + } + } + } + }; + + LevelChunkTicks blockLevelChunkTicks = new LevelChunkTicks<>(); + LevelChunkTicks fluidLevelChunkTicks = new LevelChunkTicks<>(); + SlimeChunkLevel nmsChunk = new SlimeChunkLevel(instance, pos, UpgradeData.EMPTY, blockLevelChunkTicks, fluidLevelChunkTicks, 0L, sections, loadEntities, null); + + // Height Maps + EnumSet heightMapTypes = nmsChunk.getStatus().heightmapsAfter(); + CompoundMap heightMaps = chunk.getHeightMaps().getValue(); + EnumSet unsetHeightMaps = EnumSet.noneOf(Heightmap.Types.class); + + // Light + nmsChunk.setBlockNibbles(blockNibbles); + nmsChunk.setSkyNibbles(skyNibbles); + + for (Heightmap.Types type : heightMapTypes) { + String name = type.getSerializedName(); + + if (heightMaps.containsKey(name)) { + LongArrayTag heightMap = (LongArrayTag) heightMaps.get(name); + nmsChunk.setHeightmap(type, heightMap.getValue()); + } else { + unsetHeightMaps.add(type); + } + } + + // Don't try to populate heightmaps if there are none. + // Does a crazy amount of block lookups + if (!unsetHeightMaps.isEmpty()) { + Heightmap.primeHeightmaps(nmsChunk, unsetHeightMaps); + } + + return nmsChunk; + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeChunkLevel.java b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkLevel.java new file mode 100644 index 0000000000000000000000000000000000000000..b159fc8751e9840b311cc1eda01e496e2dbc5f2e --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkLevel.java @@ -0,0 +1,27 @@ +package com.infernalsuite.aswm.level; + +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.ticks.LevelChunkTicks; +import org.jetbrains.annotations.Nullable; + +public class SlimeChunkLevel extends LevelChunk { + + private final SlimeInMemoryWorld inMemoryWorld; + + public SlimeChunkLevel(SlimeLevelInstance world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks blockTickScheduler, LevelChunkTicks fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) { + super(world, pos, upgradeData, blockTickScheduler, fluidTickScheduler, inhabitedTime, sectionArrayInitializer, entityLoader, blendingData); + this.inMemoryWorld = world.slimeInstance; + } + + @Override + public void unloadCallback() { + super.unloadCallback(); + this.inMemoryWorld.unload(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeInMemoryWorld.java b/src/main/java/com/infernalsuite/aswm/level/SlimeInMemoryWorld.java new file mode 100644 index 0000000000000000000000000000000000000000..fd4cfb9cceb4f23265cb3cce7f1f251051bfba92 --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/SlimeInMemoryWorld.java @@ -0,0 +1,251 @@ +package com.infernalsuite.aswm.level; + +import com.flowpowered.nbt.CompoundTag; +import com.infernalsuite.aswm.ChunkPos; +import com.infernalsuite.aswm.api.exceptions.WorldAlreadyExistsException; +import com.infernalsuite.aswm.api.loaders.SlimeLoader; +import com.infernalsuite.aswm.serialization.slime.SlimeSerializer; +import com.infernalsuite.aswm.skeleton.SkeletonCloning; +import com.infernalsuite.aswm.skeleton.SkeletonSlimeWorld; +import com.infernalsuite.aswm.skeleton.SlimeChunkSkeleton; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.infernalsuite.aswm.api.world.SlimeWorld; +import com.infernalsuite.aswm.api.world.SlimeWorldInstance; +import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.ticks.LevelChunkTicks; +import org.bukkit.World; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/* +The concept of this is a bit flawed, since ideally this should be a 1:1 representation of the MC world. +However, due to the complexity of the chunk system we essentially need to wrap around it. +This stores slime chunks, and when unloaded, will properly convert it to a normal slime chunk for storage. + */ +public class SlimeInMemoryWorld implements SlimeWorld, SlimeWorldInstance { + + private final SlimeLevelInstance instance; + private final SlimeWorld liveWorld; + + private final CompoundTag extra; + private final SlimePropertyMap propertyMap; + private final SlimeLoader loader; + + private final Map chunkStorage = new HashMap<>(); + private boolean readOnly; + // private final Map> entityStorage = new HashMap<>(); + + public SlimeInMemoryWorld(SlimeBootstrap bootstrap, SlimeLevelInstance instance) { + this.instance = instance; + this.extra = bootstrap.initial().getExtraData(); + this.propertyMap = bootstrap.initial().getPropertyMap(); + this.loader = bootstrap.initial().getLoader(); + this.readOnly = bootstrap.initial().isReadOnly(); + + for (SlimeChunk initial : bootstrap.initial().getChunkStorage()) { + ChunkPos pos = new ChunkPos(initial.getX(), initial.getZ()); + List tags = new ArrayList<>(initial.getEntities()); + + // this.entityStorage.put(pos, tags); + this.chunkStorage.put(pos, initial); + } + + this.liveWorld = new NMSSlimeWorld(this); + } + + @Override + public String getName() { + return this.instance.getMinecraftWorld().serverLevelData.getLevelName(); + } + + @Override + public SlimeLoader getLoader() { + return this.loader; + } + + public LevelChunk promote(int x, int z, SlimeChunk chunk) { + SlimeChunkLevel levelChunk; + if (chunk == null) { + net.minecraft.world.level.ChunkPos pos = new net.minecraft.world.level.ChunkPos(x, z); + LevelChunkTicks blockLevelChunkTicks = new LevelChunkTicks<>(); + LevelChunkTicks fluidLevelChunkTicks = new LevelChunkTicks<>(); + + levelChunk = new SlimeChunkLevel(this.instance, pos, UpgradeData.EMPTY, blockLevelChunkTicks, fluidLevelChunkTicks, + 0L, null, null, null); + + chunk = new NMSSlimeChunk(levelChunk); + + } else { + levelChunk = SlimeChunkConverter.deserializeSlimeChunk(this.instance, chunk); + chunk = new SafeNmsChunkWrapper(new NMSSlimeChunk(levelChunk), chunk); + } + this.chunkStorage.put(new ChunkPos(x, z), chunk); + + return levelChunk; + } + + // Authored by: Kenox + // Don't use the NMS live chunk in the chunk map + public void unload(LevelChunk providedChunk) { + final int x = providedChunk.locX; + final int z = providedChunk.locZ; + + SlimeChunk chunk = new NMSSlimeChunk(providedChunk); + + if (FastChunkPruner.canBePruned(this.liveWorld, providedChunk)) { + this.chunkStorage.remove(new ChunkPos(x, z)); + return; + } + + this.chunkStorage.put(new ChunkPos(x, z), + new SlimeChunkSkeleton(chunk.getX(), chunk.getZ(), chunk.getSections(), + chunk.getHeightMaps(), chunk.getTileEntities(), chunk.getEntities())); + } + + @Override + public SlimeChunk getChunk(int x, int z) { + return this.chunkStorage.get(new ChunkPos(x, z)); + } + + @Override + public Collection getChunkStorage() { + return this.chunkStorage.values(); + } + + @Override + public World getBukkitWorld() { + return this.instance.getWorld(); + } + + @Override + public SlimeWorld getSlimeWorldMirror() { + return this.liveWorld; + } + + @Override + public SlimePropertyMap getPropertyMap() { + return this.propertyMap; + } + + @Override + public boolean isReadOnly() { + return this.getSaveStrategy() == null || this.readOnly; + } + + @Override + public SlimeWorld clone(String worldName) { + try { + return clone(worldName, null); + } catch (WorldAlreadyExistsException | IOException ignored) { + return null; // Never going to happen + } + } + + @Override + public SlimeWorld clone(String worldName, SlimeLoader loader) throws WorldAlreadyExistsException, IOException { + if (this.getName().equals(worldName)) { + throw new IllegalArgumentException("The clone world cannot have the same name as the original world!"); + } + + if (worldName == null) { + throw new IllegalArgumentException("The world name cannot be null!"); + } + if (loader != null) { + if (loader.worldExists(worldName)) { + throw new WorldAlreadyExistsException(worldName); + } + } + + SlimeWorld cloned = SkeletonCloning.fullClone(worldName, this, loader); + if (loader != null) { + loader.saveWorld(worldName, SlimeSerializer.serialize(cloned)); + } + + return cloned; + } + + @Override + public int getDataVersion() { + return this.liveWorld.getDataVersion(); + } + + @Override + public SlimeLoader getSaveStrategy() { + return this.loader; + } + + @Override + public CompoundTag getExtraData() { + return this.extra; + } + + @Override + public Collection getWorldMaps() { + return List.of(); + } + + // public Map> getEntityStorage() { + // return entityStorage; + // } + + public SlimeWorld getForSerialization() { + SlimeWorld world = SkeletonCloning.weakCopy(this); + + Map cloned = new HashMap<>(); + for (Map.Entry entry : this.chunkStorage.entrySet()) { + SlimeChunk clonedChunk = entry.getValue(); + // NMS "live" chunks need to be converted + { + LevelChunk chunk = null; + if (clonedChunk instanceof SafeNmsChunkWrapper safeNmsChunkWrapper) { + if (safeNmsChunkWrapper.shouldDefaultBackToSlimeChunk()) { + clonedChunk = safeNmsChunkWrapper.getSafety(); + } else { + chunk = safeNmsChunkWrapper.getWrapper().getChunk(); + } + } else if (clonedChunk instanceof NMSSlimeChunk nmsSlimeChunk) { + chunk = nmsSlimeChunk.getChunk(); + } + + if (chunk != null) { + if (FastChunkPruner.canBePruned(world, chunk)) { + continue; + } + + clonedChunk = new SlimeChunkSkeleton( + clonedChunk.getX(), + clonedChunk.getZ(), + clonedChunk.getSections(), + clonedChunk.getHeightMaps(), + clonedChunk.getTileEntities(), + clonedChunk.getEntities() + ); + } + } + + cloned.put(entry.getKey(), clonedChunk); + } + + return new SkeletonSlimeWorld(world.getName(), + world.getLoader(), + world.isReadOnly(), + cloned, + world.getExtraData(), + world.getPropertyMap(), + world.getDataVersion() + ); + } + + public SlimeLevelInstance getInstance() { + return instance; + } +} diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeLevelGenerator.java b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..4f48b7a1a41aabc78cc9276fbf9f372cb117003f --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelGenerator.java @@ -0,0 +1,39 @@ +package com.infernalsuite.aswm.level; + +import com.mojang.serialization.Codec; +import net.minecraft.core.Holder; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.levelgen.FlatLevelSource; +import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public class SlimeLevelGenerator extends FlatLevelSource { + + public SlimeLevelGenerator(Holder biome) { + super(new FlatLevelGeneratorSettings(Optional.empty(), biome, List.of()), getSource(biome)); + } + + private static BiomeSource getSource(Holder biome) { + return new BiomeSource() { + @Override + protected Codec codec() { + return null; + } + + @Override + protected Stream> collectPossibleBiomes() { + return Stream.of(biome); + } + + @Override + public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler noise) { + return biome; + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeLevelInstance.java b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelInstance.java new file mode 100644 index 0000000000000000000000000000000000000000..2d18d76829b6dc590913d974d50dbaafcb79e175 --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelInstance.java @@ -0,0 +1,195 @@ +package com.infernalsuite.aswm.level; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.infernalsuite.aswm.Converter; +import com.infernalsuite.aswm.serialization.slime.SlimeSerializer; +import com.infernalsuite.aswm.api.world.SlimeChunk; +import com.infernalsuite.aswm.api.world.SlimeWorld; +import com.infernalsuite.aswm.api.world.SlimeWorldInstance; +import com.infernalsuite.aswm.api.world.properties.SlimeProperties; +import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap; +import io.papermc.paper.chunk.system.scheduling.ChunkLoadTask; +import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler; +import io.papermc.paper.chunk.system.scheduling.GenericDataLoadTask; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.ProgressListener; +import net.minecraft.util.Unit; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.validation.DirectoryValidator; +import net.minecraft.world.level.validation.PathAllowList; +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.event.world.WorldSaveEvent; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.stream.Collectors; + +public class SlimeLevelInstance extends ServerLevel { + + + public static LevelStorageSource CUSTOM_LEVEL_STORAGE; + + static { + try { + Path path = Files.createTempDirectory("swm-" + UUID.randomUUID().toString().substring(0, 5)).toAbsolutePath(); + DirectoryValidator directoryvalidator = LevelStorageSource.parseValidator(path.resolve("allowed_symlinks.txt")); + CUSTOM_LEVEL_STORAGE = new LevelStorageSource(path, path, directoryvalidator, DataFixers.getDataFixer()); + + FileUtils.forceDeleteOnExit(path.toFile()); + + } catch (IOException ex) { + throw new IllegalStateException("Couldn't create dummy file directory.", ex); + } + } + + private static final ExecutorService WORLD_SAVER_SERVICE = Executors.newFixedThreadPool(4, new ThreadFactoryBuilder() + .setNameFormat("SWM Pool Thread #%1$d").build()); + private static final TicketType SWM_TICKET = TicketType.create("swm-chunk", (a, b) -> 0); + + private final Object saveLock = new Object(); + + private boolean ready = false; + + public SlimeLevelInstance(SlimeBootstrap slimeBootstrap, PrimaryLevelData primaryLevelData, + ResourceKey worldKey, + ResourceKey dimensionKey, LevelStem worldDimension, + org.bukkit.World.Environment environment) throws IOException { + + super(slimeBootstrap, MinecraftServer.getServer(), MinecraftServer.getServer().executor, + CUSTOM_LEVEL_STORAGE.createAccess(slimeBootstrap.initial().getName() + UUID.randomUUID(), dimensionKey), + primaryLevelData, worldKey, worldDimension, + MinecraftServer.getServer().progressListenerFactory.create(11), false, 0, + Collections.emptyList(), true, environment, null, null); + this.slimeInstance = new SlimeInMemoryWorld(slimeBootstrap, this); + + + SlimePropertyMap propertyMap = slimeBootstrap.initial().getPropertyMap(); + + this.serverLevelData.setDifficulty(Difficulty.valueOf(propertyMap.getValue(SlimeProperties.DIFFICULTY).toUpperCase())); + this.serverLevelData.setSpawn(new BlockPos(propertyMap.getValue(SlimeProperties.SPAWN_X), propertyMap.getValue(SlimeProperties.SPAWN_Y), propertyMap.getValue(SlimeProperties.SPAWN_Z)), 0); + super.setSpawnSettings(propertyMap.getValue(SlimeProperties.ALLOW_MONSTERS), propertyMap.getValue(SlimeProperties.ALLOW_ANIMALS)); + + this.pvpMode = propertyMap.getValue(SlimeProperties.PVP); + + this.keepSpawnInMemory = false; + } + + @Override + public ChunkGenerator getGenerator(SlimeBootstrap slimeBootstrap) { + String biomeStr = slimeBootstrap.initial().getPropertyMap().getValue(SlimeProperties.DEFAULT_BIOME); + ResourceKey biomeKey = ResourceKey.create(Registries.BIOME, new ResourceLocation(biomeStr)); + Holder defaultBiome = MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME).getHolder(biomeKey).orElseThrow(); + return new SlimeLevelGenerator(defaultBiome); + } + + @Override + public void save(@Nullable ProgressListener progressUpdate, boolean forceSave, boolean savingDisabled, boolean close) { + try { + if (!this.slimeInstance.isReadOnly() && !savingDisabled) { + Bukkit.getPluginManager().callEvent(new WorldSaveEvent(getWorld())); + + //this.getChunkSource().save(forceSave); + this.serverLevelData.setWorldBorder(this.getWorldBorder().createSettings()); + this.serverLevelData.setCustomBossEvents(MinecraftServer.getServer().getCustomBossEvents().save()); + + // Update level data + net.minecraft.nbt.CompoundTag compound = new net.minecraft.nbt.CompoundTag(); + net.minecraft.nbt.CompoundTag nbtTagCompound = this.serverLevelData.createTag(MinecraftServer.getServer().registryAccess(), compound); + + if (MinecraftServer.getServer().isStopped()) { // Make sure the world gets saved before stopping the server by running it from the main thread + save().get(); // Async wait for it to finish + this.slimeInstance.getLoader().unlockWorld(this.slimeInstance.getName()); // Unlock + } else { + this.save(); + //WORLD_SAVER_SERVICE.execute(this::save); + } + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + + @Override + public void saveIncrementally(boolean doFull) { + if (doFull) { + this.save(null, false, false); + } + } + + private Future save() { + synchronized (saveLock) { // Don't want to save the SlimeWorld from multiple threads simultaneously + SlimeWorldInstance slimeWorld = this.slimeInstance; + Bukkit.getLogger().log(Level.INFO, "Saving world " + this.slimeInstance.getName() + "..."); + long start = System.currentTimeMillis(); + + Bukkit.getLogger().log(Level.INFO, "CONVERTING NMS -> SKELETON"); + SlimeWorld world = this.slimeInstance.getForSerialization(); + Bukkit.getLogger().log(Level.INFO, "CONVERTED TO SKELETON, PUSHING OFF-THREAD"); + return WORLD_SAVER_SERVICE.submit(() -> { + try { + byte[] serializedWorld = SlimeSerializer.serialize(world); + long saveStart = System.currentTimeMillis(); + slimeWorld.getSaveStrategy().saveWorld(slimeWorld.getName(), serializedWorld); + Bukkit.getLogger().log(Level.INFO, "World " + slimeWorld.getName() + " serialized in " + (saveStart - start) + "ms and saved in " + (System.currentTimeMillis() - saveStart) + "ms."); + } catch (IOException | IllegalStateException ex) { + ex.printStackTrace(); + } + }); + + } + } + + public SlimeWorldInstance getSlimeInstance() { + return this.slimeInstance; + } + + public ChunkDataLoadTask getLoadTask(ChunkLoadTask task, ChunkTaskScheduler scheduler, ServerLevel world, int chunkX, int chunkZ, PrioritisedExecutor.Priority priority, Consumer> onRun) { + return new ChunkDataLoadTask(task, scheduler, world, chunkX, chunkZ, priority, onRun); + } + + public void loadEntities(int chunkX, int chunkZ) { + SlimeChunk slimeChunk = this.slimeInstance.getChunk(chunkX, chunkZ); + if (slimeChunk != null) { + this.getEntityLookup().addLegacyChunkEntities(new ArrayList<>( + EntityType.loadEntitiesRecursive(slimeChunk.getEntities() + .stream() + .map((tag) -> (net.minecraft.nbt.CompoundTag) Converter.convertTag(tag)) + .collect(Collectors.toList()), this) + .toList() + )); + } + } + + // @Override + // public void unload(LevelChunk chunk) { + // this.slimeInstance.unload(chunk); + // super.unload(chunk); + // } +} \ No newline at end of file diff --git a/src/main/java/com/infernalsuite/aswm/util/NmsUtil.java b/src/main/java/com/infernalsuite/aswm/util/NmsUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..3500005bb09dc484bc333f1e0799613d097a37d3 --- /dev/null +++ b/src/main/java/com/infernalsuite/aswm/util/NmsUtil.java @@ -0,0 +1,9 @@ +package com.infernalsuite.aswm.util; + +public class NmsUtil { + + public static long asLong(int chunkX, int chunkZ) { + return (((long) chunkZ) * Integer.MAX_VALUE + ((long) chunkX)); + //return (long)chunkX & 4294967295L | ((long)chunkZ & 4294967295L) << 32; + } +} \ No newline at end of file diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java index abd0217cf0bff183c8e262edc173a53403797c1a..ab450a0ffbfd914c33323d740b3c382a96cb0e8f 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java @@ -165,7 +165,8 @@ public final class ChunkHolderManager { return this.chunkHolders.size(); } - public void close(final boolean save, final boolean halt) { + public void close(boolean save, final boolean halt) { // ASWM + if (this.world instanceof com.infernalsuite.aswm.level.SlimeLevelInstance) save = false; // ASWM TickThread.ensureTickThread("Closing world off-main"); if (halt) { LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'"); diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java index e7fb084ddb88ab62f1d493a999cc82b9258d275e..f366359a66bff0c8e4515383041d59c462936286 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java @@ -6,6 +6,7 @@ import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; +import com.infernalsuite.aswm.level.CommonLoadTask; // ASWM import com.mojang.logging.LogUtils; import io.papermc.paper.chunk.system.io.RegionFileIOThread; import io.papermc.paper.chunk.system.poi.PoiChunk; @@ -32,8 +33,8 @@ public final class ChunkLoadTask extends ChunkProgressionTask { private static final Logger LOGGER = LogUtils.getClassLogger(); - private final NewChunkHolder chunkHolder; - private final ChunkDataLoadTask loadTask; + public final NewChunkHolder chunkHolder; // ASWM + private final CommonLoadTask loadTask; private volatile boolean cancelled; private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; @@ -45,11 +46,20 @@ public final class ChunkLoadTask extends ChunkProgressionTask { final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { super(scheduler, world, chunkX, chunkZ); this.chunkHolder = chunkHolder; - this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); - this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> { - ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement - ChunkLoadTask.this.tryCompleteLoad(); - }); + // ASWM start + if (world instanceof com.infernalsuite.aswm.level.SlimeLevelInstance levelInstance) { + + this.loadTask = levelInstance.getLoadTask(this, scheduler, world, chunkX, chunkZ, priority, result -> { + ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); + }); + } else { + ChunkDataLoadTask task = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); + task.addCallback((final GenericDataLoadTask.TaskResult result) -> { + ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); + }); + this.loadTask = task; + } + // ASWM end } private void tryCompleteLoad() { @@ -274,7 +284,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask { } } - public static final class ChunkDataLoadTask extends CallbackDataLoadTask { + public static final class ChunkDataLoadTask extends CallbackDataLoadTask implements CommonLoadTask { // ASWM protected ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java index 51304c5cf4b0ac7646693ef97ef4a3847d3342b5..376a64344720a31f89c863bcdcdc36666502d66b 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java @@ -112,7 +112,11 @@ public final class NewChunkHolder { } if (!transientChunk) { - if (entityChunk != null) { + // ASWM start + if(this.world instanceof com.infernalsuite.aswm.level.SlimeLevelInstance world) { + world.loadEntities(this.chunkX, this.chunkZ); + }else if(entityChunk != null) { + // ASWM end final List entities = EntityStorage.readEntities(this.world, entityChunk); this.world.getEntityLookup().addEntityChunkEntities(entities); diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java index 7e8dc9e8f381abfdcce2746edc93122d623622d1..12aadf4c981cdb1a6405de99016f4b40e83a04b8 100644 --- a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java +++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java @@ -36,7 +36,7 @@ public final class ChunkEntitySlices { protected final EntityCollectionBySection allEntities; protected final EntityCollectionBySection hardCollidingEntities; protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; - protected final EntityList entities = new EntityList(); + public final EntityList entities = new EntityList(); // ASWM public FullChunkStatus status; diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 435f5ee3388f5da93df938c43ea2578f7d586407..cbc7039007c0ed8aec3284c2f639d5c1b04bd4e1 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -272,7 +272,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); // Paper end - public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { + // ASWM START + public final com.infernalsuite.aswm.level.SlimeBootstrap bootstrap; + public ServerChunkCache(com.infernalsuite.aswm.level.SlimeBootstrap bootstrap, ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { // ASWM + this.bootstrap = bootstrap; + // ASWM end this.level = world; this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world); this.mainThread = Thread.currentThread(); diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 18aac3da3c88f33b1a71a5920a8daa27e9723913..0c517e589b2fa713051fb74f0bf6c4cb59448283 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -643,6 +643,14 @@ public class ServerLevel extends Level implements WorldGenLevel { // Add env and gen to constructor, IWorldDataServer -> WorldDataServer public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { + // ASWM START + this(null, minecraftserver, executor, convertable_conversionsession, iworlddataserver, resourcekey, worlddimension, worldloadlistener, flag, i, list, flag1, env, gen, biomeProvider); + } + + public com.infernalsuite.aswm.level.SlimeInMemoryWorld slimeInstance; + + public ServerLevel(com.infernalsuite.aswm.level.SlimeBootstrap bootstrap, MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { + // ASWM END // IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error // Holder holder = worlddimension.type(); // CraftBukkit - decompile error @@ -681,6 +689,12 @@ public class ServerLevel extends Level implements WorldGenLevel { chunkgenerator = new org.bukkit.craftbukkit.generator.CustomChunkGenerator(this, chunkgenerator, gen); } // CraftBukkit end + // ASWM START + ChunkGenerator result = this.getGenerator(bootstrap); + if (result != null) { + chunkgenerator = result; + } + // ASWM END boolean flag2 = minecraftserver.forceSynchronousWrites(); DataFixer datafixer = minecraftserver.getFixerUpper(); this.entityStorage = new EntityRegionFileStorage(convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system //EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, minecraftserver); @@ -692,7 +706,7 @@ public class ServerLevel extends Level implements WorldGenLevel { //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system - this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system + this.chunkSource = new ServerChunkCache(bootstrap, this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system // ASWM return minecraftserver.overworld().getDataStorage(); }); this.chunkSource.getGeneratorState().ensureStructuresGenerated(); @@ -721,7 +735,7 @@ public class ServerLevel extends Level implements WorldGenLevel { this.sleepStatus = new SleepStatus(); this.gameEventDispatcher = new GameEventDispatcher(this); - this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> { + this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(getRandomSequences(), () -> { // ASWM return (RandomSequences) this.getDataStorage().computeIfAbsent((nbttagcompound) -> { return RandomSequences.load(l, nbttagcompound); }, () -> { @@ -748,6 +762,12 @@ public class ServerLevel extends Level implements WorldGenLevel { this.dragonFight = enderDragonFight; } + // ASWM START + public ChunkGenerator getGenerator(com.infernalsuite.aswm.level.SlimeBootstrap bootstrap) { + return null; + } + // ASWM END + public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) { this.serverLevelData.setClearWeatherTime(clearDuration); this.serverLevelData.setRainTime(rainDuration); diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java index 7f5547dc31aa53b2863f4c09f598fa88e7fe2afd..64cda201058798a663e9f964bb407119a66ac3f5 100644 --- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java @@ -30,7 +30,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer }; public final IdMap registry; private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values - private volatile PalettedContainer.Data data; + public volatile PalettedContainer.Data data; // ASWM private final PalettedContainer.Strategy strategy; // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused @@ -398,7 +398,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer void accept(T object, int count); } - static record Data(PalettedContainer.Configuration configuration, BitStorage storage, Palette palette) { + public static record Data(PalettedContainer.Configuration configuration, BitStorage storage, Palette palette) { // ASWM public void copyFrom(Palette palette, BitStorage storage) { for(int i = 0; i < storage.getSize(); ++i) { T object = palette.valueFor(storage.get(i)); diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java index 9c6a2884c34a9f6e775103da42480cd6b8c693b3..3ef732bd50239c547eddfd0dcbc053a720de32bf 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java @@ -434,7 +434,7 @@ public class ChunkSerializer { ChunkSerializer.LOGGER.error("Recoverable errors when loading section [" + chunkPos.x + ", " + y + ", " + chunkPos.z + "]: " + message); } - private static Codec>> makeBiomeCodec(Registry biomeRegistry) { + public static Codec>> makeBiomeCodec(Registry biomeRegistry) { // ASWM return PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS)); } diff --git a/src/main/resources/META-INF/services/com.infernalsuite.aswm.api.SlimeNMSBridge b/src/main/resources/META-INF/services/com.infernalsuite.aswm.api.SlimeNMSBridge new file mode 100644 index 0000000000000000000000000000000000000000..916b4d2edba2f2a8a0fc1fdb6ab6a57e2a16f938 --- /dev/null +++ b/src/main/resources/META-INF/services/com.infernalsuite.aswm.api.SlimeNMSBridge @@ -0,0 +1 @@ +com.infernalsuite.aswm.SlimeNMSBridgeImpl