From 359e1be5e60aa191ef56a89e4581b2bb4568db15 Mon Sep 17 00:00:00 2001 From: wzp <55318422+Wzp-2008@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:53:02 +0800 Subject: [PATCH] Support Servux Litematics protocol (#448) --------- Co-authored-by: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> Co-authored-by: violetc <58360096+s-yh-china@users.noreply.github.com> --- build.gradle.kts | 1 + gradle.properties | 1 - .../org/leavesmc/leaves/LeavesConfig.java | 20 + .../protocol/servux/ServuxProtocol.java | 3 + .../litematics/LitematicaSchematic.java | 267 +++++++++++ .../servux/litematics/SchematicMetadata.java | 63 +++ .../litematics/ServuxLitematicsProtocol.java | 385 ++++++++++++++++ .../container/LitematicaBitArray.java | 87 ++++ .../LitematicaBlockStateContainer.java | 101 +++++ .../LitematicaBlockStatePalette.java | 24 + .../LitematicaBlockStatePaletteHashMap.java | 93 ++++ .../LitematicaBlockStatePaletteLinear.java | 94 ++++ .../placement/SchematicPlacement.java | 283 ++++++++++++ .../placement/SubRegionPlacement.java | 38 ++ .../servux/litematics/selection/Box.java | 19 + .../servux/litematics/utils/EntityUtils.java | 94 ++++ .../servux/litematics/utils/FileType.java | 11 + .../litematics/utils/Int2ObjectBiMap.java | 194 ++++++++ .../litematics/utils/IntBoundingBox.java | 26 ++ .../servux/litematics/utils/NbtUtils.java | 78 ++++ .../litematics/utils/PositionUtils.java | 132 ++++++ .../litematics/utils/ReplaceBehavior.java | 23 + .../servux/litematics/utils/Schema.java | 148 ++++++ .../utils/SchematicPlacingUtils.java | 426 ++++++++++++++++++ 24 files changed, 2610 insertions(+), 1 deletion(-) create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java create mode 100755 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java diff --git a/build.gradle.kts b/build.gradle.kts index d212bb0e..19c11a2e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ subprojects { options.encoding = Charsets.UTF_8.name() options.release = 21 options.isFork = true + options.forkOptions.memoryMaximumSize = "6g" } tasks.withType { options.encoding = Charsets.UTF_8.name() diff --git a/gradle.properties b/gradle.properties index 8ea477e6..5fbbc59f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,5 @@ version=1.21.4-R0.1-SNAPSHOT mcVersion=1.21.4 paperRef=9b1798d6438107fdf0d5939b79a8cf71f4d16e2c preVersion=false -org.gradle.jvmargs=-Xmx2G org.gradle.caching=true org.gradle.parallel=true diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java index 3567f7c2..a58635c3 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java @@ -7,6 +7,9 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import org.bukkit.command.Command; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; import org.jetbrains.annotations.NotNull; import org.leavesmc.leaves.bot.ServerBot; import org.leavesmc.leaves.command.LeavesCommand; @@ -797,6 +800,23 @@ public final class LeavesConfig { @GlobalConfig("hud-metadata-protocol-share-seed") public boolean hudMetadataShareSeed = true; + + @GlobalConfig(value = "litematics-protocol", validator = LitematicsProtocolValidator.class) + public boolean litematicsProtocol = false; + + public static class LitematicsProtocolValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + PluginManager pluginManager = MinecraftServer.getServer().server.getPluginManager(); + if (value) { + if (pluginManager.getPermission("leaves.protocol.litematics") == null) { + pluginManager.addPermission(new Permission("leaves.protocol.litematics", PermissionDefault.OP)); + } + } else { + pluginManager.removePermission("leaves.protocol.litematics"); + } + } + } } @GlobalConfig("bbor-protocol") diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java index f56ac05a..3838b95c 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java @@ -3,10 +3,13 @@ package org.leavesmc.leaves.protocol.servux; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Contract; import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ServuxProtocol { public static final String PROTOCOL_ID = "servux"; + public static final Logger LOGGER = LoggerFactory.getLogger(PROTOCOL_ID.toUpperCase()); public static final String SERVUX_STRING = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID); @Contract("_ -> new") diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java new file mode 100644 index 00000000..4a799efa --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java @@ -0,0 +1,267 @@ +package org.leavesmc.leaves.protocol.servux.litematics; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.ScheduledTick; +import net.minecraft.world.ticks.TickPriority; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.container.LitematicaBlockStateContainer; +import org.leavesmc.leaves.protocol.servux.litematics.utils.FileType; +import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils; +import org.leavesmc.leaves.protocol.servux.litematics.utils.PositionUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public record LitematicaSchematic(Map subRegions, SchematicMetadata metadata) { + + public static final int MINECRAFT_DATA_VERSION = SharedConstants.getProtocolVersion(); + public static final int SCHEMATIC_VERSION = 7; + + @NotNull + @Unmodifiable + public Map getAreaSizes() { + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(subRegions.size()); + for (Map.Entry entry : subRegions.entrySet()) { + builder.put(entry.getKey(), entry.getValue().size()); + } + return builder.build(); + } + + @NotNull + public SubRegion getSubRegion(String name) { + return Objects.requireNonNull(subRegions.get(name)); + } + + @NotNull + @Contract("_ -> new") + public static LitematicaSchematic readFromNBT(@NotNull CompoundTag nbt) { + if (nbt.contains("Version", Tag.TAG_INT)) { + final int version = nbt.getInt("Version"); + final int minecraftDataVersion = nbt.contains("MinecraftDataVersion") ? nbt.getInt("MinecraftDataVersion") : SharedConstants.getProtocolVersion(); + + if (version >= 1 && version <= SCHEMATIC_VERSION) { + SchematicMetadata metadata = SchematicMetadata.readFromNBT(nbt.getCompound("Metadata"), version, minecraftDataVersion, FileType.LITEMATICA_SCHEMATIC); + Map subRegions = readSubRegionsFromNBT(nbt.getCompound("Regions"), version, minecraftDataVersion); + return new LitematicaSchematic(subRegions, metadata); + } else { + throw new RuntimeException("Unsupported or future schematic version"); + } + } else { + throw new RuntimeException("The schematic doesn't have version information, and can't be safely loaded!"); + } + } + + @NotNull + private static Map readSubRegionsFromNBT(@NotNull CompoundTag tag, int version, int minecraftDataVersion) { + Map subRegions = new HashMap<>(); + for (String regionName : tag.getAllKeys()) { + Tag region = tag.get(regionName); + if (region == null || region.getId() != Tag.TAG_COMPOUND) { + throw new RuntimeException("Unknown region: " + regionName); + } + subRegions.put(regionName, SubRegion.readFromNBT(tag.getCompound(regionName), version, minecraftDataVersion)); + } + return subRegions; + } + + public record SubRegion( + LitematicaBlockStateContainer blockContainers, + Map tileEntities, + Map> pendingBlockTicks, + Map> pendingFluidTicks, + List entities, + BlockPos position, + BlockPos size + ) { + @NotNull + @Contract("_, _, _ -> new") + public static SubRegion readFromNBT(@NotNull CompoundTag regionTag, int version, int minecraftDataVersion) { + BlockPos position = NbtUtils.readBlockPos(regionTag.getCompound("Position")); + BlockPos size = NbtUtils.readBlockPos(regionTag.getCompound("Size")); + if (position == null || size == null) { + throw new IllegalArgumentException("Invalid region"); + } + + Map tileEntities; + List entities; + if (version >= 2) { + tileEntities = readTileEntitiesFromNBT(regionTag.getList("TileEntities", Tag.TAG_COMPOUND)); + entities = readEntitiesFromNBT(regionTag.getList("Entities", Tag.TAG_COMPOUND)); + } else { + tileEntities = readTileEntitiesFromNBT_v1(regionTag.getList("TileEntities", Tag.TAG_COMPOUND)); + entities = readEntitiesFromNBT_v1(regionTag.getList("Entities", Tag.TAG_COMPOUND)); + } + + Map> pendingBlockTicks = null; + if (version >= 3) { + pendingBlockTicks = readPendingTicksFromNBT(regionTag.getList("PendingBlockTicks", Tag.TAG_COMPOUND), BuiltInRegistries.BLOCK, "Block", Blocks.AIR); + } + + Map> pendingFluidTicks = null; + if (version >= 5) { + pendingFluidTicks = readPendingTicksFromNBT(regionTag.getList("PendingFluidTicks", Tag.TAG_COMPOUND), BuiltInRegistries.FLUID, "Fluid", Fluids.EMPTY); + } + + LitematicaBlockStateContainer blockContainers = null; + if (regionTag.contains("BlockStates", Tag.TAG_LONG_ARRAY)) { + ListTag palette = regionTag.getList("BlockStatePalette", Tag.TAG_COMPOUND); + long[] blockStateArr = regionTag.getLongArray("BlockStates"); + BlockPos posEndRel = PositionUtils.getRelativeEndPositionFromAreaSize(size).offset(position); + BlockPos posMin = PositionUtils.getMinCorner(position, posEndRel); + BlockPos posMax = PositionUtils.getMaxCorner(position, posEndRel); + BlockPos containerSize = posMax.subtract(posMin).offset(1, 1, 1); + blockContainers = LitematicaBlockStateContainer.createFrom(palette, blockStateArr, containerSize); + if (minecraftDataVersion < MINECRAFT_DATA_VERSION) { + ServuxProtocol.LOGGER.warn("Cannot process minecraft data version: {}", minecraftDataVersion); + } + } + + return new SubRegion(blockContainers, tileEntities, pendingBlockTicks, pendingFluidTicks, entities, position, size); + } + + private static List readEntitiesFromNBT(ListTag tagList) { + List entityList = new ArrayList<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag entityData = tagList.getCompound(i); + Vec3 posVec = NbtUtils.readEntityPositionFromTag(entityData); + + if (posVec != null && !entityData.isEmpty()) { + entityList.add(new EntityInfo(posVec, entityData)); + } + } + + return entityList; + } + + private static Map readTileEntitiesFromNBT(ListTag tagList) { + Map tileMap = new HashMap<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompound(i); + BlockPos pos = NbtUtils.readBlockPos(tag); + + if (pos != null && !tag.isEmpty()) { + tileMap.put(pos, tag); + } + } + + return tileMap; + } + + private static Map> readPendingTicksFromNBT(ListTag tagList, Registry registry, String tagName, T emptyValue) { + Map> tickMap = new HashMap<>(); + final int size = tagList.size(); + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompound(i); + + // XXX these were accidentally saved as longs in version 3 + if (!tag.contains("Time", Tag.TAG_ANY_NUMERIC)) { + continue; + } + T target; + ResourceLocation resourceLocation = ResourceLocation.tryParse(tag.getString(tagName)); + if (resourceLocation == null) { + continue; + } + Optional> tReference = registry.get(resourceLocation); + if (tReference.isEmpty()) { + continue; + } + target = tReference.get().value(); + if (target == emptyValue) { + continue; + } + BlockPos pos = new BlockPos(tag.getInt("x"), tag.getInt("y"), tag.getInt("z")); + TickPriority priority = TickPriority.byValue(tag.getInt("Priority")); + int scheduledTime = tag.getInt("Time"); + long subTick = tag.getLong("SubTick"); + tickMap.put(pos, new ScheduledTick<>(target, pos, scheduledTime, priority, subTick)); + } + + return tickMap; + } + + private static List readEntitiesFromNBT_v1(ListTag tagList) { + List entityList = new ArrayList<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompound(i); + Vec3 posVec = NbtUtils.readVec3(tag); + CompoundTag entityData = tag.getCompound("EntityData"); + + if (posVec != null && !entityData.isEmpty()) { + // Update the correct position to the TileEntity NBT, where it is stored in version 2 + NbtUtils.writeEntityPositionToTag(posVec, entityData); + entityList.add(new EntityInfo(posVec, entityData)); + } + } + + return entityList; + } + + private static Map readTileEntitiesFromNBT_v1(ListTag tagList) { + Map tileMap = new HashMap<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompound(i); + CompoundTag tileNbt = tag.getCompound("TileNBT"); + + // Note: This within-schematic relative position is not inside the tile tag! + BlockPos pos = NbtUtils.readBlockPos(tag); + + if (pos != null && !tileNbt.isEmpty()) { + // Update the correct position to the entity NBT, where it is stored in version 2 + NbtUtils.writeBlockPosToTag(pos, tileNbt); + tileMap.put(pos, tileNbt); + } + } + + return tileMap; + } + } + + public record EntityInfo(Vec3 posVec, CompoundTag nbt) { + public EntityInfo(Vec3 posVec, CompoundTag nbt) { + this.posVec = posVec; + + if (nbt.contains("SleepingX", Tag.TAG_INT)) { + nbt.putInt("SleepingX", Mth.floor(posVec.x)); + } + if (nbt.contains("SleepingY", Tag.TAG_INT)) { + nbt.putInt("SleepingY", Mth.floor(posVec.y)); + } + if (nbt.contains("SleepingZ", Tag.TAG_INT)) { + nbt.putInt("SleepingZ", Mth.floor(posVec.z)); + } + + this.nbt = nbt; + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java new file mode 100644 index 00000000..5206b10b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java @@ -0,0 +1,63 @@ +package org.leavesmc.leaves.protocol.servux.litematics; + +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.servux.litematics.utils.FileType; +import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils; +import org.leavesmc.leaves.protocol.servux.litematics.utils.Schema; + +import javax.annotation.Nullable; +import java.util.Objects; + +public record SchematicMetadata( + String name, + String author, + String description, + Vec3i enclosingSize, + long timeCreated, + long timeModified, + int minecraftDataVersion, + int schematicVersion, + Schema schema, + FileType type, + int regionCount, + long totalVolume, + long totalBlocks, + @Nullable int[] thumbnailPixelData +) { + @NotNull + @Contract("_, _, _, _ -> new") + public static SchematicMetadata readFromNBT(@NotNull CompoundTag nbt, int version, int minecraftDataVersion, FileType fileType) { + String name = nbt.getString("Name"); + String author = nbt.getString("Author"); + String description = nbt.getString("Description"); + int regionCount = nbt.getInt("RegionCount"); + long timeCreated = nbt.getLong("TimeCreated"); + long timeModified = nbt.getLong("TimeModified"); + + long totalVolume = -1; + if (nbt.contains("TotalVolume", Tag.TAG_ANY_NUMERIC)) { + totalVolume = nbt.getInt("TotalVolume"); + } + + long totalBlocks = -1; + if (nbt.contains("TotalBlocks", Tag.TAG_ANY_NUMERIC)) { + totalBlocks = nbt.getInt("TotalBlocks"); + } + + Vec3i enclosingSize = Vec3i.ZERO; + if (nbt.contains("EnclosingSize", Tag.TAG_COMPOUND)) { + enclosingSize = Objects.requireNonNullElse(NbtUtils.readVec3iFromTag(nbt.getCompound("EnclosingSize")), Vec3i.ZERO); + } + + int[] thumbnailPixelData = null; + if (nbt.contains("PreviewImageData", Tag.TAG_INT_ARRAY)) { + thumbnailPixelData = nbt.getIntArray("PreviewImageData"); + } + + return new SchematicMetadata(name, author, description, enclosingSize, timeCreated, timeModified, minecraftDataVersion, version, Schema.getSchemaByDataVersion(minecraftDataVersion), fileType, regionCount, totalVolume, totalBlocks, thumbnailPixelData); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java new file mode 100755 index 00000000..1a928f7f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java @@ -0,0 +1,385 @@ +package org.leavesmc.leaves.protocol.servux.litematics; + +import io.netty.buffer.Unpooled; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.servux.PacketSplitter; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SchematicPlacement; +import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils; +import org.leavesmc.leaves.protocol.servux.litematics.utils.ReplaceBehavior; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +@LeavesProtocol(namespace = "servux") +public class ServuxLitematicsProtocol { + + public static final ResourceLocation CHANNEL = ServuxProtocol.id("litematics"); + public static final int PROTOCOL_VERSION = 1; + + private static final CompoundTag metadata = new CompoundTag(); + private static final Map playerSession = new HashMap<>(); + + @ProtocolHandler.Init + public static void init() { + metadata.putString("name", "litematic_data"); + metadata.putString("id", CHANNEL.toString()); + metadata.putInt("version", PROTOCOL_VERSION); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + } + + public static boolean hasPermission(ServerPlayer player) { + CraftPlayer bukkitEntity = player.getBukkitEntity(); + return bukkitEntity.hasPermission("leaves.protocol.litematics"); + } + + public static boolean isEnabled() { + return LeavesConfig.protocol.servux.litematicsProtocol; + } + + public static void encodeServerData(ServerPlayer player, @NotNull ServuxLitematicaPayload packet) { + if (packet.packetType.equals(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_START)) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeVarInt(packet.getTransactionId()); + buffer.writeNbt(packet.getCompound()); + PacketSplitter.send(ServuxLitematicsProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, packet); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + ServuxLitematicaPayload payload = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_DATA); + payload.buffer = buf; + payload.nbt = new CompoundTag(); + encodeServerData(player, payload); + } + + @ProtocolHandler.PayloadReceiver(payload = ServuxLitematicaPayload.class, payloadId = "litematics") + public static void onPacketReceive(ServerPlayer player, ServuxLitematicaPayload payload) { + if (!isEnabled() || !hasPermission(player)) { + return; + } + + switch (payload.packetType) { + case PACKET_C2S_METADATA_REQUEST -> { + ServuxLitematicaPayload send = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_METADATA); + send.nbt.merge(metadata); + encodeServerData(player, send); + } + + case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> onBulkEntityRequest(player, payload.getChunkPos(), payload.getCompound()); + + case PACKET_C2S_NBT_RESPONSE_DATA -> { + ServuxProtocol.LOGGER.debug("nbt response data"); + UUID uuid = player.getUUID(); + Long session = playerSession.getOrDefault(uuid, new Random().nextLong()); + playerSession.put(uuid, session); + FriendlyByteBuf fullPacket = PacketSplitter.receive(session, payload.getBuffer()); + if (fullPacket == null) { + ServuxProtocol.LOGGER.debug("packet is none"); + return; + } + playerSession.remove(uuid); + fullPacket.readVarInt(); + CompoundTag compoundTag = fullPacket.readNbt(); + if (compoundTag == null) { + ServuxProtocol.LOGGER.error("cannot read nbt tag from packet"); + return; + } + handleClientPasteRequest(player, compoundTag); + } + } + } + + public static void onBulkEntityRequest(ServerPlayer player, ChunkPos chunkPos, CompoundTag req) { + if (req == null || req.isEmpty()) { + return; + } + + ServerLevel world = player.serverLevel(); + ChunkAccess chunk = world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.FULL, false); + + if (chunk == null) { + return; + } + if ((req.contains("Task") && req.getString("Task").equals("BulkEntityRequest")) || + !req.contains("Task")) { + ServuxProtocol.LOGGER.debug("litematic_data: Sending Bulk NBT Data for ChunkPos [{}] to player {}", chunkPos, player.getName().getString()); + + long timeStart = System.currentTimeMillis(); + ListTag tileList = new ListTag(); + ListTag entityList = new ListTag(); + int minY = req.getInt("minY"); + int maxY = req.getInt("maxY"); + BlockPos pos1 = new BlockPos(chunkPos.getMinBlockX(), minY, chunkPos.getMinBlockZ()); + BlockPos pos2 = new BlockPos(chunkPos.getMaxBlockX(), maxY, chunkPos.getMaxBlockZ()); + AABB bb = AABB.encapsulatingFullBlocks(pos1, pos2); + Set teSet = chunk.getBlockEntitiesPos(); + List entities = world.getEntitiesOfClass(Entity.class, bb, e -> !(e instanceof Player)); + for (BlockPos tePos : teSet) { + if ((tePos.getX() < chunkPos.getMinBlockX() || tePos.getX() > chunkPos.getMaxBlockX()) || + (tePos.getZ() < chunkPos.getMinBlockZ() || tePos.getZ() > chunkPos.getMaxBlockZ()) || + (tePos.getY() < minY || tePos.getY() > maxY)) { + continue; + } + + BlockEntity be = world.getBlockEntity(tePos); + CompoundTag beTag = be != null ? be.saveWithId(player.registryAccess()) : new CompoundTag(); + tileList.add(beTag); + } + + for (Entity entity : entities) { + CompoundTag entTag = new CompoundTag(); + + if (entity.save(entTag)) { + Vec3 posVec = new Vec3(entity.getX() - pos1.getX(), entity.getY() - pos1.getY(), entity.getZ() - pos1.getZ()); + NbtUtils.writeEntityPositionToTag(posVec, entTag); + entTag.putInt("entityId", entity.getId()); + entityList.add(entTag); + } + } + + CompoundTag output = new CompoundTag(); + output.putString("Task", "BulkEntityReply"); + output.put("TileEntities", tileList); + output.put("Entities", entityList); + output.putInt("chunkX", chunkPos.x); + output.putInt("chunkZ", chunkPos.z); + ServuxProtocol.LOGGER.debug("process bulk entity used: {}ms", System.currentTimeMillis() - timeStart); + + ServuxLitematicaPayload send = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_START); + send.nbt.merge(output); + encodeServerData(player, send); + } + } + + public static void handleClientPasteRequest(ServerPlayer player, @NotNull CompoundTag tags) { + if (tags.getString("Task").equals("LitematicaPaste")) { + ServuxProtocol.LOGGER.debug("litematic_data: Servux Paste request from player {}", player.getName().getString()); + ServerLevel serverLevel = player.serverLevel(); + long timeStart = System.currentTimeMillis(); + SchematicPlacement placement = SchematicPlacement.createFromNbt(tags); + ReplaceBehavior replaceMode = ReplaceBehavior.fromStringStatic(tags.getString("ReplaceMode")); + MinecraftServer server = MinecraftServer.getServer(); + server.scheduleOnMain(() -> { + placement.pasteTo(serverLevel, replaceMode); + long timeElapsed = System.currentTimeMillis() - timeStart; + player.getBukkitEntity().sendActionBar( + Component.text("Pasted ") + .append(Component.text(placement.getName()).color(NamedTextColor.AQUA)) + .append(Component.text(" to world ")) + .append(Component.text(serverLevel.serverLevelData.getLevelName()).color(NamedTextColor.LIGHT_PURPLE)) + .append(Component.text(" in ")) + .append(Component.text(timeElapsed).color(NamedTextColor.GREEN)) + .append(Component.text("ms")) + ); + }); + } + } + + public enum ServuxLitematicaPayloadType { + PACKET_S2C_METADATA(1), + PACKET_C2S_METADATA_REQUEST(2), + PACKET_C2S_BLOCK_ENTITY_REQUEST(3), + PACKET_C2S_ENTITY_REQUEST(4), + PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE(5), + PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE(6), + PACKET_C2S_BULK_ENTITY_NBT_REQUEST(7), + // For Packet Splitter (Oversize Packets, S2C) + PACKET_S2C_NBT_RESPONSE_START(10), + PACKET_S2C_NBT_RESPONSE_DATA(11), + // For Packet Splitter (Oversize Packets, C2S) + PACKET_C2S_NBT_RESPONSE_START(12), + PACKET_C2S_NBT_RESPONSE_DATA(13); + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + + public final int type; + + ServuxLitematicaPayloadType(int type) { + this.type = type; + ServuxLitematicaPayloadType.Helper.ID_TO_TYPE.put(type, this); + } + + public static ServuxLitematicaPayloadType fromId(int id) { + return ServuxLitematicaPayloadType.Helper.ID_TO_TYPE.get(id); + } + } + + public static class ServuxLitematicaPayload implements LeavesCustomPayload { + + private final ServuxLitematicaPayloadType packetType; + private final int transactionId; + private int entityId; + private BlockPos pos; + private CompoundTag nbt; + private ChunkPos chunkPos; + private FriendlyByteBuf buffer; + public static final int PROTOCOL_VERSION = 1; + + private ServuxLitematicaPayload(ServuxLitematicaPayloadType type) { + this.packetType = type; + this.transactionId = -1; + this.entityId = -1; + this.pos = BlockPos.ZERO; + this.chunkPos = ChunkPos.ZERO; + this.nbt = new CompoundTag(); + } + + public int getVersion() { + return PROTOCOL_VERSION; + } + + public int getTransactionId() { + return this.transactionId; + } + + public int getEntityId() { + return this.entityId; + } + + public BlockPos getPos() { + return this.pos; + } + + public CompoundTag getCompound() { + return this.nbt; + } + + public ChunkPos getChunkPos() { + return this.chunkPos; + } + + public FriendlyByteBuf getBuffer() { + return this.buffer; + } + + public boolean hasBuffer() { + return this.buffer != null && this.buffer.isReadable(); + } + + public boolean hasNbt() { + return this.nbt != null && !this.nbt.isEmpty(); + } + + public boolean isEmpty() { + return !this.hasBuffer() && !this.hasNbt(); + } + + @New + public static ServuxLitematicaPayload decode(ResourceLocation location, FriendlyByteBuf input) { + ServuxLitematicaPayloadType type = ServuxLitematicaPayloadType.fromId(input.readVarInt()); + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } + + ServuxLitematicaPayload payload = new ServuxLitematicaPayload(type); + switch (type) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + input.readVarInt(); + payload.pos = input.readBlockPos().immutable(); + } + + case PACKET_C2S_ENTITY_REQUEST -> { + input.readVarInt(); + payload.entityId = input.readVarInt(); + } + + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + payload.pos = input.readBlockPos().immutable(); + payload.nbt = input.readNbt(); + } + + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + payload.entityId = input.readVarInt(); + payload.nbt = input.readNbt(); + } + + case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> { + payload.chunkPos = input.readChunkPos(); + payload.nbt = input.readNbt(); + } + + case PACKET_C2S_NBT_RESPONSE_DATA, PACKET_S2C_NBT_RESPONSE_DATA -> payload.buffer = new FriendlyByteBuf(input.readBytes(input.readableBytes())); + + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> payload.nbt = input.readNbt(); + } + + return payload; + } + + @Override + public void write(FriendlyByteBuf output) { + output.writeVarInt(this.packetType.type); + + switch (this.packetType) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + output.writeVarInt(this.transactionId); + output.writeBlockPos(this.pos); + } + + case PACKET_C2S_ENTITY_REQUEST -> { + output.writeVarInt(this.transactionId); + output.writeVarInt(this.entityId); + } + + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + output.writeBlockPos(this.pos); + output.writeNbt(this.nbt); + } + + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + output.writeVarInt(this.entityId); + output.writeNbt(this.nbt); + } + + case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> { + output.writeChunkPos(this.chunkPos); + output.writeNbt(this.nbt); + } + + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> output.writeBytes(this.buffer.readBytes(this.buffer.readableBytes())); + + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> output.writeNbt(this.nbt); + + default -> ServuxProtocol.LOGGER.error("ServuxLitematicaPacket#toPacket: Unknown packet type!"); + } + } + + @Override + public ResourceLocation id() { + return CHANNEL; + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java new file mode 100644 index 00000000..c4dc4bff --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java @@ -0,0 +1,87 @@ +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class LitematicaBitArray { + /** + * The long array that is used to store the data for this BitArray. + */ + private final long[] longArray; + /** + * Number of bits a single entry takes up + */ + private final int bitsPerEntry; + /** + * The maximum value for a single entry. This also works as a bitmask for a single entry. + * For instance, if bitsPerEntry were 5, this value would be 31 (ie, {@code 0b00011111}). + */ + private final long maxEntryValue; + /** + * Number of entries in this array (not the length of the long array that internally backs this array) + */ + private final long arraySize; + + public LitematicaBitArray(int bitsPerEntryIn, long arraySizeIn) { + this(bitsPerEntryIn, arraySizeIn, null); + } + + public LitematicaBitArray(int bitsPerEntryIn, long arraySizeIn, @Nullable long[] longArrayIn) { + Validate.inclusiveBetween(1L, 32L, bitsPerEntryIn); + this.arraySize = arraySizeIn; + this.bitsPerEntry = bitsPerEntryIn; + this.maxEntryValue = (1L << bitsPerEntryIn) - 1L; + + this.longArray = Objects.requireNonNullElseGet(longArrayIn, () -> new long[(int) (roundUp(arraySizeIn * bitsPerEntryIn, 64L) / 64L)]); + } + + public void setAt(long index, int value) { + long startOffset = index * (long) this.bitsPerEntry; + int startArrIndex = (int) (startOffset >> 6); // startOffset / 64 + int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6); + int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64 + this.longArray[startArrIndex] = this.longArray[startArrIndex] & ~(this.maxEntryValue << startBitOffset) | ((long) value & this.maxEntryValue) << startBitOffset; + + if (startArrIndex != endArrIndex) { + int endOffset = 64 - startBitOffset; + int j1 = this.bitsPerEntry - endOffset; + this.longArray[endArrIndex] = this.longArray[endArrIndex] >>> j1 << j1 | ((long) value & this.maxEntryValue) >> endOffset; + } + } + + public int getAt(long index) { + long startOffset = index * (long) this.bitsPerEntry; + int startArrIndex = (int) (startOffset >> 6); // startOffset / 64 + int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6); + int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64 + + if (startArrIndex == endArrIndex) { + return (int) (this.longArray[startArrIndex] >>> startBitOffset & this.maxEntryValue); + } else { + int endOffset = 64 - startBitOffset; + return (int) ((this.longArray[startArrIndex] >>> startBitOffset | this.longArray[endArrIndex] << endOffset) & this.maxEntryValue); + } + } + + public long size() { + return this.arraySize; + } + + public static long roundUp(long value, long interval) { + if (interval == 0L) { + return 0L; + } else if (value == 0L) { + return interval; + } else { + if (value < 0L) { + interval *= -1L; + } + + long remainder = value % interval; + + return remainder == 0L ? value : value + interval - remainder; + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java new file mode 100644 index 00000000..94f2a54e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java @@ -0,0 +1,101 @@ +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public class LitematicaBlockStateContainer { + + public static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); + protected LitematicaBitArray storage; + protected LitematicaBlockStatePalette palette; + protected final Vec3i size; + protected final int sizeX; + protected final int sizeY; + protected final int sizeZ; + protected final int sizeLayer; + protected final long totalVolume; + protected int bits; + + public LitematicaBlockStateContainer(int sizeX, int sizeY, int sizeZ, int bits, @Nullable long[] backingLongArray) { + this.sizeX = sizeX; + this.sizeY = sizeY; + this.sizeZ = sizeZ; + this.sizeLayer = sizeX * sizeZ; + this.totalVolume = (long) this.sizeX * (long) this.sizeY * (long) this.sizeZ; + this.size = new Vec3i(this.sizeX, this.sizeY, this.sizeZ); + + this.setBits(bits, backingLongArray); + } + + public Vec3i getSize() { + return this.size; + } + + public LitematicaBitArray getArray() { + return this.storage; + } + + public BlockState get(int x, int y, int z) { + BlockState state = this.palette.getBlockState(this.storage.getAt(this.getIndex(x, y, z))); + return state == null ? AIR_BLOCK_STATE : state; + } + + protected int getIndex(int x, int y, int z) { + return (y * this.sizeLayer) + z * this.sizeX + x; + } + + protected void setBits(int bitsIn, @Nullable long[] backingLongArray) { + if (bitsIn != this.bits) { + this.bits = bitsIn; + + if (this.bits <= 4) { + this.bits = Math.max(2, this.bits); + this.palette = new LitematicaBlockStatePaletteLinear(this.bits, this); + } else { + this.palette = new LitematicaBlockStatePaletteHashMap(this.bits, this); + } + + this.palette.idFor(AIR_BLOCK_STATE); + + if (backingLongArray != null) { + this.storage = new LitematicaBitArray(this.bits, this.totalVolume, backingLongArray); + } else { + this.storage = new LitematicaBitArray(this.bits, this.totalVolume); + } + } + } + + public int onResize(int bits, BlockState state) { + LitematicaBitArray oldStorage = this.storage; + LitematicaBlockStatePalette oldPalette = this.palette; + final long storageLength = oldStorage.size(); + + this.setBits(bits, null); + + LitematicaBitArray newStorage = this.storage; + + for (long index = 0; index < storageLength; ++index) { + newStorage.setAt(index, oldStorage.getAt(index)); + } + + this.palette.readFromNBT(oldPalette.writeToNBT()); + + return this.palette.idFor(state); + } + + public LitematicaBlockStatePalette getPalette() { + return this.palette; + } + + public static LitematicaBlockStateContainer createFrom(ListTag palette, long[] blockStates, BlockPos size) { + int bits = Math.max(2, Integer.SIZE - Integer.numberOfLeadingZeros(palette.size() - 1)); + LitematicaBlockStateContainer container = new LitematicaBlockStateContainer(size.getX(), size.getY(), size.getZ(), bits, blockStates); + container.palette.readFromNBT(palette); + return container; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java new file mode 100644 index 00000000..c69f628d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java @@ -0,0 +1,24 @@ +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.nbt.ListTag; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public interface LitematicaBlockStatePalette { + /** + * Gets the palette id for the given block state and adds + * the state to the palette if it doesn't exist there yet. + */ + int idFor(BlockState state); + + /** + * Gets the block state by the palette id. + */ + @Nullable + BlockState getBlockState(int indexKey); + + void readFromNBT(ListTag tagList); + + ListTag writeToNBT(); +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java new file mode 100644 index 00000000..1ad11431 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java @@ -0,0 +1,93 @@ +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.leavesmc.leaves.protocol.servux.litematics.utils.Int2ObjectBiMap; + +import javax.annotation.Nullable; + +public class LitematicaBlockStatePaletteHashMap implements LitematicaBlockStatePalette { + + private final Int2ObjectBiMap statePaletteMap; + private final LitematicaBlockStateContainer blockStateContainer; + private final int bits; + + public LitematicaBlockStatePaletteHashMap(int bitsIn, LitematicaBlockStateContainer blockStateContainer) { + this.bits = bitsIn; + this.blockStateContainer = blockStateContainer; + this.statePaletteMap = Int2ObjectBiMap.create(1 << bitsIn); + } + + @Override + public int idFor(BlockState state) { + int i = this.statePaletteMap.getRawId(state); + + if (i == -1) { + i = this.statePaletteMap.add(state); + + if (i >= (1 << this.bits)) { + i = this.blockStateContainer.onResize(this.bits + 1, state); + } + } + + return i; + } + + @Override + @Nullable + public BlockState getBlockState(int indexKey) { + return this.statePaletteMap.get(indexKey); + } + + private void requestNewId(BlockState state) { + final int origId = this.statePaletteMap.add(state); + + if (origId >= (1 << this.bits)) { + int newId = this.blockStateContainer.onResize(this.bits + 1, LitematicaBlockStateContainer.AIR_BLOCK_STATE); + + if (newId <= origId) { + this.statePaletteMap.add(state); + } + } + } + + @Override + public void readFromNBT(ListTag tagList) { + Registry lookup = MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BLOCK); + + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompound(i); + BlockState state = NbtUtils.readBlockState(lookup, tag); + + if (i > 0 || state != LitematicaBlockStateContainer.AIR_BLOCK_STATE) { + this.requestNewId(state); + } + } + } + + @Override + public ListTag writeToNBT() { + ListTag tagList = new ListTag(); + + for (int id = 0; id < this.statePaletteMap.size(); ++id) { + BlockState state = this.statePaletteMap.get(id); + + if (state == null) { + state = LitematicaBlockStateContainer.AIR_BLOCK_STATE; + } + + CompoundTag tag = NbtUtils.writeBlockState(state); + tagList.add(tag); + } + + return tagList; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java new file mode 100644 index 00000000..6f6ea248 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java @@ -0,0 +1,94 @@ +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public class LitematicaBlockStatePaletteLinear implements LitematicaBlockStatePalette { + + private final BlockState[] states; + private final LitematicaBlockStateContainer blockStateContainer; + private final int bits; + private int currentSize; + + public LitematicaBlockStatePaletteLinear(int bitsIn, LitematicaBlockStateContainer blockStateContainer) { + this.states = new BlockState[1 << bitsIn]; + this.bits = bitsIn; + this.blockStateContainer = blockStateContainer; + } + + @Override + public int idFor(BlockState state) { + for (int i = 0; i < this.currentSize; ++i) { + if (this.states[i] == state) { + return i; + } + } + + final int size = this.currentSize; + + if (size < this.states.length) { + this.states[size] = state; + ++this.currentSize; + return size; + } else { + return this.blockStateContainer.onResize(this.bits + 1, state); + } + } + + @Override + @Nullable + public BlockState getBlockState(int indexKey) { + return indexKey >= 0 && indexKey < this.currentSize ? this.states[indexKey] : null; + } + + private void requestNewId(BlockState state) { + final int size = this.currentSize; + + if (size < this.states.length) { + this.states[size] = state; + ++this.currentSize; + } + } + + @Override + public void readFromNBT(ListTag tagList) { + Registry lookup = MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BLOCK); + + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompound(i); + BlockState state = NbtUtils.readBlockState(lookup, tag); + + if (i > 0 || state != LitematicaBlockStateContainer.AIR_BLOCK_STATE) { + this.requestNewId(state); + } + } + } + + @Override + public ListTag writeToNBT() { + ListTag tagList = new ListTag(); + + for (int id = 0; id < this.currentSize; ++id) { + BlockState state = this.states[id]; + + if (state == null) { + state = LitematicaBlockStateContainer.AIR_BLOCK_STATE; + } + + CompoundTag tag = NbtUtils.writeBlockState(state); + tagList.add(tag); + } + + return tagList; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java new file mode 100644 index 00000000..180b3b06 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java @@ -0,0 +1,283 @@ +package org.leavesmc.leaves.protocol.servux.litematics.placement; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.core.BlockBox; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.AABB; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.LitematicaSchematic; +import org.leavesmc.leaves.protocol.servux.litematics.selection.Box; +import org.leavesmc.leaves.protocol.servux.litematics.utils.IntBoundingBox; +import org.leavesmc.leaves.protocol.servux.litematics.utils.PositionUtils; +import org.leavesmc.leaves.protocol.servux.litematics.utils.ReplaceBehavior; +import org.leavesmc.leaves.protocol.servux.litematics.utils.SchematicPlacingUtils; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +public class SchematicPlacement { + + private final Map relativeSubRegionPlacements = new HashMap<>(); + private final LitematicaSchematic schematic; + private final BlockPos origin; + private final String name; + private Rotation rotation = Rotation.NONE; + private Mirror mirror = Mirror.NONE; + + private SchematicPlacement(LitematicaSchematic schematic, BlockPos origin, String name) { + this.schematic = schematic; + this.origin = origin; + this.name = name; + } + + public static SchematicPlacement createFromNbt(CompoundTag tags) { + SchematicPlacement placement = new SchematicPlacement( + LitematicaSchematic.readFromNBT(tags.getCompound("Schematics")), + NbtUtils.readBlockPos(tags, "Origin").orElseThrow(), + tags.getString("Name") + ); + placement.mirror = Mirror.values()[tags.getInt("Mirror")]; + placement.rotation = Rotation.values()[tags.getInt("Rotation")]; + for (String name : tags.getCompound("SubRegions").getAllKeys()) { + CompoundTag compound = tags.getCompound("SubRegions").getCompound(name); + var sub = new SubRegionPlacement( + compound.getString("Name"), + NbtUtils.readBlockPos(compound, "Pos").orElseThrow(), + Rotation.values()[compound.getInt("Rotation")], + Mirror.values()[compound.getInt("Mirror")], + compound.getBoolean("Enabled"), + compound.getBoolean("IgnoreEntities") + ); + placement.relativeSubRegionPlacements.put(name, sub); + } + return placement; + } + + public boolean ignoreEntities() { + return false; + } + + public String getName() { + return this.name; + } + + public LitematicaSchematic getSchematic() { + return schematic; + } + + public BlockPos getOrigin() { + return origin; + } + + public Rotation getRotation() { + return rotation; + } + + public Mirror getMirror() { + return mirror; + } + + @Nullable + public SubRegionPlacement getRelativeSubRegionPlacement(String areaName) { + return this.relativeSubRegionPlacements.get(areaName); + } + + public ImmutableMap getSubRegionBoxes(SubRegionPlacement.RequiredEnabled required) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + Map areaSizes = this.schematic.getAreaSizes(); + + for (Map.Entry entry : this.relativeSubRegionPlacements.entrySet()) { + String name = entry.getKey(); + BlockPos areaSize = areaSizes.get(name); + + if (areaSize == null) { + ServuxProtocol.LOGGER.warn("SchematicPlacement.getSubRegionBoxes(): Size for sub-region '{}' not found in the schematic '{}'", name, this.schematic.metadata().name()); + continue; + } + + SubRegionPlacement placement = entry.getValue(); + + if (placement.matchesRequirement(required)) { + putBoxPosIntoBuilder(builder, name, areaSize, placement); + } + } + + return builder.build(); + } + + private void putBoxPosIntoBuilder(ImmutableMap.Builder builder, String name, BlockPos areaSize, SubRegionPlacement placement) { + BlockPos boxOriginRelative = placement.pos(); + + BlockPos boxOriginAbsolute = PositionUtils.getTransformedBlockPos(boxOriginRelative, this.mirror, this.rotation).offset(this.origin); + BlockPos pos2 = PositionUtils.getRelativeEndPositionFromAreaSize(areaSize); + pos2 = PositionUtils.getTransformedBlockPos(pos2, this.mirror, this.rotation); + pos2 = PositionUtils.getTransformedBlockPos(pos2, placement.mirror(), placement.rotation()).offset(boxOriginAbsolute); + + builder.put(name, new Box(boxOriginAbsolute, pos2, name)); + } + + public static IntBoundingBox getBoundsWithinChunkForBox(Box box, int chunkX, int chunkZ) { + final int chunkXMin = chunkX << 4; + final int chunkZMin = chunkZ << 4; + final int chunkXMax = chunkXMin + 15; + final int chunkZMax = chunkZMin + 15; + BlockPos boxPos1 = box.pos1(); + BlockPos boxPos2 = box.pos2(); + if (boxPos1 == null || boxPos2 == null) { + return null; + } + + int x1 = boxPos1.getX(); + int x2 = boxPos2.getX(); + int y1 = boxPos1.getY(); + int y2 = boxPos2.getY(); + int z1 = boxPos1.getZ(); + int z2 = boxPos2.getZ(); + final int boxXMin = Math.min(x1, x2); + final int boxZMin = Math.min(z1, z2); + final int boxXMax = Math.max(x1, x2); + final int boxZMax = Math.max(z1, z2); + + boolean notOverlapping = boxXMin > chunkXMax || boxZMin > chunkZMax || boxXMax < chunkXMin || boxZMax < chunkZMin; + + if (!notOverlapping) { + final int xMin = Math.max(chunkXMin, boxXMin); + final int yMin = Math.min(y1, y2); + final int zMin = Math.max(chunkZMin, boxZMin); + final int xMax = Math.min(chunkXMax, boxXMax); + final int yMax = Math.max(y1, y2); + final int zMax = Math.min(chunkZMax, boxZMax); + + return new IntBoundingBox(xMin, yMin, zMin, xMax, yMax, zMax); + } + + return null; + } + + public ImmutableMap getSubRegionBoxFor(String regionName, SubRegionPlacement.RequiredEnabled required) { + SubRegionPlacement placement = this.relativeSubRegionPlacements.get(regionName); + if (placement == null) return ImmutableMap.of(); + ImmutableMap.Builder builder = ImmutableMap.builder(); + Map areaSizes = this.schematic.getAreaSizes(); + + if (placement.matchesRequirement(required)) { + BlockPos areaSize = areaSizes.get(regionName); + + if (areaSize != null) { + putBoxPosIntoBuilder(builder, regionName, areaSize, placement); + } else { + ServuxProtocol.LOGGER.warn("SchematicPlacement.getSubRegionBoxFor(): Size for sub-region '{}' not found in the schematic '{}'", regionName, this.schematic.metadata().name()); + } + } + + return builder.build(); + } + + public Set getRegionsTouchingChunk(int chunkX, int chunkZ) { + ImmutableMap map = this.getSubRegionBoxes(SubRegionPlacement.RequiredEnabled.PLACEMENT_ENABLED); + final int chunkXMin = chunkX << 4; + final int chunkZMin = chunkZ << 4; + final int chunkXMax = chunkXMin + 15; + final int chunkZMax = chunkZMin + 15; + Set set = new HashSet<>(); + + for (Map.Entry entry : map.entrySet()) { + Box box = entry.getValue(); + BlockPos boxPos1 = box.pos1(); + BlockPos boxPos2 = box.pos2(); + if (boxPos1 == null || boxPos2 == null) { + continue; + } + + int x1 = boxPos1.getX(); + int x2 = boxPos2.getX(); + int z1 = boxPos1.getZ(); + int z2 = boxPos2.getZ(); + final int boxXMin = Math.min(x1, x2); + final int boxZMin = Math.min(z1, z2); + final int boxXMax = Math.max(x1, x2); + final int boxZMax = Math.max(z1, z2); + + boolean notOverlapping = boxXMin > chunkXMax || boxZMin > chunkZMax || boxXMax < chunkXMin || boxZMax < chunkZMin; + + if (!notOverlapping) { + set.add(entry.getKey()); + } + } + + return set; + } + + @Nullable + public IntBoundingBox getBoxWithinChunkForRegion(String regionName, int chunkX, int chunkZ) { + Box box = this.getSubRegionBoxFor(regionName, SubRegionPlacement.RequiredEnabled.PLACEMENT_ENABLED).get(regionName); + return box != null ? getBoundsWithinChunkForBox(box, chunkX, chunkZ) : null; + } + + private Box getEnclosingBox() { + ImmutableMap boxes = this.getSubRegionBoxes(SubRegionPlacement.RequiredEnabled.ANY); + BlockPos pos1 = null; + BlockPos pos2 = null; + + for (Box box : boxes.values()) { + BlockPos tmp; + BlockPos boxPos1 = box.pos1(); + BlockPos boxPos2 = box.pos2(); + if (boxPos1 == null || boxPos2 == null) continue; + tmp = PositionUtils.getMinCorner(boxPos1, boxPos2); + + if (pos1 == null) { + pos1 = tmp; + } else if (tmp.getX() < pos1.getX() || tmp.getY() < pos1.getY() || tmp.getZ() < pos1.getZ()) { + pos1 = PositionUtils.getMinCorner(tmp, pos1); + } + + tmp = PositionUtils.getMaxCorner(boxPos1, boxPos2); + + if (pos2 == null) { + pos2 = tmp; + } else if (tmp.getX() > pos2.getX() || tmp.getY() > pos2.getY() || tmp.getZ() > pos2.getZ()) { + pos2 = PositionUtils.getMaxCorner(tmp, pos2); + } + } + + if (pos1 != null) { + return new Box(pos1, pos2, "Enclosing Box (Servux)"); + } + + return null; + } + + public Stream streamChunkPos(@NotNull BlockBox box) { + AABB aabb = box.aabb(); + int i = SectionPos.blockToSectionCoord(aabb.minX); + int j = SectionPos.blockToSectionCoord(aabb.minZ); + int k = SectionPos.blockToSectionCoord(aabb.maxX); + int l = SectionPos.blockToSectionCoord(aabb.maxZ); + return ChunkPos.rangeClosed(new ChunkPos(i, j), new ChunkPos(k, l)); + } + + public void pasteTo(ServerLevel serverWorld, ReplaceBehavior replaceBehavior) { + Box enclosingBox = this.getEnclosingBox(); + if (enclosingBox == null || enclosingBox.pos1() == null || enclosingBox.pos2() == null) { + ServuxProtocol.LOGGER.error("receiver a null enclosing box"); + return; + } + streamChunkPos(Objects.requireNonNull(enclosingBox.toVanilla())).forEach(chunkPos -> + SchematicPlacingUtils.placeToWorldWithinChunk(serverWorld, chunkPos, this, replaceBehavior, false) + ); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java new file mode 100644 index 00000000..a49a05ad --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java @@ -0,0 +1,38 @@ +package org.leavesmc.leaves.protocol.servux.litematics.placement; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; + +public record SubRegionPlacement( + String name, + BlockPos pos, + Rotation rotation, + Mirror mirror, + boolean enabled, + boolean ignoreEntities +) { + public SubRegionPlacement(String name, BlockPos pos) { + this(name, pos, Rotation.NONE, Mirror.NONE, true, false); + } + + public boolean matchesRequirement(RequiredEnabled required) { + if (required == RequiredEnabled.ANY) { + return true; + } + + if (required == RequiredEnabled.PLACEMENT_ENABLED) { + return this.enabled(); + } + + ServuxProtocol.LOGGER.warn("RequiredEnabled.RENDERING_ENABLED is not supported on server side!"); + return false; + } + + public enum RequiredEnabled { + ANY, + PLACEMENT_ENABLED, + RENDERING_ENABLED + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java new file mode 100644 index 00000000..72acbec0 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java @@ -0,0 +1,19 @@ +package org.leavesmc.leaves.protocol.servux.litematics.selection; + +import net.minecraft.core.BlockBox; +import net.minecraft.core.BlockPos; +import org.jetbrains.annotations.Nullable; + +public record Box(@Nullable BlockPos pos1, @Nullable BlockPos pos2, String name) { + public Box() { + this(BlockPos.ZERO, BlockPos.ZERO, "Unnamed"); + } + + @Nullable + public BlockBox toVanilla() { + if (pos1 != null && pos2 != null) { + return new BlockBox(pos1, pos2); + } + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java new file mode 100644 index 00000000..a594e2b7 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java @@ -0,0 +1,94 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import com.google.common.collect.ImmutableList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.UUID; + +public class EntityUtils { + + @Nullable + private static Entity createEntityFromNBTSingle(CompoundTag nbt, Level world) { + try { + Optional optional = EntityType.create(nbt, world, EntitySpawnReason.LOAD); + + if (optional.isPresent()) { + Entity entity = optional.get(); + entity.setUUID(UUID.randomUUID()); + return entity; + } + } catch (Exception ignore) { + } + + return null; + } + + /** + * Note: This does NOT spawn any of the entities in the world! + * + * @param nbt () + * @param world () + * @return () + */ + @Nullable + public static Entity createEntityAndPassengersFromNBT(CompoundTag nbt, Level world) { + Entity entity = createEntityFromNBTSingle(nbt, world); + + if (entity == null) { + return null; + } + if (nbt.contains("Passengers", Tag.TAG_LIST)) { + ListTag tagList = nbt.getList("Passengers", Tag.TAG_LIST); + + for (int i = 0; i < tagList.size(); ++i) { + Entity passenger = createEntityAndPassengersFromNBT(tagList.getCompound(i), world); + + if (passenger != null) { + passenger.startRiding(entity, true); + } + } + } + + return entity; + } + + public static void spawnEntityAndPassengersInWorld(Entity entity, Level world) { + ImmutableList passengers = entity.passengers; + if (world.addFreshEntity(entity) && !passengers.isEmpty()) { + for (Entity passenger : passengers) { + passenger.absMoveTo( + entity.getX(), + entity.getY() + entity.getPassengerRidingPosition(passenger).y(), + entity.getZ(), + passenger.getYRot(), passenger.getXRot() + ); + setEntityRotations(passenger, passenger.getYRot(), passenger.getXRot()); + spawnEntityAndPassengersInWorld(passenger, world); + } + } + } + + public static void setEntityRotations(Entity entity, float yaw, float pitch) { + entity.setYRot(yaw); + entity.yRotO = yaw; + + entity.setXRot(pitch); + entity.xRotO = pitch; + + if (entity instanceof LivingEntity livingBase) { + livingBase.yHeadRot = yaw; + livingBase.yBodyRot = yaw; + livingBase.yHeadRotO = yaw; + livingBase.yBodyRotO = yaw; + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java new file mode 100644 index 00000000..874bf91d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java @@ -0,0 +1,11 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +public enum FileType { + INVALID, + UNKNOWN, + JSON, + LITEMATICA_SCHEMATIC, + SCHEMATICA_SCHEMATIC, + SPONGE_SCHEMATIC, + VANILLA_STRUCTURE +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java new file mode 100644 index 00000000..dc8d044b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java @@ -0,0 +1,194 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import com.google.common.base.Predicates; +import com.google.common.collect.Iterators; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Iterator; + +public class Int2ObjectBiMap implements Iterable { + + private static final Object EMPTY = null; + private K[] values; + private int[] ids; + private K[] idToValues; + private int nextId; + private int size; + + @SuppressWarnings("unchecked") + private Int2ObjectBiMap(int size) { + this.values = (K[]) (new Object[size]); + this.ids = new int[size]; + this.idToValues = (K[]) (new Object[size]); + } + + private Int2ObjectBiMap(K[] values, int[] ids, K[] idToValues, int nextId, int size) { + this.values = values; + this.ids = ids; + this.idToValues = idToValues; + this.nextId = nextId; + this.size = size; + } + + public static Int2ObjectBiMap create(int expectedSize) { + return new Int2ObjectBiMap<>((int) ((float) expectedSize / 0.8F)); + } + + public int getRawId(@Nullable K value) { + return this.getIdFromIndex(this.findIndex(value, this.getIdealIndex(value))); + } + + @Nullable + public K get(int index) { + return index >= 0 && index < this.idToValues.length ? this.idToValues[index] : null; + } + + private int getIdFromIndex(int index) { + return index == -1 ? -1 : this.ids[index]; + } + + public boolean contains(K value) { + return this.getRawId(value) != -1; + } + + public boolean containsKey(int index) { + return this.get(index) != null; + } + + public int add(K value) { + int i = this.nextId(); + this.put(value, i); + return i; + } + + private int nextId() { + while (this.nextId < this.idToValues.length && this.idToValues[this.nextId] != null) { + this.nextId++; + } + + return this.nextId; + } + + private void resize(int newSize) { + K[] objects = this.values; + int[] is = this.ids; + Int2ObjectBiMap int2ObjectBiMap = new Int2ObjectBiMap<>(newSize); + + for (int i = 0; i < objects.length; i++) { + if (objects[i] != null) { + int2ObjectBiMap.put(objects[i], is[i]); + } + } + + this.values = int2ObjectBiMap.values; + this.ids = int2ObjectBiMap.ids; + this.idToValues = int2ObjectBiMap.idToValues; + this.nextId = int2ObjectBiMap.nextId; + this.size = int2ObjectBiMap.size; + } + + public void put(K value, int id) { + int i = Math.max(id, this.size + 1); + if ((float) i >= (float) this.values.length * 0.8F) { + int j = this.values.length << 1; + + while (j < id) { + j <<= 1; + } + + this.resize(j); + } + + int j = this.findFree(this.getIdealIndex(value)); + this.values[j] = value; + this.ids[j] = id; + this.idToValues[id] = value; + this.size++; + if (id == this.nextId) { + this.nextId++; + } + } + + private static int idealHash(int value) { + value ^= value >>> 16; + value *= -2048144789; + value ^= value >>> 13; + value *= -1028477387; + return value ^ value >>> 16; + } + + private int getIdealIndex(@Nullable K value) { + + return (idealHash(System.identityHashCode(value)) & 2147483647) % this.values.length; + } + + private int findIndex(@Nullable K value, int id) { + for (int i = id; i < this.values.length; i++) { + if (this.values[i] == value) { + return i; + } + + if (this.values[i] == EMPTY) { + return -1; + } + } + + for (int i = 0; i < id; i++) { + if (this.values[i] == value) { + return i; + } + + if (this.values[i] == EMPTY) { + return -1; + } + } + + return -1; + } + + private int findFree(int size) { + for (int i = size; i < this.values.length; i++) { + if (this.values[i] == EMPTY) { + return i; + } + } + + for (int ix = 0; ix < size; ix++) { + if (this.values[ix] == EMPTY) { + return ix; + } + } + + throw new RuntimeException("Overflowed :("); + } + + public @NotNull Iterator iterator() { + return Iterators.filter(Iterators.forArray(this.idToValues), Predicates.notNull()); + } + + public void clear() { + Arrays.fill(this.values, null); + Arrays.fill(this.idToValues, null); + this.nextId = 0; + this.size = 0; + } + + public int size() { + return this.size; + } + + public Int2ObjectBiMap copy() { + return new Int2ObjectBiMap<>(this.values.clone(), this.ids.clone(), this.idToValues.clone(), this.nextId, this.size); + } + + public K getOrThrow(int index) { + K object = this.get(index); + if (object == null) { + throw new IllegalArgumentException("No value with id " + index); + } else { + return object; + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java new file mode 100644 index 00000000..bb0790b9 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java @@ -0,0 +1,26 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.Vec3i; + +public record IntBoundingBox(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + + public boolean containsPos(Vec3i pos) { + return pos.getX() >= this.minX && pos.getX() <= this.maxX && pos.getZ() >= this.minZ && pos.getZ() <= this.maxZ && pos.getY() >= this.minY && pos.getY() <= this.maxY; + } + + public boolean intersects(IntBoundingBox box) { + return this.maxX >= box.minX && this.minX <= box.maxX && this.maxZ >= box.minZ && this.minZ <= box.maxZ && this.maxY >= box.minY && this.minY <= box.maxY; + } + + public IntBoundingBox expand(int amount) { + return this.expand(amount, amount, amount); + } + + public IntBoundingBox expand(int x, int y, int z) { + return new IntBoundingBox(this.minX - x, this.minY - y, this.minZ - z, this.maxX + x, this.maxY + y, this.maxZ + z); + } + + public IntBoundingBox shrink(int x, int y, int z) { + return this.expand(-x, -y, -z); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java new file mode 100644 index 00000000..88257aa8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java @@ -0,0 +1,78 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +public class NbtUtils { + + public static void writeBlockPosToTag(Vec3i pos, CompoundTag tag) { + tag.putInt("x", pos.getX()); + tag.putInt("y", pos.getY()); + tag.putInt("z", pos.getZ()); + } + + @Nullable + public static BlockPos readBlockPos(@Nullable CompoundTag tag) { + if (tag != null && + tag.contains("x", Tag.TAG_INT) && + tag.contains("y", Tag.TAG_INT) && + tag.contains("z", Tag.TAG_INT)) { + return new BlockPos(tag.getInt("x"), tag.getInt("y"), tag.getInt("z")); + } + + return null; + } + + public static void writeEntityPositionToTag(Vec3 pos, CompoundTag tag) { + ListTag posList = new ListTag(); + + posList.add(DoubleTag.valueOf(pos.x)); + posList.add(DoubleTag.valueOf(pos.y)); + posList.add(DoubleTag.valueOf(pos.z)); + tag.put("Pos", posList); + } + + @Nullable + public static Vec3 readVec3(@Nullable CompoundTag tag) { + if (tag != null && + tag.contains("dx", Tag.TAG_DOUBLE) && + tag.contains("dy", Tag.TAG_DOUBLE) && + tag.contains("dz", Tag.TAG_DOUBLE)) { + return new Vec3(tag.getDouble("dx"), tag.getDouble("dy"), tag.getDouble("dz")); + } + + return null; + } + + @Nullable + public static Vec3 readEntityPositionFromTag(@Nullable CompoundTag tag) { + if (tag != null && tag.contains("Pos", Tag.TAG_LIST)) { + ListTag tagList = tag.getList("Pos", Tag.TAG_DOUBLE); + + if (tagList.getElementType() == Tag.TAG_DOUBLE && tagList.size() == 3) { + return new Vec3(tagList.getDouble(0), tagList.getDouble(1), tagList.getDouble(2)); + } + } + + return null; + } + + @Nullable + public static Vec3i readVec3iFromTag(@Nullable CompoundTag tag) { + if (tag != null && + tag.contains("x", Tag.TAG_INT) && + tag.contains("y", Tag.TAG_INT) && + tag.contains("z", Tag.TAG_INT)) { + return new Vec3i(tag.getInt("x"), tag.getInt("y"), tag.getInt("z")); + } + + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java new file mode 100644 index 00000000..8ad2fb37 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java @@ -0,0 +1,132 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SchematicPlacement; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SubRegionPlacement; + +public class PositionUtils { + + public static Direction rotateYCounterclockwise(Direction direction) { + Direction var10000; + switch (direction.ordinal()) { + case 2 -> var10000 = Direction.WEST; + case 3 -> var10000 = Direction.EAST; + case 4 -> var10000 = Direction.SOUTH; + case 5 -> var10000 = Direction.NORTH; + default -> throw new IllegalStateException("Unable to get CCW facing of " + direction); + } + + return var10000; + } + + public static BlockPos getMinCorner(BlockPos pos1, BlockPos pos2) { + return new BlockPos(Math.min(pos1.getX(), pos2.getX()), Math.min(pos1.getY(), pos2.getY()), Math.min(pos1.getZ(), pos2.getZ())); + } + + public static BlockPos getMaxCorner(BlockPos pos1, BlockPos pos2) { + return new BlockPos(Math.max(pos1.getX(), pos2.getX()), Math.max(pos1.getY(), pos2.getY()), Math.max(pos1.getZ(), pos2.getZ())); + } + + public static BlockPos getTransformedPlacementPosition(BlockPos posWithinSub, SchematicPlacement schematicPlacement, SubRegionPlacement placement) { + BlockPos pos = posWithinSub; + pos = getTransformedBlockPos(pos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + pos = getTransformedBlockPos(pos, placement.mirror(), placement.rotation()); + return pos; + } + + public static BlockPos getRelativeEndPositionFromAreaSize(Vec3i size) { + int x = size.getX(); + int y = size.getY(); + int z = size.getZ(); + + x = x >= 0 ? x - 1 : x + 1; + y = y >= 0 ? y - 1 : y + 1; + z = z >= 0 ? z - 1 : z + 1; + + return new BlockPos(x, y, z); + } + + /** + * Mirrors and then rotates the given position around the origin + */ + public static BlockPos getTransformedBlockPos(BlockPos pos, Mirror mirror, Rotation rotation) { + int x = pos.getX(); + int y = pos.getY(); + int z = pos.getZ(); + boolean isMirrored = true; + + switch (mirror) { + case LEFT_RIGHT -> z = -z; // LEFT_RIGHT is essentially NORTH_SOUTH + case FRONT_BACK -> x = -x; // FRONT_BACK is essentially EAST_WEST + default -> isMirrored = false; + } + + return switch (rotation) { + case CLOCKWISE_90 -> new BlockPos(-z, y, x); + case COUNTERCLOCKWISE_90 -> new BlockPos(z, y, -x); + case CLOCKWISE_180 -> new BlockPos(-x, y, -z); + default -> isMirrored ? new BlockPos(x, y, z) : pos; + }; + } + + public static BlockPos getReverseTransformedBlockPos(BlockPos pos, Mirror mirror, Rotation rotation) { + int x = pos.getX(); + int y = pos.getY(); + int z = pos.getZ(); + boolean isRotated = true; + int tmp = x; + + switch (rotation) { + case CLOCKWISE_90 -> { + x = z; + z = -tmp; + } + case COUNTERCLOCKWISE_90 -> { + x = -z; + z = tmp; + } + case CLOCKWISE_180 -> { + x = -x; + z = -z; + } + default -> isRotated = false; + } + + switch (mirror) { + case LEFT_RIGHT -> z = -z; // LEFT_RIGHT is essentially NORTH_SOUTH + case FRONT_BACK -> x = -x; // FRONT_BACK is essentially EAST_WEST + default -> { + if (!isRotated) { + return pos; + } + } + } + + return new BlockPos(x, y, z); + } + + public static Vec3 getTransformedPosition(Vec3 originalPos, Mirror mirror, Rotation rotation) { + double x = originalPos.x; + double y = originalPos.y; + double z = originalPos.z; + boolean transformed = true; + + switch (mirror) { + case LEFT_RIGHT -> z = 1.0D - z; + case FRONT_BACK -> x = 1.0D - x; + default -> transformed = false; + } + + return switch (rotation) { + case COUNTERCLOCKWISE_90 -> new Vec3(z, y, 1.0D - x); + case CLOCKWISE_90 -> new Vec3(1.0D - z, y, x); + case CLOCKWISE_180 -> new Vec3(1.0D - x, y, 1.0D - z); + default -> transformed ? new Vec3(x, y, z) : originalPos; + }; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java new file mode 100644 index 00000000..314a5065 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java @@ -0,0 +1,23 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +public enum ReplaceBehavior { + NONE("none"), + ALL("all"), + WITH_NON_AIR("with_non_air"); + + private final String configString; + + ReplaceBehavior(String configString) { + this.configString = configString; + } + + public static ReplaceBehavior fromStringStatic(String name) { + for (ReplaceBehavior val : ReplaceBehavior.values()) { + if (val.configString.equalsIgnoreCase(name)) { + return val; + } + } + + return ReplaceBehavior.NONE; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java new file mode 100644 index 00000000..5a108348 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java @@ -0,0 +1,148 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import javax.annotation.Nullable; + +public enum Schema { + // TODO --> Add Schema Versions to this as versions get released + // Minecraft Data Versions + SCHEMA_25W03A(4304, "25w03a"), // Entity Data Components ( https://www.minecraft.net/en-us/article/minecraft-snapshot-25w03a ) + SCHEMA_25W02A(4298, "25w02a"), + SCHEMA_1_21_04(4189, "1.21.4"), + SCHEMA_24W46A(4178, "24w46a"), + SCHEMA_24W44A(4174, "24w44a"), + SCHEMA_1_21_03(4082, "1.21.3"), + SCHEMA_1_21_02(4080, "1.21.2"), + SCHEMA_24W40A(4072, "24w40a"), + SCHEMA_24W37A(4065, "24w37a"), + SCHEMA_24W35A(4062, "24w35a"), + SCHEMA_24W33A(4058, "24w33a"), + SCHEMA_1_21_01(3955, "1.21.1"), + SCHEMA_1_21_00(3953, "1.21"), + SCHEMA_24W21A(3946, "24w21a"), + SCHEMA_24W18A(3940, "24w18a"), + SCHEMA_1_20_05(3837, "1.20.5"), + SCHEMA_24W14A(3827, "24w14a"), + SCHEMA_24W13A(3826, "24w13a"), + SCHEMA_24W12A(3824, "24w12a"), + SCHEMA_24W10A(3821, "24w10a"), + SCHEMA_24W09A(3819, "24w09a"), // Data Components ( https://minecraft.wiki/w/Data_component_format ) + SCHEMA_24W07A(3817, "24w07a"), + SCHEMA_24W03A(3804, "24w03a"), + SCHEMA_23W51A(3801, "23w51a"), + SCHEMA_1_20_04(3700, "1.20.4"), + SCHEMA_23W46A(3691, "23w46a"), + SCHEMA_23W43B(3687, "23w43b"), + SCHEMA_23W40A(3679, "23w40a"), + SCHEMA_1_20_02(3578, "1.20.2"), + SCHEMA_23W35A(3571, "23w35a"), + SCHEMA_23W31A(3567, "23w31a"), + SCHEMA_1_20_01(3465, "1.20.1"), + SCHEMA_1_20_00(3463, "1.20"), + SCHEMA_23W18A(3453, "23w18a"), + SCHEMA_23W16A(3449, "23w16a"), + SCHEMA_23W12A(3442, "23w12a"), + SCHEMA_1_19_04(3337, "1.19.4"), + SCHEMA_1_19_03(3218, "1.19.3"), + SCHEMA_1_19_02(3120, "1.19.2"), + SCHEMA_1_19_01(3117, "1.19.1"), + SCHEMA_1_19_00(3105, "1.19"), + SCHEMA_22W19A(3096, "22w19a"), + SCHEMA_22W16A(3091, "22w16a"), + SCHEMA_22W11A(3080, "22w11a"), + SCHEMA_1_18_02(2975, "1.18.2"), + SCHEMA_1_18_01(2865, "1.18.1"), + SCHEMA_1_18_00(2860, "1.18"), + SCHEMA_21W44A(2845, "21w44a"), + SCHEMA_21W41A(2839, "21w41a"), + SCHEMA_21W37A(2834, "21w37a"), + SCHEMA_1_17_01(2730, "1.17.1"), + SCHEMA_1_17_00(2724, "1.17"), + SCHEMA_21W20A(2715, "21w20a"), + SCHEMA_21W15A(2709, "21w15a"), + SCHEMA_21W10A(2699, "21w10a"), + SCHEMA_21W05A(2690, "21w05a"), + SCHEMA_20W49A(2685, "20w49a"), + SCHEMA_20W45A(2681, "20w45a"), + SCHEMA_1_16_05(2586, "1.16.5"), + SCHEMA_1_16_04(2584, "1.16.4"), + SCHEMA_1_16_03(2580, "1.16.3"), + SCHEMA_1_16_02(2578, "1.16.2"), + SCHEMA_1_16_01(2567, "1.16.1"), + SCHEMA_1_16_00(2566, "1.16"), + SCHEMA_20W22A(2555, "20w22a"), + SCHEMA_20W15A(2525, "20w15a"), + SCHEMA_20W06A(2504, "20w06a"), + SCHEMA_1_15_02(2230, "1.15.2"), + SCHEMA_1_15_01(2227, "1.15.1"), + SCHEMA_1_15_00(2225, "1.15"), + SCHEMA_19W46B(2217, "19w46b"), + SCHEMA_19W40A(2208, "19w40a"), + SCHEMA_19W34A(2200, "19w34a"), + SCHEMA_1_14_04(1976, "1.14.4"), + SCHEMA_1_14_03(1968, "1.14.3"), + SCHEMA_1_14_02(1963, "1.14.2"), + SCHEMA_1_14_01(1957, "1.14.1"), + SCHEMA_1_14_00(1952, "1.14"), + SCHEMA_19W14B(1945, "19w14b"), + SCHEMA_19W08B(1934, "19w08b"), + SCHEMA_18W50A(1919, "18w50a"), + SCHEMA_18W43A(1901, "18w43a"), + SCHEMA_1_13_02(1631, "1.13.2"), + SCHEMA_1_13_01(1628, "1.13.1"), + SCHEMA_1_13_00(1519, "1.13"), + SCHEMA_18W22C(1499, "18w22c"), + SCHEMA_18W14B(1481, "18w14b"), + SCHEMA_18W07C(1469, "18w07c"), + SCHEMA_17W50A(1457, "17w50a"), + SCHEMA_17W47A(1451, "17w47a"), // The Flattening ( https://minecraft.wiki/w/Java_Edition_1.13/Flattening ) + SCHEMA_17W46A(1449, "17w46a"), + SCHEMA_17W43A(1444, "17w43a"), + SCHEMA_1_12_02(1343, "1.12.2"), + SCHEMA_1_12_01(1241, "1.12.1"), + SCHEMA_1_12_00(1139, "1.12"), + SCHEMA_1_11_02(922, "1.11.2"), + SCHEMA_1_11_00(819, "1.11"), + SCHEMA_1_10_02(512, "1.10.2"), + SCHEMA_1_10_00(510, "1.10"), + SCHEMA_1_09_04(184, "1.9.4"), + SCHEMA_1_09_00(169, "1.9"), + SCHEMA_15W32A(100, "15w32a"); + + private final int schemaId; + private final String str; + + Schema(int id, String ver) { + this.schemaId = id; + this.str = ver; + } + + public int getDataVersion() { + return this.schemaId; + } + + public String getString() { + return this.str; + } + + /** + * Returns the Schema of the closest dataVersion, or below it. + * + * @param dataVersion (Schema ID) + * @return (Schema | null) + */ + public static @Nullable Schema getSchemaByDataVersion(int dataVersion) { + for (Schema schema : Schema.values()) { + if (schema.getDataVersion() <= dataVersion) { + return schema; + } + } + + return null; + } + + @Override + public String toString() { + return "MC: " + this.getString() + " [Schema: " + this.getDataVersion() + "]"; + } +} + diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java new file mode 100644 index 00000000..a2834138 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java @@ -0,0 +1,426 @@ +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Container; +import net.minecraft.world.entity.Display; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.decoration.Painting; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTicks; +import net.minecraft.world.ticks.ScheduledTick; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.LitematicaSchematic; +import org.leavesmc.leaves.protocol.servux.litematics.container.LitematicaBlockStateContainer; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SchematicPlacement; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SubRegionPlacement; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class SchematicPlacingUtils { + public static void placeToWorldWithinChunk( + Level world, + ChunkPos chunkPos, + SchematicPlacement schematicPlacement, + ReplaceBehavior replace, + boolean notifyNeighbors + ) { + LitematicaSchematic schematic = schematicPlacement.getSchematic(); + Set regionsTouchingChunk = schematicPlacement.getRegionsTouchingChunk(chunkPos.x, chunkPos.z); + BlockPos origin = schematicPlacement.getOrigin(); + + for (String regionName : regionsTouchingChunk) { + LitematicaSchematic.SubRegion subRegion = schematic.getSubRegion(regionName); + LitematicaBlockStateContainer container = subRegion.blockContainers(); + + if (container == null) { + continue; + } + + SubRegionPlacement placement = schematicPlacement.getRelativeSubRegionPlacement(regionName); + if (placement == null) { + ServuxProtocol.LOGGER.error("receiver a null placement for region: {}", regionName); + continue; + } + if (!placement.enabled()) { + continue; + } + Map blockEntityMap = subRegion.tileEntities(); + Map> scheduledBlockTicks = subRegion.pendingBlockTicks(); + Map> scheduledFluidTicks = subRegion.pendingFluidTicks(); + + if (!placeBlocksWithinChunk(world, chunkPos, regionName, container, blockEntityMap, + origin, schematicPlacement, placement, scheduledBlockTicks, scheduledFluidTicks, replace, notifyNeighbors)) { + ServuxProtocol.LOGGER.warn("Invalid/missing schematic data in schematic '{}' for sub-region '{}'", schematic.metadata().name(), regionName); + } + + List entityList = subRegion.entities(); + + if (!schematicPlacement.ignoreEntities() && + !placement.ignoreEntities() && entityList != null) { + placeEntitiesToWorldWithinChunk(world, chunkPos, entityList, origin, schematicPlacement, placement); + } + } + } + + public static boolean placeBlocksWithinChunk( + Level world, ChunkPos chunkPos, String regionName, + LitematicaBlockStateContainer container, + Map blockEntityMap, + BlockPos origin, + SchematicPlacement schematicPlacement, + SubRegionPlacement placement, + @Nullable Map> scheduledBlockTicks, + @Nullable Map> scheduledFluidTicks, + ReplaceBehavior replace, boolean notifyNeighbors + ) { + IntBoundingBox bounds = schematicPlacement.getBoxWithinChunkForRegion(regionName, chunkPos.x, chunkPos.z); + Vec3i regionSize = schematicPlacement.getSchematic().getSubRegion(regionName).size(); + + if (bounds == null || container == null || blockEntityMap == null || regionSize == null) { + return false; + } + + BlockPos regionPos = placement.pos(); + + // These are the untransformed relative positions + BlockPos posEndRel = (new BlockPos(PositionUtils.getRelativeEndPositionFromAreaSize(regionSize))).offset(regionPos); + BlockPos posMinRel = PositionUtils.getMinCorner(regionPos, posEndRel); + + // The transformed sub-region origin position + BlockPos regionPosTransformed = PositionUtils.getTransformedBlockPos(regionPos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + + // The relative offset of the affected region's corners, to the sub-region's origin corner + BlockPos boxMinRel = new BlockPos(bounds.minX() - origin.getX() - regionPosTransformed.getX(), 0, bounds.minZ() - origin.getZ() - regionPosTransformed.getZ()); + BlockPos boxMaxRel = new BlockPos(bounds.maxX() - origin.getX() - regionPosTransformed.getX(), 0, bounds.maxZ() - origin.getZ() - regionPosTransformed.getZ()); + + // Reverse transform that relative offset, to get the untransformed orientation's offsets + boxMinRel = PositionUtils.getReverseTransformedBlockPos(boxMinRel, placement.mirror(), placement.rotation()); + boxMaxRel = PositionUtils.getReverseTransformedBlockPos(boxMaxRel, placement.mirror(), placement.rotation()); + + boxMinRel = PositionUtils.getReverseTransformedBlockPos(boxMinRel, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + boxMaxRel = PositionUtils.getReverseTransformedBlockPos(boxMaxRel, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + + // Get the offset relative to the sub-region's minimum corner, instead of the origin corner (which can be at any corner) + boxMinRel = boxMinRel.subtract(posMinRel.subtract(regionPos)); + boxMaxRel = boxMaxRel.subtract(posMinRel.subtract(regionPos)); + + BlockPos posMin = PositionUtils.getMinCorner(boxMinRel, boxMaxRel); + BlockPos posMax = PositionUtils.getMaxCorner(boxMinRel, boxMaxRel); + + final int startX = posMin.getX(); + final int startZ = posMin.getZ(); + final int endX = posMax.getX(); + final int endZ = posMax.getZ(); + final int startY = 0; + final int endY = Math.abs(regionSize.getY()) - 1; + BlockPos.MutableBlockPos posMutable = new BlockPos.MutableBlockPos(); + if (startX < 0 || startZ < 0 || endX >= container.getSize().getX() || endZ >= container.getSize().getZ()) { + return false; + } + + final Rotation rotationCombined = schematicPlacement.getRotation().getRotated(placement.rotation()); + final Mirror mirrorMain = schematicPlacement.getMirror(); + final BlockState barrier = Blocks.BARRIER.defaultBlockState(); + Mirror mirrorSub = placement.mirror(); + final boolean ignoreInventories = false; + + if (mirrorSub != Mirror.NONE && + (schematicPlacement.getRotation() == Rotation.CLOCKWISE_90 || + schematicPlacement.getRotation() == Rotation.COUNTERCLOCKWISE_90)) { + mirrorSub = mirrorSub == Mirror.FRONT_BACK ? Mirror.LEFT_RIGHT : Mirror.FRONT_BACK; + } + + final int posMinRelMinusRegX = posMinRel.getX() - regionPos.getX(); + final int posMinRelMinusRegY = posMinRel.getY() - regionPos.getY(); + final int posMinRelMinusRegZ = posMinRel.getZ() - regionPos.getZ(); + + for (int y = startY; y <= endY; ++y) { + for (int z = startZ; z <= endZ; ++z) { + for (int x = startX; x <= endX; ++x) { + BlockState state = container.get(x, y, z); + + if (state.getBlock() == Blocks.STRUCTURE_VOID) { + continue; + } + + posMutable.set(x, y, z); + CompoundTag teNBT = blockEntityMap.get(posMutable); + + posMutable.set(posMinRelMinusRegX + x, + posMinRelMinusRegY + y, + posMinRelMinusRegZ + z); + + BlockPos pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + + BlockState stateOld = world.getBlockState(pos); + + if ((replace == ReplaceBehavior.NONE && !stateOld.isAir()) || + (replace == ReplaceBehavior.WITH_NON_AIR && state.isAir())) { + continue; + } + + if (mirrorMain != Mirror.NONE) { + state = state.mirror(mirrorMain); + } + if (mirrorSub != Mirror.NONE) { + state = state.mirror(mirrorSub); + } + if (rotationCombined != Rotation.NONE) { + state = state.rotate(rotationCombined); + } + + BlockEntity te = world.getBlockEntity(pos); + + if (te != null) { + if (te instanceof Container) { + ((Container) te).clearContent(); + } + + world.setBlock(pos, barrier, 4 | 16 | (notifyNeighbors ? 0 : 1024)); + } + + if (world.setBlock(pos, state, 2 | 16 | (notifyNeighbors ? 0 : 1024)) && teNBT != null) { + te = world.getBlockEntity(pos); + + if (te == null) { + continue; + } + teNBT = teNBT.copy(); + NbtUtils.writeBlockPosToTag(pos, teNBT); + + try { + te.loadWithComponents(teNBT, world.registryAccess().freeze()); + + } catch (Exception e) { + ServuxProtocol.LOGGER.warn("Failed to load BlockEntity data for {} @ {}", state, pos); + } + } + } + } + } + ServerLevel serverWorld = (ServerLevel) world; + IntBoundingBox box = new IntBoundingBox(startX, startY, startZ, endX, endY, endZ); + + if (scheduledBlockTicks != null && !scheduledBlockTicks.isEmpty()) { + LevelTicks scheduler = serverWorld.getBlockTicks(); + + for (Map.Entry> entry : scheduledBlockTicks.entrySet()) { + BlockPos pos = entry.getKey(); + + if (!box.containsPos(pos)) { + continue; + } + posMutable.set(posMinRelMinusRegX + pos.getX(), + posMinRelMinusRegY + pos.getY(), + posMinRelMinusRegZ + pos.getZ()); + + pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + ScheduledTick tick = entry.getValue(); + + if (world.getBlockState(pos).getBlock() == tick.type()) { + scheduler.schedule(new ScheduledTick<>(tick.type(), pos, tick.triggerTick(), tick.priority(), tick.subTickOrder())); + } + } + } + + if (scheduledFluidTicks != null && !scheduledFluidTicks.isEmpty()) { + LevelTicks scheduler = serverWorld.getFluidTicks(); + + for (Map.Entry> entry : scheduledFluidTicks.entrySet()) { + BlockPos pos = entry.getKey(); + + if (!box.containsPos(pos)) { + continue; + } + posMutable.set(posMinRelMinusRegX + pos.getX(), + posMinRelMinusRegY + pos.getY(), + posMinRelMinusRegZ + pos.getZ()); + + pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + ScheduledTick tick = entry.getValue(); + + if (world.getBlockState(pos).getFluidState().getType() == tick.type()) { + scheduler.schedule(new ScheduledTick<>(tick.type(), pos, tick.triggerTick(), tick.priority(), tick.subTickOrder())); + } + } + } + + if (!notifyNeighbors) { + return true; + } + for (int y = startY; y <= endY; ++y) { + for (int z = startZ; z <= endZ; ++z) { + for (int x = startX; x <= endX; ++x) { + posMutable.set(posMinRelMinusRegX + x, + posMinRelMinusRegY + y, + posMinRelMinusRegZ + z); + BlockPos pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + world.updateNeighborsAt(pos, world.getBlockState(pos).getBlock()); + } + } + } + + return true; + } + + public static void placeEntitiesToWorldWithinChunk( + Level world, ChunkPos chunkPos, + List entityList, + BlockPos origin, + SchematicPlacement schematicPlacement, + SubRegionPlacement placement + ) { + BlockPos regionPos = placement.pos(); + + if (entityList == null) { + return; + } + + BlockPos regionPosRelTransformed = PositionUtils.getTransformedBlockPos(regionPos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + final int offX = regionPosRelTransformed.getX() + origin.getX(); + final int offY = regionPosRelTransformed.getY() + origin.getY(); + final int offZ = regionPosRelTransformed.getZ() + origin.getZ(); + final double minX = (chunkPos.x << 4); + final double minZ = (chunkPos.z << 4); + final double maxX = (chunkPos.x << 4) + 16; + final double maxZ = (chunkPos.z << 4) + 16; + + final Rotation rotationCombined = schematicPlacement.getRotation().getRotated(placement.rotation()); + final Mirror mirrorMain = schematicPlacement.getMirror(); + Mirror mirrorSub = placement.mirror(); + + if (mirrorSub != Mirror.NONE && + (schematicPlacement.getRotation() == Rotation.CLOCKWISE_90 || + schematicPlacement.getRotation() == Rotation.COUNTERCLOCKWISE_90)) { + mirrorSub = mirrorSub == Mirror.FRONT_BACK ? Mirror.LEFT_RIGHT : Mirror.FRONT_BACK; + } + + for (LitematicaSchematic.EntityInfo info : entityList) { + Vec3 pos = info.posVec(); + pos = PositionUtils.getTransformedPosition(pos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + pos = PositionUtils.getTransformedPosition(pos, placement.mirror(), placement.rotation()); + double x = pos.x + offX; + double y = pos.y + offY; + double z = pos.z + offZ; + float[] origRot = new float[2]; + + if (!(x >= minX && x < maxX && z >= minZ && z < maxZ)) { + continue; + } + CompoundTag tag = info.nbt().copy(); + String id = tag.getString("id"); + + // Avoid warning about invalid hanging position. + // Note that this position isn't technically correct, but it only needs to be within 16 blocks + // of the entity position to avoid the warning. + if (id.equals("minecraft:glow_item_frame") || + id.equals("minecraft:item_frame") || + id.equals("minecraft:leash_knot") || + id.equals("minecraft:painting")) { + Vec3 p = NbtUtils.readEntityPositionFromTag(tag); + + if (p == null) { + p = new Vec3(x, y, z); + NbtUtils.writeEntityPositionToTag(p, tag); + } + + tag.putInt("TileX", (int) p.x); + tag.putInt("TileY", (int) p.y); + tag.putInt("TileZ", (int) p.z); + } + + ListTag rotation = tag.getList("Rotation", Tag.TAG_FLOAT); + origRot[0] = rotation.getFloat(0); + origRot[1] = rotation.getFloat(1); + + Entity entity = EntityUtils.createEntityAndPassengersFromNBT(tag, world); + + if (entity == null) { + continue; + } + rotateEntity(entity, x, y, z, rotationCombined, mirrorMain, mirrorSub); + + // Update the sleeping position to the current position + if (entity instanceof LivingEntity living && living.isSleeping()) { + living.setSleepingPos(BlockPos.containing(x, y, z)); + } + + // Hack fix to fix the painting position offsets. + // The vanilla code will end up moving the position by one in two of the orientations, + // because it sets the hanging position to the given position (floored) + // and then it offsets the position from the hanging position + // by 0.5 or 1.0 blocks depending on the painting size. + if (entity instanceof Painting paintingEntity) { + Direction right = PositionUtils.rotateYCounterclockwise(paintingEntity.getDirection()); + + if ((paintingEntity.getVariant().value().width() % 2) == 0 && + right.getAxisDirection() == Direction.AxisDirection.POSITIVE) { + x -= 1.0 * right.getStepX(); + z -= 1.0 * right.getStepZ(); + } + + if ((paintingEntity.getVariant().value().height() % 2) == 0) { + y -= 1.0; + } + + entity.teleportTo(x, y, z); + } + if (entity instanceof ItemFrame frameEntity) { + if (frameEntity.getYRot() != origRot[0] && (frameEntity.getXRot() == 90.0F || frameEntity.getXRot() == -90.0F)) { + // Fix Yaw only if Pitch is +/- 90.0F (Floor, Ceiling mounted) + frameEntity.setYRot(origRot[0]); + } + } + + EntityUtils.spawnEntityAndPassengersInWorld(entity, world); + + if (entity instanceof Display) { + entity.tick(); // Required to set the full data for rendering + } + } + } + + public static void rotateEntity( + Entity entity, double x, double y, double z, + Rotation rotationCombined, Mirror mirrorMain, Mirror mirrorSub + ) { + float rotationYaw = entity.getYRot(); + + if (mirrorMain != Mirror.NONE) { + rotationYaw = entity.mirror(mirrorMain); + } + if (mirrorSub != Mirror.NONE) { + rotationYaw = entity.mirror(mirrorSub); + } + if (rotationCombined != Rotation.NONE) { + rotationYaw += entity.getYRot() - entity.rotate(rotationCombined); + } + + entity.absMoveTo(x, y, z, rotationYaw, entity.getXRot()); + EntityUtils.setEntityRotations(entity, rotationYaw, entity.getXRot()); + } +} \ No newline at end of file