3
0
Mirror von https://github.com/Moulberry/AxiomPaperPlugin.git synchronisiert 2024-11-14 12:10:05 +01:00

Implement Server Blueprints

Dieser Commit ist enthalten in:
Moulberry 2024-02-22 18:18:53 +08:00
Ursprung b4757985de
Commit 4338fbd446
15 geänderte Dateien mit 808 neuen und 18 gelöschten Zeilen

Datei anzeigen

@ -1,6 +1,7 @@
package com.moulberry.axiom; package com.moulberry.axiom;
import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.RateLimiter;
import com.moulberry.axiom.blueprint.ServerBlueprintManager;
import com.moulberry.axiom.buffer.CompressedBlockEntity; import com.moulberry.axiom.buffer.CompressedBlockEntity;
import com.moulberry.axiom.event.AxiomCreateWorldPropertiesEvent; import com.moulberry.axiom.event.AxiomCreateWorldPropertiesEvent;
import com.moulberry.axiom.event.AxiomModifyWorldEvent; import com.moulberry.axiom.event.AxiomModifyWorldEvent;
@ -35,6 +36,9 @@ import org.bukkit.plugin.messaging.Messenger;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -50,6 +54,8 @@ public class AxiomPaper extends JavaPlugin implements Listener {
public IdMapper<BlockState> allowedBlockRegistry = null; public IdMapper<BlockState> allowedBlockRegistry = null;
private boolean logLargeBlockBufferChanges = false; private boolean logLargeBlockBufferChanges = false;
public Path blueprintFolder = null;
@Override @Override
public void onEnable() { public void onEnable() {
PLUGIN = this; PLUGIN = this;
@ -132,9 +138,13 @@ public class AxiomPaper extends JavaPlugin implements Listener {
if (configuration.getBoolean("packet-handlers.marker-nbt-request")) { if (configuration.getBoolean("packet-handlers.marker-nbt-request")) {
msg.registerIncomingPluginChannel(this, "axiom:marker_nbt_request", new MarkerNbtRequestPacketListener(this)); msg.registerIncomingPluginChannel(this, "axiom:marker_nbt_request", new MarkerNbtRequestPacketListener(this));
} }
if (configuration.getBoolean("packet-handlers.blueprint-request")) {
msg.registerIncomingPluginChannel(this, "axiom:request_blueprint", new BlueprintRequestPacketListener(this));
}
if (configuration.getBoolean("packet-handlers.set-buffer")) { if (configuration.getBoolean("packet-handlers.set-buffer")) {
SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this);
UploadBlueprintPacketListener uploadBlueprintPacketListener = new UploadBlueprintPacketListener(this);
ChannelInitializeListenerHolder.addListener(Key.key("axiom:handle_big_payload"), new ChannelInitializeListener() { ChannelInitializeListenerHolder.addListener(Key.key("axiom:handle_big_payload"), new ChannelInitializeListener() {
@Override @Override
@ -153,11 +163,20 @@ public class AxiomPaper extends JavaPlugin implements Listener {
Connection connection = (Connection) channel.pipeline().get("packet_handler"); Connection connection = (Connection) channel.pipeline().get("packet_handler");
channel.pipeline().addBefore("decoder", "axiom-big-payload-handler", channel.pipeline().addBefore("decoder", "axiom-big-payload-handler",
new AxiomBigPayloadHandler(payloadId, connection, setBlockBufferPacketListener)); new AxiomBigPayloadHandler(payloadId, connection, setBlockBufferPacketListener,
uploadBlueprintPacketListener));
} }
}); });
} }
if (this.configuration.getBoolean("blueprint-sharing")) {
this.blueprintFolder = this.getDataFolder().toPath().resolve("blueprints");
try {
Files.createDirectories(this.blueprintFolder);
} catch (IOException ignored) {}
ServerBlueprintManager.initialize(this.blueprintFolder);
}
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> {
HashSet<UUID> stillActiveAxiomPlayers = new HashSet<>(); HashSet<UUID> stillActiveAxiomPlayers = new HashSet<>();

Datei anzeigen

@ -0,0 +1,25 @@
package com.moulberry.axiom.blueprint;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import java.util.HashMap;
import java.util.Map;
public class BlockEntityMap {
private static final Map<Block, BlockEntityType<?>> blockBlockEntityTypeMap = new HashMap<>();
static {
for (BlockEntityType<?> blockEntityType : BuiltInRegistries.BLOCK_ENTITY_TYPE) {
for (Block validBlock : blockEntityType.validBlocks) {
blockBlockEntityTypeMap.put(validBlock, blockEntityType);
}
}
}
public static BlockEntityType<?> get(Block block) {
return blockBlockEntityTypeMap.get(block);
}
}

Datei anzeigen

@ -0,0 +1,65 @@
package com.moulberry.axiom.blueprint;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.FriendlyByteBuf;
import java.util.ArrayList;
import java.util.List;
public record BlueprintHeader(String name, String author, List<String> tags, float thumbnailYaw, float thumbnailPitch, boolean lockedThumbnail, int blockCount) {
private static final int CURRENT_VERSION = 0;
public void write(FriendlyByteBuf friendlyByteBuf) {
friendlyByteBuf.writeUtf(this.name);
friendlyByteBuf.writeUtf(this.author);
friendlyByteBuf.writeCollection(this.tags, FriendlyByteBuf::writeUtf);
friendlyByteBuf.writeInt(this.blockCount);
}
public static BlueprintHeader read(FriendlyByteBuf friendlyByteBuf) {
String name = friendlyByteBuf.readUtf();
String author = friendlyByteBuf.readUtf();
List<String> tags = friendlyByteBuf.readList(FriendlyByteBuf::readUtf);
int blockCount = friendlyByteBuf.readInt();
return new BlueprintHeader(name, author, tags, 0, 0, true, blockCount);
}
public static BlueprintHeader load(CompoundTag tag) {
long version = tag.getLong("Version");
String name = tag.getString("Name");
String author = tag.getString("Author");
float thumbnailYaw = tag.contains("ThumbnailYaw", Tag.TAG_FLOAT) ? tag.getFloat("ThumbnailYaw") : 135;
float thumbnailPitch = tag.contains("ThumbnailPitch", Tag.TAG_FLOAT) ? tag.getFloat("ThumbnailPitch") : 30;
boolean lockedThumbnail = tag.getBoolean("LockedThumbnail");
int blockCount = tag.getInt("BlockCount");
List<String> tags = new ArrayList<>();
for (Tag string : tag.getList("Tags", Tag.TAG_STRING)) {
tags.add(string.getAsString());
}
return new BlueprintHeader(name, author, tags, thumbnailYaw, thumbnailPitch, lockedThumbnail, blockCount);
}
public CompoundTag save(CompoundTag tag) {
ListTag listTag = new ListTag();
for (String string : this.tags) {
listTag.add(StringTag.valueOf(string));
}
tag.putLong("Version", CURRENT_VERSION);
tag.putString("Name", this.name);
tag.putString("Author", this.author);
tag.put("Tags", listTag);
tag.putFloat("ThumbnailYaw", this.thumbnailYaw);
tag.putFloat("ThumbnailPitch", this.thumbnailPitch);
tag.putBoolean("LockedThumbnail", this.lockedThumbnail);
tag.putInt("BlockCount", this.blockCount);
return tag;
}
}

Datei anzeigen

@ -0,0 +1,249 @@
package com.moulberry.axiom.blueprint;
import com.mojang.serialization.Codec;
import com.mojang.serialization.Dynamic;
import com.moulberry.axiom.buffer.CompressedBlockEntity;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.util.datafix.fixes.References;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.PalettedContainer;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
public class BlueprintIo {
private static final int MAGIC = 0xAE5BB36;
private static final IOException NOT_VALID_BLUEPRINT = new IOException("Not a valid Blueprint");
public static BlueprintHeader readHeader(InputStream inputStream) throws IOException {
if (inputStream.available() < 4) throw NOT_VALID_BLUEPRINT;
DataInputStream dataInputStream = new DataInputStream(inputStream);
int magic = dataInputStream.readInt();
if (magic != MAGIC) throw NOT_VALID_BLUEPRINT;
dataInputStream.readInt(); // Ignore header length
CompoundTag headerTag = NbtIo.read(dataInputStream);
return BlueprintHeader.load(headerTag);
}
public static RawBlueprint readRawBlueprint(InputStream inputStream) throws IOException {
if (inputStream.available() < 4) throw NOT_VALID_BLUEPRINT;
DataInputStream dataInputStream = new DataInputStream(inputStream);
int magic = dataInputStream.readInt();
if (magic != MAGIC) throw NOT_VALID_BLUEPRINT;
// Header
dataInputStream.readInt(); // Ignore header length
CompoundTag headerTag = NbtIo.read(dataInputStream);
BlueprintHeader header = BlueprintHeader.load(headerTag);
// Thumbnail
int thumbnailLength = dataInputStream.readInt();
byte[] thumbnailBytes = dataInputStream.readNBytes(thumbnailLength);
if (thumbnailBytes.length < thumbnailLength) throw NOT_VALID_BLUEPRINT;
int currentDataVersion = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
// Block data
dataInputStream.readInt(); // Ignore block data length
CompoundTag blockDataTag = NbtIo.readCompressed(dataInputStream);
int blueprintDataVersion = blockDataTag.getInt("DataVersion");
if (blueprintDataVersion == 0) blueprintDataVersion = currentDataVersion;
ListTag listTag = blockDataTag.getList("BlockRegion", Tag.TAG_COMPOUND);
Long2ObjectMap<PalettedContainer<BlockState>> blockMap = readBlocks(listTag, blueprintDataVersion);
// Block Entities
ListTag blockEntitiesTag = blockDataTag.getList("BlockEntities", Tag.TAG_COMPOUND);
Long2ObjectMap<CompressedBlockEntity> blockEntities = new Long2ObjectOpenHashMap<>();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (Tag tag : blockEntitiesTag) {
CompoundTag blockEntityCompound = (CompoundTag) tag;
// Data Fix
if (blueprintDataVersion != currentDataVersion) {
Dynamic<Tag> dynamic = new Dynamic<>(NbtOps.INSTANCE, blockEntityCompound);
Dynamic<Tag> output = DataFixers.getDataFixer().update(References.BLOCK_ENTITY, dynamic,
blueprintDataVersion, currentDataVersion);
blockEntityCompound = (CompoundTag) output.getValue();
}
BlockPos blockPos = BlockEntity.getPosFromTag(blockEntityCompound);
long pos = blockPos.asLong();
String id = blockEntityCompound.getString("id");
BlockEntityType<?> type = BuiltInRegistries.BLOCK_ENTITY_TYPE.get(new ResourceLocation(id));
if (type != null) {
PalettedContainer<BlockState> container = blockMap.get(BlockPos.asLong(
blockPos.getX() >> 4,
blockPos.getY() >> 4,
blockPos.getZ() >> 4
));
BlockState blockState = container.get(blockPos.getX() & 0xF, blockPos.getY() & 0xF, blockPos.getZ() & 0xF);
if (type.isValid(blockState)) {
CompoundTag newTag = blockEntityCompound.copy();
newTag.remove("x");
newTag.remove("y");
newTag.remove("z");
newTag.remove("id");
CompressedBlockEntity compressedBlockEntity = CompressedBlockEntity.compress(newTag, baos);
blockEntities.put(pos, compressedBlockEntity);
}
}
}
return new RawBlueprint(header, thumbnailBytes, blockMap, blockEntities);
}
public static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC,
PalettedContainer.Strategy.SECTION_STATES, Blocks.STRUCTURE_VOID.defaultBlockState());
public static Long2ObjectMap<PalettedContainer<BlockState>> readBlocks(ListTag list, int dataVersion) {
Long2ObjectMap<PalettedContainer<BlockState>> map = new Long2ObjectOpenHashMap<>();
for (Tag tag : list) {
if (tag instanceof CompoundTag compoundTag) {
int cx = compoundTag.getInt("X");
int cy = compoundTag.getInt("Y");
int cz = compoundTag.getInt("Z");
CompoundTag blockStates = compoundTag.getCompound("BlockStates");
blockStates = DFUHelper.updatePalettedContainer(blockStates, dataVersion);
PalettedContainer<BlockState> container = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, blockStates)
.getOrThrow(false, err -> {});
map.put(BlockPos.asLong(cx, cy, cz), container);
}
}
return map;
}
public static void writeHeader(Path inPath, Path outPath, BlueprintHeader newHeader) throws IOException {
byte[] thumbnailAndBlockBytes;
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(inPath))) {
if (inputStream.available() < 4) throw NOT_VALID_BLUEPRINT;
DataInputStream dataInputStream = new DataInputStream(inputStream);
int magic = dataInputStream.readInt();
if (magic != MAGIC) throw NOT_VALID_BLUEPRINT;
// Header
int headerLength = dataInputStream.readInt(); // Ignore header length
if (dataInputStream.skip(headerLength) < headerLength) throw NOT_VALID_BLUEPRINT;
thumbnailAndBlockBytes = dataInputStream.readAllBytes();
}
try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(outPath))) {
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeInt(MAGIC);
// Write header
CompoundTag headerTag = newHeader.save(new CompoundTag());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (DataOutputStream os = new DataOutputStream(baos)) {
NbtIo.write(headerTag, os);
}
dataOutputStream.writeInt(baos.size());
baos.writeTo(dataOutputStream);
// Copy remaining bytes
dataOutputStream.write(thumbnailAndBlockBytes);
}
}
public static void writeRaw(OutputStream outputStream, RawBlueprint rawBlueprint) throws IOException {
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeInt(MAGIC);
// Write header
CompoundTag headerTag = rawBlueprint.header().save(new CompoundTag());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (DataOutputStream os = new DataOutputStream(baos)) {
NbtIo.write(headerTag, os);
}
dataOutputStream.writeInt(baos.size());
baos.writeTo(dataOutputStream);
// Write thumbnail
dataOutputStream.writeInt(rawBlueprint.thumbnail().length);
dataOutputStream.write(rawBlueprint.thumbnail());
// Write block data
CompoundTag compound = new CompoundTag();
ListTag savedBlockRegions = new ListTag();
for (Long2ObjectMap.Entry<PalettedContainer<BlockState>> entry : rawBlueprint.blocks().long2ObjectEntrySet()) {
long pos = entry.getLongKey();
PalettedContainer<BlockState> container = entry.getValue();
int cx = BlockPos.getX(pos);
int cy = BlockPos.getY(pos);
int cz = BlockPos.getZ(pos);
CompoundTag tag = new CompoundTag();
tag.putInt("X", cx);
tag.putInt("Y", cy);
tag.putInt("Z", cz);
Tag encoded = BlueprintIo.BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, container)
.getOrThrow(false, err -> {});
tag.put("BlockStates", encoded);
savedBlockRegions.add(tag);
}
compound.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
compound.put("BlockRegion", savedBlockRegions);
// Write Block Entities
ListTag blockEntitiesTag = new ListTag();
rawBlueprint.blockEntities().forEach((pos, compressedBlockEntity) -> {
int x = BlockPos.getX(pos);
int y = BlockPos.getY(pos);
int z = BlockPos.getZ(pos);
PalettedContainer<BlockState> container = rawBlueprint.blocks().get(BlockPos.asLong(x >> 4,y >> 4, z >> 4));
BlockState blockState = container.get(x & 0xF, y & 0xF, z & 0xF);
BlockEntityType<?> type = BlockEntityMap.get(blockState.getBlock());
if (type == null) return;
ResourceLocation resourceLocation = BlockEntityType.getKey(type);
if (resourceLocation != null) {
CompoundTag tag = compressedBlockEntity.decompress();
tag.putInt("x", x);
tag.putInt("y", y);
tag.putInt("z", z);
tag.putString("id", resourceLocation.toString());
blockEntitiesTag.add(tag);
}
});
compound.put("BlockEntities", blockEntitiesTag);
baos.reset();
NbtIo.writeCompressed(compound, baos);
dataOutputStream.writeInt(baos.size());
baos.writeTo(dataOutputStream);
}
}

Datei anzeigen

@ -0,0 +1,45 @@
package com.moulberry.axiom.blueprint;
import com.mojang.serialization.Dynamic;
import net.minecraft.SharedConstants;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.util.datafix.fixes.References;
public class DFUHelper {
private static final int DATA_VERSION = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
public static CompoundTag updatePalettedContainer(CompoundTag tag, int fromVersion) {
if (!hasExpectedPaletteTag(tag)) {
return tag;
}
if (fromVersion == DATA_VERSION) return tag;
tag = tag.copy();
ListTag newPalette = new ListTag();
for (Tag entry : tag.getList("palette", Tag.TAG_COMPOUND)) {
Dynamic<Tag> dynamic = new Dynamic<>(NbtOps.INSTANCE, entry);
Dynamic<Tag> output = DataFixers.getDataFixer().update(References.BLOCK_STATE, dynamic, fromVersion, DATA_VERSION);
newPalette.add(output.getValue());
}
tag.put("palette", newPalette);
return tag;
}
private static boolean hasExpectedPaletteTag(CompoundTag tag) {
if (!tag.contains("palette", Tag.TAG_LIST)) return false;
ListTag listTag = (ListTag) tag.get("palette");
if (listTag == null) return false;
return listTag.isEmpty() || listTag.getElementType() == Tag.TAG_COMPOUND;
}
}

Datei anzeigen

@ -0,0 +1,84 @@
package com.moulberry.axiom.blueprint;
import com.moulberry.axiom.buffer.CompressedBlockEntity;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.PalettedContainer;
public record RawBlueprint(BlueprintHeader header, byte[] thumbnail, Long2ObjectMap<PalettedContainer<BlockState>> blocks,
Long2ObjectMap<CompressedBlockEntity> blockEntities) {
public static void writeHeader(FriendlyByteBuf friendlyByteBuf, RawBlueprint rawBlueprint) {
rawBlueprint.header.write(friendlyByteBuf);
friendlyByteBuf.writeByteArray(rawBlueprint.thumbnail);
}
public static RawBlueprint readHeader(FriendlyByteBuf friendlyByteBuf) {
BlueprintHeader header = BlueprintHeader.read(friendlyByteBuf);
byte[] thumbnail = friendlyByteBuf.readByteArray();
return new RawBlueprint(header, thumbnail, null, null);
}
public static void write(FriendlyByteBuf friendlyByteBuf, RawBlueprint rawBlueprint) {
rawBlueprint.header.write(friendlyByteBuf);
friendlyByteBuf.writeByteArray(rawBlueprint.thumbnail);
LongSet chunkKeys = rawBlueprint.blocks.keySet();
friendlyByteBuf.writeVarInt(chunkKeys.size());
LongIterator longIterator = chunkKeys.longIterator();
while (longIterator.hasNext()) {
long pos = longIterator.nextLong();
friendlyByteBuf.writeLong(pos);
rawBlueprint.blocks.get(pos).write(friendlyByteBuf);
}
LongSet blockEntityKeys = rawBlueprint.blockEntities.keySet();
friendlyByteBuf.writeVarInt(blockEntityKeys.size());
longIterator = blockEntityKeys.longIterator();
while (longIterator.hasNext()) {
long pos = longIterator.nextLong();
friendlyByteBuf.writeLong(pos);
rawBlueprint.blockEntities.get(pos).write(friendlyByteBuf);
}
}
public static RawBlueprint read(FriendlyByteBuf friendlyByteBuf) {
BlueprintHeader header = BlueprintHeader.read(friendlyByteBuf);
byte[] thumbnail = friendlyByteBuf.readByteArray();
Long2ObjectMap<PalettedContainer<BlockState>> blocks = new Long2ObjectOpenHashMap<>();
int chunkCount = friendlyByteBuf.readVarInt();
for (int i = 0; i < chunkCount; i++) {
long pos = friendlyByteBuf.readLong();
PalettedContainer<BlockState> palettedContainer = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY,
Blocks.STRUCTURE_VOID.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
palettedContainer.read(friendlyByteBuf);
blocks.put(pos, palettedContainer);
}
Long2ObjectMap<CompressedBlockEntity> blockEntities = new Long2ObjectOpenHashMap<>();
int blockEntityCount = friendlyByteBuf.readVarInt();
for (int i = 0; i < blockEntityCount; i++) {
long pos = friendlyByteBuf.readLong();
CompressedBlockEntity compressedBlockEntity = CompressedBlockEntity.read(friendlyByteBuf);
blockEntities.put(pos, compressedBlockEntity);
}
return new RawBlueprint(header, thumbnail, blocks, blockEntities);
}
}

Datei anzeigen

@ -0,0 +1,107 @@
package com.moulberry.axiom.blueprint;
import com.moulberry.axiom.AxiomPaper;
import com.moulberry.axiom.packet.CustomByteArrayPayload;
import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class ServerBlueprintManager {
private static ServerBlueprintRegistry registry = null;
public static void initialize(Path blueprintDirectory) {
Map<String, RawBlueprint> map = new HashMap<>();
loadRegistryFromFolder(map, blueprintDirectory, "/");
registry = new ServerBlueprintRegistry(map);
}
private static final int MAX_SIZE = 1000000;
private static final ResourceLocation PACKET_BLUEPRINT_MANIFEST_IDENTIFIER = new ResourceLocation("axiom:blueprint_manifest");
public static void sendManifest(List<ServerPlayer> serverPlayers) {
if (registry != null) {
List<ServerPlayer> sendTo = new ArrayList<>();
for (ServerPlayer serverPlayer : serverPlayers) {
CraftPlayer craftPlayer = serverPlayer.getBukkitEntity();
if (AxiomPaper.PLUGIN.canUseAxiom(craftPlayer) &&
craftPlayer.getListeningPluginChannels().contains("axiom:blueprint_manifest")) {
sendTo.add(serverPlayer);
}
}
if (sendTo.isEmpty()) return;
FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
buf.writeBoolean(true); // replace
for (Map.Entry<String, RawBlueprint> entry : registry.blueprints().entrySet()) {
buf.writeUtf(entry.getKey());
RawBlueprint.writeHeader(buf, entry.getValue());
if (buf.writerIndex() > MAX_SIZE) {
// Finish and send current packet
buf.writeUtf("");
byte[] bytes = new byte[buf.writerIndex()];
buf.getBytes(0, bytes);
var payload = new CustomByteArrayPayload(PACKET_BLUEPRINT_MANIFEST_IDENTIFIER, bytes);
for (ServerPlayer serverPlayer : sendTo) {
serverPlayer.connection.send(new ClientboundCustomPayloadPacket(payload));
}
// Continue
buf = new FriendlyByteBuf(Unpooled.buffer());
buf.writeBoolean(false); // don't replace
}
}
buf.writeUtf("");
byte[] bytes = new byte[buf.writerIndex()];
buf.getBytes(0, bytes);
var payload = new CustomByteArrayPayload(PACKET_BLUEPRINT_MANIFEST_IDENTIFIER, bytes);
for (ServerPlayer serverPlayer : sendTo) {
serverPlayer.connection.send(new ClientboundCustomPayloadPacket(payload));
}
}
}
public static ServerBlueprintRegistry getRegistry() {
return registry;
}
private static void loadRegistryFromFolder(Map<String, RawBlueprint> map, Path folder, String location) {
if (!Files.isDirectory(folder)) {
return;
}
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(folder)) {
for (Path path : directoryStream) {
String filename = path.getFileName().toString();
if (filename.endsWith(".bp")) {
try {
RawBlueprint rawBlueprint = BlueprintIo.readRawBlueprint(new BufferedInputStream(Files.newInputStream(path)));
String newLocation = location + filename.substring(0, filename.length()-3);
map.put(newLocation, rawBlueprint);
} catch (Exception e) {
e.printStackTrace();
}
} else if (Files.isDirectory(path)) {
String newLocation = location + filename + "/";
loadRegistryFromFolder(map, path, newLocation);
}
}
} catch (IOException ignored) {}
}
}

Datei anzeigen

@ -0,0 +1,32 @@
package com.moulberry.axiom.blueprint;
import net.minecraft.network.FriendlyByteBuf;
import java.util.HashMap;
import java.util.Map;
public record ServerBlueprintRegistry(Map<String, RawBlueprint> blueprints) {
public void writeManifest(FriendlyByteBuf friendlyByteBuf) {
for (Map.Entry<String, RawBlueprint> entry : this.blueprints.entrySet()) {
friendlyByteBuf.writeUtf(entry.getKey());
RawBlueprint.writeHeader(friendlyByteBuf, entry.getValue());
}
friendlyByteBuf.writeUtf("");
}
public static ServerBlueprintRegistry readManifest(FriendlyByteBuf friendlyByteBuf) {
Map<String, RawBlueprint> blueprints = new HashMap<>();
while (true) {
String path = friendlyByteBuf.readUtf();
if (path.isEmpty()) {
return new ServerBlueprintRegistry(blueprints);
}
blueprints.put(path, RawBlueprint.readHeader(friendlyByteBuf));
}
}
}

Datei anzeigen

@ -19,7 +19,7 @@ public record MarkerData(UUID uuid, Vec3 position, @Nullable String name, @Nulla
int lineArgb, float lineThickness, int faceArgb) { int lineArgb, float lineThickness, int faceArgb) {
public static MarkerData read(FriendlyByteBuf friendlyByteBuf) { public static MarkerData read(FriendlyByteBuf friendlyByteBuf) {
UUID uuid = friendlyByteBuf.readUUID(); UUID uuid = friendlyByteBuf.readUUID();
Vec3 position = friendlyByteBuf.readVec3(); Vec3 position = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble());
String name = friendlyByteBuf.readNullable(FriendlyByteBuf::readUtf); String name = friendlyByteBuf.readNullable(FriendlyByteBuf::readUtf);
Vec3 minRegion = null; Vec3 minRegion = null;
@ -31,8 +31,8 @@ public record MarkerData(UUID uuid, Vec3 position, @Nullable String name, @Nulla
byte flags = friendlyByteBuf.readByte(); byte flags = friendlyByteBuf.readByte();
if (flags != 0) { if (flags != 0) {
minRegion = friendlyByteBuf.readVec3(); minRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble());
maxRegion = friendlyByteBuf.readVec3(); maxRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble());
if ((flags & 2) != 0) { if ((flags & 2) != 0) {
lineArgb = friendlyByteBuf.readInt(); lineArgb = friendlyByteBuf.readInt();

Datei anzeigen

@ -1,5 +1,6 @@
package com.moulberry.axiom.packet; package com.moulberry.axiom.packet;
import com.moulberry.axiom.AxiomPaper;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
@ -14,14 +15,18 @@ import java.util.List;
public class AxiomBigPayloadHandler extends ByteToMessageDecoder { public class AxiomBigPayloadHandler extends ByteToMessageDecoder {
private static final ResourceLocation SET_BUFFER = new ResourceLocation("axiom", "set_buffer"); private static final ResourceLocation SET_BUFFER = new ResourceLocation("axiom", "set_buffer");
private static final ResourceLocation UPLOAD_BLUEPRINT = new ResourceLocation("axiom", "upload_blueprint");
private final int payloadId; private final int payloadId;
private final Connection connection; private final Connection connection;
private final SetBlockBufferPacketListener listener; private final SetBlockBufferPacketListener setBlockBuffer;
private final UploadBlueprintPacketListener uploadBlueprint;
public AxiomBigPayloadHandler(int payloadId, Connection connection, SetBlockBufferPacketListener listener) { public AxiomBigPayloadHandler(int payloadId, Connection connection, SetBlockBufferPacketListener setBlockBuffer,
UploadBlueprintPacketListener uploadBlueprint) {
this.payloadId = payloadId; this.payloadId = payloadId;
this.connection = connection; this.connection = connection;
this.listener = listener; this.setBlockBuffer = setBlockBuffer;
this.uploadBlueprint = uploadBlueprint;
} }
@Override @Override
@ -44,12 +49,19 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder {
ResourceLocation identifier = buf.readResourceLocation(); ResourceLocation identifier = buf.readResourceLocation();
if (identifier.equals(SET_BUFFER)) { if (identifier.equals(SET_BUFFER)) {
ServerPlayer player = connection.getPlayer(); ServerPlayer player = connection.getPlayer();
if (player != null && player.getBukkitEntity().hasPermission("axiom.*")) { if (AxiomPaper.PLUGIN.canUseAxiom(player.getBukkitEntity())) {
if (listener.onReceive(player, buf)) { setBlockBuffer.onReceive(player, buf);
success = true; success = true;
in.skipBytes(in.readableBytes()); in.skipBytes(in.readableBytes());
return; return;
} }
} else if (identifier.equals(UPLOAD_BLUEPRINT)) {
ServerPlayer player = connection.getPlayer();
if (AxiomPaper.PLUGIN.canUseAxiom(player.getBukkitEntity())) {
uploadBlueprint.onReceive(player, buf);
success = true;
in.skipBytes(in.readableBytes());
return;
} }
} }
} }
@ -74,7 +86,7 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder {
if (evt == ConnectionEvent.COMPRESSION_THRESHOLD_SET || evt == ConnectionEvent.COMPRESSION_DISABLED) { if (evt == ConnectionEvent.COMPRESSION_THRESHOLD_SET || evt == ConnectionEvent.COMPRESSION_DISABLED) {
ctx.channel().pipeline().remove("axiom-big-payload-handler"); ctx.channel().pipeline().remove("axiom-big-payload-handler");
ctx.channel().pipeline().addBefore("decoder", "axiom-big-payload-handler", ctx.channel().pipeline().addBefore("decoder", "axiom-big-payload-handler",
new AxiomBigPayloadHandler(payloadId, connection, listener)); new AxiomBigPayloadHandler(payloadId, connection, setBlockBuffer, uploadBlueprint));
} }
super.userEventTriggered(ctx, evt); super.userEventTriggered(ctx, evt);
} }

Datei anzeigen

@ -0,0 +1,62 @@
package com.moulberry.axiom.packet;
import com.moulberry.axiom.AxiomPaper;
import com.moulberry.axiom.blueprint.RawBlueprint;
import com.moulberry.axiom.blueprint.ServerBlueprintManager;
import com.moulberry.axiom.blueprint.ServerBlueprintRegistry;
import com.moulberry.axiom.marker.MarkerData;
import io.netty.buffer.Unpooled;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Marker;
import org.bukkit.craftbukkit.v1_20_R2.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class BlueprintRequestPacketListener implements PluginMessageListener {
private final AxiomPaper plugin;
public BlueprintRequestPacketListener(AxiomPaper plugin) {
this.plugin = plugin;
}
private static final ResourceLocation RESPONSE_PACKET_IDENTIFIER = new ResourceLocation("axiom:response_blueprint");
@Override
public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) {
if (!this.plugin.canUseAxiom(player)) {
return;
}
FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message));
String path = friendlyByteBuf.readUtf();
ServerBlueprintRegistry registry = ServerBlueprintManager.getRegistry();
if (registry == null) {
return;
}
RawBlueprint rawBlueprint = registry.blueprints().get(path);
if (rawBlueprint != null) {
FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
buf.writeUtf(path);
RawBlueprint.write(buf, rawBlueprint);
byte[] bytes = new byte[buf.writerIndex()];
buf.getBytes(0, bytes);
var payload = new CustomByteArrayPayload(RESPONSE_PACKET_IDENTIFIER, bytes);
((CraftPlayer)player).getHandle().connection.send(new ClientboundCustomPayloadPacket(payload));
}
}
}

Datei anzeigen

@ -5,6 +5,7 @@ import com.moulberry.axiom.AxiomConstants;
import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.AxiomPaper;
import com.moulberry.axiom.View; import com.moulberry.axiom.View;
import com.moulberry.axiom.WorldExtension; import com.moulberry.axiom.WorldExtension;
import com.moulberry.axiom.blueprint.ServerBlueprintManager;
import com.moulberry.axiom.event.AxiomHandshakeEvent; import com.moulberry.axiom.event.AxiomHandshakeEvent;
import com.moulberry.axiom.persistence.ItemStackDataType; import com.moulberry.axiom.persistence.ItemStackDataType;
import com.moulberry.axiom.persistence.UUIDDataType; import com.moulberry.axiom.persistence.UUIDDataType;
@ -17,6 +18,7 @@ import net.minecraft.network.FriendlyByteBuf;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftItemStack; import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftItemStack;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@ -25,6 +27,7 @@ import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.messaging.PluginMessageListener; import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -170,6 +173,8 @@ public class HelloPacketListener implements PluginMessageListener {
} }
WorldExtension.onPlayerJoin(world, player); WorldExtension.onPlayerJoin(world, player);
ServerBlueprintManager.sendManifest(List.of(((CraftPlayer)player).getHandle()));
} }
} }

Datei anzeigen

@ -68,9 +68,9 @@ public class SetBlockBufferPacketListener {
} }
} }
public boolean onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) { public void onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) {
MinecraftServer server = player.getServer(); MinecraftServer server = player.getServer();
if (server == null) return false; if (server == null) return;
ResourceKey<Level> worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); ResourceKey<Level> worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION);
friendlyByteBuf.readUUID(); // Discard, we don't need to associate buffers friendlyByteBuf.readUUID(); // Discard, we don't need to associate buffers
@ -116,8 +116,6 @@ public class SetBlockBufferPacketListener {
} else { } else {
throw new RuntimeException("Unknown buffer type: " + type); throw new RuntimeException("Unknown buffer type: " + type);
} }
return true;
} }
private void applyBlockBuffer(ServerPlayer player, MinecraftServer server, BlockBuffer buffer, ResourceKey<Level> worldKey) { private void applyBlockBuffer(ServerPlayer player, MinecraftServer server, BlockBuffer buffer, ResourceKey<Level> worldKey) {

Datei anzeigen

@ -0,0 +1,83 @@
package com.moulberry.axiom.packet;
import com.moulberry.axiom.AxiomPaper;
import com.moulberry.axiom.blueprint.BlueprintIo;
import com.moulberry.axiom.blueprint.RawBlueprint;
import com.moulberry.axiom.blueprint.ServerBlueprintManager;
import com.moulberry.axiom.blueprint.ServerBlueprintRegistry;
import com.moulberry.axiom.marker.MarkerData;
import io.netty.buffer.Unpooled;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Marker;
import org.bukkit.craftbukkit.v1_20_R2.CraftServer;
import org.bukkit.craftbukkit.v1_20_R2.CraftWorld;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
public class UploadBlueprintPacketListener {
private final AxiomPaper plugin;
public UploadBlueprintPacketListener(AxiomPaper plugin) {
this.plugin = plugin;
}
public void onReceive(ServerPlayer serverPlayer, FriendlyByteBuf friendlyByteBuf) {
if (!this.plugin.canUseAxiom(serverPlayer.getBukkitEntity())) {
return;
}
ServerBlueprintRegistry registry = ServerBlueprintManager.getRegistry();
if (registry == null || this.plugin.blueprintFolder == null) {
return;
}
String pathStr = friendlyByteBuf.readUtf();
RawBlueprint rawBlueprint = RawBlueprint.read(friendlyByteBuf);
pathStr = pathStr.replace("\\", "/");
if (!pathStr.endsWith(".bp") || pathStr.contains("..") || !pathStr.startsWith("/")) {
return;
}
pathStr = pathStr.substring(1);
Path relative = Path.of(pathStr).normalize();
if (relative.isAbsolute()) {
return;
}
Path path = this.plugin.blueprintFolder.resolve(relative);
// Write file
try {
Files.createDirectories(path.getParent());
} catch (IOException e) {
return;
}
try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(path))) {
BlueprintIo.writeRaw(outputStream, rawBlueprint);
} catch (IOException e) {
return;
}
// Update registry
registry.blueprints().put("/" + pathStr.substring(0, pathStr.length()-3), rawBlueprint);
// Resend manifest
ServerBlueprintManager.sendManifest(serverPlayer.getServer().getPlayerList().getPlayers());
}
}

Datei anzeigen

@ -11,6 +11,9 @@ max-chunk-load-distance: 128
# Whether players are allowed to teleport between worlds using views # Whether players are allowed to teleport between worlds using views
allow-teleport-between-worlds: true allow-teleport-between-worlds: true
# Whether to allow clients to save/load/share blueprints through the server
blueprint-sharing: false
# Action to take when a user with an incompatible Minecraft version or Axiom version joins # Action to take when a user with an incompatible Minecraft version or Axiom version joins
# Valid actions are 'kick', 'warn' and 'ignore' # Valid actions are 'kick', 'warn' and 'ignore'
# 'warn' will give the player a warning and disable Axiom # 'warn' will give the player a warning and disable Axiom
@ -76,3 +79,4 @@ packet-handlers:
manipulate-entity: true manipulate-entity: true
delete-entity: true delete-entity: true
marker-nbt-request: true marker-nbt-request: true
blueprint-request: true