9
0
mirror of https://github.com/LeavesMC/Leaves.git synced 2025-12-19 14:59:32 +00:00

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>
This commit is contained in:
wzp
2025-04-22 17:53:02 +08:00
committed by GitHub
parent 4b77c25f30
commit 359e1be5e6
24 changed files with 2610 additions and 1 deletions

View File

@@ -34,6 +34,7 @@ subprojects {
options.encoding = Charsets.UTF_8.name()
options.release = 21
options.isFork = true
options.forkOptions.memoryMaximumSize = "6g"
}
tasks.withType<Javadoc> {
options.encoding = Charsets.UTF_8.name()

View File

@@ -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

View File

@@ -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")

View File

@@ -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")

View File

@@ -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<String, SubRegion> subRegions, SchematicMetadata metadata) {
public static final int MINECRAFT_DATA_VERSION = SharedConstants.getProtocolVersion();
public static final int SCHEMATIC_VERSION = 7;
@NotNull
@Unmodifiable
public Map<String, BlockPos> getAreaSizes() {
ImmutableMap.Builder<String, BlockPos> builder = ImmutableMap.builderWithExpectedSize(subRegions.size());
for (Map.Entry<String, SubRegion> 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<String, SubRegion> 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<String, SubRegion> readSubRegionsFromNBT(@NotNull CompoundTag tag, int version, int minecraftDataVersion) {
Map<String, SubRegion> 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<BlockPos, CompoundTag> tileEntities,
Map<BlockPos, ScheduledTick<Block>> pendingBlockTicks,
Map<BlockPos, ScheduledTick<Fluid>> pendingFluidTicks,
List<EntityInfo> 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<BlockPos, CompoundTag> tileEntities;
List<EntityInfo> 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<BlockPos, ScheduledTick<Block>> pendingBlockTicks = null;
if (version >= 3) {
pendingBlockTicks = readPendingTicksFromNBT(regionTag.getList("PendingBlockTicks", Tag.TAG_COMPOUND), BuiltInRegistries.BLOCK, "Block", Blocks.AIR);
}
Map<BlockPos, ScheduledTick<Fluid>> 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<EntityInfo> readEntitiesFromNBT(ListTag tagList) {
List<EntityInfo> 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<BlockPos, CompoundTag> readTileEntitiesFromNBT(ListTag tagList) {
Map<BlockPos, CompoundTag> 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 <T> Map<BlockPos, ScheduledTick<T>> readPendingTicksFromNBT(ListTag tagList, Registry<T> registry, String tagName, T emptyValue) {
Map<BlockPos, ScheduledTick<T>> 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<Holder.Reference<T>> 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<EntityInfo> readEntitiesFromNBT_v1(ListTag tagList) {
List<EntityInfo> 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<BlockPos, CompoundTag> readTileEntitiesFromNBT_v1(ListTag tagList) {
Map<BlockPos, CompoundTag> 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<UUID, Long> 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<BlockPos> teSet = chunk.getBlockEntitiesPos();
List<Entity> 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<Integer, ServuxLitematicaPayloadType> 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<ServuxLitematicaPayload> {
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;
}
}
}

View File

@@ -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 (<b>not</b> 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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<BlockState> 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<Block> 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;
}
}

View File

@@ -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<Block> 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;
}
}

View File

@@ -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<String, SubRegionPlacement> 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<String, Box> getSubRegionBoxes(SubRegionPlacement.RequiredEnabled required) {
ImmutableMap.Builder<String, Box> builder = ImmutableMap.builder();
Map<String, BlockPos> areaSizes = this.schematic.getAreaSizes();
for (Map.Entry<String, SubRegionPlacement> 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<String, Box> 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<String, Box> getSubRegionBoxFor(String regionName, SubRegionPlacement.RequiredEnabled required) {
SubRegionPlacement placement = this.relativeSubRegionPlacements.get(regionName);
if (placement == null) return ImmutableMap.of();
ImmutableMap.Builder<String, Box> builder = ImmutableMap.builder();
Map<String, BlockPos> 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<String> getRegionsTouchingChunk(int chunkX, int chunkZ) {
ImmutableMap<String, Box> 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<String> set = new HashSet<>();
for (Map.Entry<String, Box> 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<String, Box> 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<ChunkPos> 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)
);
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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<Entity> 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<Entity> 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;
}
}
}

View File

@@ -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
}

View File

@@ -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<K> implements Iterable<K> {
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 <A> Int2ObjectBiMap<A> 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<K> 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<K> 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<K> 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
};
}
}

View File

@@ -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;
}
}

View File

@@ -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() + "]";
}
}

View File

@@ -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<String> 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<BlockPos, CompoundTag> blockEntityMap = subRegion.tileEntities();
Map<BlockPos, ScheduledTick<Block>> scheduledBlockTicks = subRegion.pendingBlockTicks();
Map<BlockPos, ScheduledTick<Fluid>> 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<LitematicaSchematic.EntityInfo> 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<BlockPos, CompoundTag> blockEntityMap,
BlockPos origin,
SchematicPlacement schematicPlacement,
SubRegionPlacement placement,
@Nullable Map<BlockPos, ScheduledTick<Block>> scheduledBlockTicks,
@Nullable Map<BlockPos, ScheduledTick<Fluid>> 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<Block> scheduler = serverWorld.getBlockTicks();
for (Map.Entry<BlockPos, ScheduledTick<Block>> 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<Block> 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<Fluid> scheduler = serverWorld.getFluidTicks();
for (Map.Entry<BlockPos, ScheduledTick<Fluid>> 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<Fluid> 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<LitematicaSchematic.EntityInfo> 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());
}
}