From 1c3cb9b77dfe91df3dda8572dec2e758b3d04ea7 Mon Sep 17 00:00:00 2001 From: Nanit Date: Mon, 24 Jan 2022 20:57:38 +0200 Subject: [PATCH] Optimized packet snapshot. Readers closing. Restructured schematic classes --- .../ru/nanit/limbo/protocol/ByteMessage.java | 12 ++- .../nanit/limbo/protocol/PacketSnapshot.java | 32 ++++++-- .../java/ru/nanit/limbo/world/BlockMap.java | 34 -------- .../ru/nanit/limbo/world/BlockMappings.java | 68 ++++++++++++++++ .../world/dimension/DimensionRegistry.java | 12 ++- .../world/schematic/AbstractSchematic.java | 73 ++++++++++++++++++ .../limbo/world/schematic/Schematic.java | 3 +- .../world/schematic/SchematicLoader.java | 17 ++-- .../schematic/versions/LegacySchematic.java | 61 +++------------ .../schematic/versions/SpongeSchematic.java | 62 +++++---------- src/test/java/SchematicTest.java | 7 +- src/test/resources/spawn.schem | Bin 0 -> 6609 bytes 12 files changed, 227 insertions(+), 154 deletions(-) delete mode 100644 src/main/java/ru/nanit/limbo/world/BlockMap.java create mode 100644 src/main/java/ru/nanit/limbo/world/BlockMappings.java create mode 100644 src/main/java/ru/nanit/limbo/world/schematic/AbstractSchematic.java create mode 100644 src/test/resources/spawn.schem diff --git a/src/main/java/ru/nanit/limbo/protocol/ByteMessage.java b/src/main/java/ru/nanit/limbo/protocol/ByteMessage.java index e3550c3..49facd1 100644 --- a/src/main/java/ru/nanit/limbo/protocol/ByteMessage.java +++ b/src/main/java/ru/nanit/limbo/protocol/ByteMessage.java @@ -160,9 +160,7 @@ public class ByteMessage extends ByteBuf { } public void writeCompoundTagArray(CompoundBinaryTag[] compoundTags) { - try { - ByteBufOutputStream stream = new ByteBufOutputStream(buf); - + try (ByteBufOutputStream stream = new ByteBufOutputStream(buf)) { writeVarInt(compoundTags.length); for (CompoundBinaryTag tag : compoundTags) { @@ -174,16 +172,16 @@ public class ByteMessage extends ByteBuf { } public CompoundBinaryTag readCompoundTag() { - try { - return BinaryTagIO.reader().read((InputStream) new ByteBufInputStream(buf)); + try (ByteBufInputStream stream = new ByteBufInputStream(buf)) { + return BinaryTagIO.reader().read((InputStream) stream); } catch (IOException thrown) { throw new DecoderException("Cannot read NBT CompoundTag"); } } public void writeCompoundTag(CompoundBinaryTag compoundTag) { - try { - BinaryTagIO.writer().write(compoundTag, (OutputStream) new ByteBufOutputStream(buf)); + try (ByteBufOutputStream stream = new ByteBufOutputStream(buf)) { + BinaryTagIO.writer().write(compoundTag, (OutputStream) stream); } catch (IOException e) { throw new EncoderException("Cannot write NBT CompoundTag"); } diff --git a/src/main/java/ru/nanit/limbo/protocol/PacketSnapshot.java b/src/main/java/ru/nanit/limbo/protocol/PacketSnapshot.java index 8034c16..602e908 100644 --- a/src/main/java/ru/nanit/limbo/protocol/PacketSnapshot.java +++ b/src/main/java/ru/nanit/limbo/protocol/PacketSnapshot.java @@ -22,14 +22,19 @@ import ru.nanit.limbo.protocol.registry.Version; import java.util.HashMap; import java.util.Map; +/** + * PacketSnapshot encodes packet to byt array for each MC version. + * Some versions have same bytes snapshot, so there are mappings + * to avoid storing same byte array for different versions + */ public class PacketSnapshot implements PacketOut { private final PacketOut packet; - private final Map versionMessages; + private final Map versionMessages = new HashMap<>(); + private final Map mappings = new HashMap<>(); public PacketSnapshot(PacketOut packet) { this.packet = packet; - this.versionMessages = new HashMap<>(); } public PacketOut getWrappedPacket() { @@ -37,11 +42,24 @@ public class PacketSnapshot implements PacketOut { } public PacketSnapshot encodePacket() { + Map hashes = new HashMap<>(); + for (Version version : Version.values()) { + if (version.equals(Version.UNDEFINED)) continue; + ByteMessage encodedMessage = ByteMessage.create(); packet.encode(encodedMessage, version); - byte[] message = encodedMessage.toByteArray(); - versionMessages.put(version, message); + + int hash = encodedMessage.hashCode(); + Version hashed = hashes.get(hash); + + if (hashed != null) { + mappings.put(version, hashed); + continue; + } + + hashes.put(hash, version); + versionMessages.put(version, encodedMessage.toByteArray()); encodedMessage.release(); } @@ -50,11 +68,11 @@ public class PacketSnapshot implements PacketOut { @Override public void encode(ByteMessage msg, Version version) { - byte[] message = versionMessages.get(version); + Version mapped = mappings.get(version); + byte[] message = versionMessages.get(mapped); - if (message != null) { + if (message != null) msg.writeBytes(message); - } } @Override diff --git a/src/main/java/ru/nanit/limbo/world/BlockMap.java b/src/main/java/ru/nanit/limbo/world/BlockMap.java deleted file mode 100644 index e69a981..0000000 --- a/src/main/java/ru/nanit/limbo/world/BlockMap.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2020 Nan1t - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package ru.nanit.limbo.world; - -import ru.nanit.limbo.protocol.registry.Version; - -public final class BlockMap { - - public BlockData convert(int id, byte data, Version version) { - // TODO - return null; - } - - public BlockData convert(String state, Version version) { - // TODO - return null; - } - -} diff --git a/src/main/java/ru/nanit/limbo/world/BlockMappings.java b/src/main/java/ru/nanit/limbo/world/BlockMappings.java new file mode 100644 index 0000000..0f5360e --- /dev/null +++ b/src/main/java/ru/nanit/limbo/world/BlockMappings.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 Nan1t + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.nanit.limbo.world; + +import ru.nanit.limbo.protocol.registry.Version; + +import java.util.HashMap; +import java.util.Map; + +public final class BlockMappings { + + private final Map idToState = new HashMap<>(); + private final Map stateToId = new HashMap<>(); + + public BlockData convert(int id, byte data, Version version) { + if (version.less(Version.V1_13)) + return new BlockData(id, data); + + String state = idToState.get(toId(id, data)); + + return state != null ? new BlockData(state) : null; + } + + public BlockData convert(String state, Version version) { + if (state == null) return null; + + if (version.moreOrEqual(Version.V1_13)) { + return new BlockData(state); + } + + String id = stateToId.get(state); + + if (id != null) { + String[] arr = id.split(":"); + int blockId = Integer.parseInt(arr[0]); + byte data = Byte.parseByte(arr[1]); + + return new BlockData(blockId, data); + } + + return null; + } + + public void register(int id, byte data, String state) { + String strId = toId(id, data); + idToState.put(strId, state); + stateToId.put(state, strId); + } + + private String toId(int id, byte data) { + return id + ":" + data; + } +} diff --git a/src/main/java/ru/nanit/limbo/world/dimension/DimensionRegistry.java b/src/main/java/ru/nanit/limbo/world/dimension/DimensionRegistry.java index e428032..60c07b3 100644 --- a/src/main/java/ru/nanit/limbo/world/dimension/DimensionRegistry.java +++ b/src/main/java/ru/nanit/limbo/world/dimension/DimensionRegistry.java @@ -89,9 +89,15 @@ public final class DimensionRegistry { return TagStringIO.get().asCompound(streamToString(in)); } - private String streamToString(InputStream in) { - return new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) - .lines() + private String streamToString(InputStream in) throws IOException { + InputStreamReader isReader = new InputStreamReader(in, StandardCharsets.UTF_8); + BufferedReader bufReader = new BufferedReader(isReader); + String content = bufReader.lines() .collect(Collectors.joining("\n")); + + isReader.close(); + bufReader.close(); + + return content; } } diff --git a/src/main/java/ru/nanit/limbo/world/schematic/AbstractSchematic.java b/src/main/java/ru/nanit/limbo/world/schematic/AbstractSchematic.java new file mode 100644 index 0000000..24ccf1e --- /dev/null +++ b/src/main/java/ru/nanit/limbo/world/schematic/AbstractSchematic.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 Nan1t + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.nanit.limbo.world.schematic; + +import ru.nanit.limbo.world.BlockEntity; +import ru.nanit.limbo.world.BlockMappings; + +import java.util.List; + +public abstract class AbstractSchematic implements Schematic { + + protected final BlockMappings mappings; + + private int width; + private int height; + private int length; + private List blockEntities; + + public AbstractSchematic(BlockMappings mappings) { + this.mappings = mappings; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public int getLength() { + return length; + } + + @Override + public List getBlockEntities() { + return blockEntities; + } + + public void setWidth(int width) { + this.width = width; + } + + public void setHeight(int height) { + this.height = height; + } + + public void setLength(int length) { + this.length = length; + } + + public void setBlockEntities(List blockEntities) { + this.blockEntities = blockEntities; + } +} diff --git a/src/main/java/ru/nanit/limbo/world/schematic/Schematic.java b/src/main/java/ru/nanit/limbo/world/schematic/Schematic.java index 548bbdf..24d0102 100644 --- a/src/main/java/ru/nanit/limbo/world/schematic/Schematic.java +++ b/src/main/java/ru/nanit/limbo/world/schematic/Schematic.java @@ -20,7 +20,6 @@ package ru.nanit.limbo.world.schematic; import ru.nanit.limbo.protocol.registry.Version; import ru.nanit.limbo.world.BlockData; import ru.nanit.limbo.world.BlockEntity; -import ru.nanit.limbo.world.BlockMap; import ru.nanit.limbo.world.Location; import java.util.List; @@ -35,6 +34,6 @@ public interface Schematic { List getBlockEntities(); - BlockData getBlock(Location loc, Version version, BlockMap mappings); + BlockData getBlock(Location loc, Version version); } diff --git a/src/main/java/ru/nanit/limbo/world/schematic/SchematicLoader.java b/src/main/java/ru/nanit/limbo/world/schematic/SchematicLoader.java index 591f58f..70596a0 100644 --- a/src/main/java/ru/nanit/limbo/world/schematic/SchematicLoader.java +++ b/src/main/java/ru/nanit/limbo/world/schematic/SchematicLoader.java @@ -21,6 +21,7 @@ import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; import ru.nanit.limbo.world.BlockEntity; +import ru.nanit.limbo.world.BlockMappings; import ru.nanit.limbo.world.Location; import ru.nanit.limbo.world.schematic.versions.LegacySchematic; import ru.nanit.limbo.world.schematic.versions.SpongeSchematic; @@ -36,6 +37,12 @@ import java.util.Map; public class SchematicLoader { + private final BlockMappings mappings; + + public SchematicLoader(BlockMappings mappings) { + this.mappings = mappings; + } + public Schematic load(Path file) throws IOException { return load(Files.newInputStream(file)); } @@ -43,16 +50,16 @@ public class SchematicLoader { public Schematic load(InputStream stream) throws IOException { CompoundBinaryTag nbt = BinaryTagIO.unlimitedReader().read(stream, BinaryTagIO.Compression.GZIP); - if (nbt.getCompound("BlockData") == CompoundBinaryTag.empty()) { - return loadLegacy(nbt); - } else { + if (nbt.keySet().contains("BlockData")) { return loadSponge(nbt); + } else { + return loadLegacy(nbt); } } // Specification: https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-2.md private Schematic loadSponge(CompoundBinaryTag nbt) { - SpongeSchematic schematic = new SpongeSchematic(); + SpongeSchematic schematic = new SpongeSchematic(mappings); schematic.setDataVersion(nbt.getInt("DataVersion")); @@ -93,7 +100,7 @@ public class SchematicLoader { } private Schematic loadLegacy(CompoundBinaryTag nbt) { - LegacySchematic schematic = new LegacySchematic(); + LegacySchematic schematic = new LegacySchematic(mappings); schematic.setWidth(nbt.getShort("Width")); schematic.setHeight(nbt.getShort("Height")); diff --git a/src/main/java/ru/nanit/limbo/world/schematic/versions/LegacySchematic.java b/src/main/java/ru/nanit/limbo/world/schematic/versions/LegacySchematic.java index 9a35a8e..70614b6 100644 --- a/src/main/java/ru/nanit/limbo/world/schematic/versions/LegacySchematic.java +++ b/src/main/java/ru/nanit/limbo/world/schematic/versions/LegacySchematic.java @@ -19,68 +19,33 @@ package ru.nanit.limbo.world.schematic.versions; import ru.nanit.limbo.protocol.registry.Version; import ru.nanit.limbo.world.BlockData; -import ru.nanit.limbo.world.BlockEntity; -import ru.nanit.limbo.world.BlockMap; +import ru.nanit.limbo.world.BlockMappings; import ru.nanit.limbo.world.Location; -import ru.nanit.limbo.world.schematic.Schematic; - -import java.util.List; +import ru.nanit.limbo.world.schematic.AbstractSchematic; /** * Legacy schematic format (1.12-) */ -public class LegacySchematic implements Schematic { +public class LegacySchematic extends AbstractSchematic { - private short width; - private short height; - private short length; private Materials materials; private byte[] blocks; private byte[] addBlocks; private byte[] data; - private List blockEntities; - @Override - public int getWidth() { - return width; + public LegacySchematic(BlockMappings mappings) { + super(mappings); } @Override - public int getHeight() { - return height; - } - - @Override - public int getLength() { - return length; - } - - @Override - public List getBlockEntities() { - return blockEntities; - } - - @Override - public BlockData getBlock(Location loc, Version version, BlockMap mappings) { - int index = (loc.getBlockY() * length + loc.getBlockZ()) * width + loc.getBlockX(); + public BlockData getBlock(Location loc, Version version) { + int index = (loc.getBlockY() * getLength() + loc.getBlockZ()) * getWidth() + loc.getBlockX(); byte id = this.blocks[index]; byte data = this.data[index]; return mappings.convert(id, data, version); } - public void setWidth(short width) { - this.width = width; - } - - public void setHeight(short height) { - this.height = height; - } - - public void setLength(short length) { - this.length = length; - } - public void setMaterials(Materials materials) { this.materials = materials; } @@ -97,21 +62,17 @@ public class LegacySchematic implements Schematic { this.data = data; } - public void setBlockEntities(List blockEntities) { - this.blockEntities = blockEntities; - } - @Override public String toString() { return "Schematic{" + - "width=" + width + - ", height=" + height + - ", length=" + length + + "width=" + getWidth() + + ", height=" + getHeight() + + ", length=" + getLength() + ", materials=" + materials + ", blocks length=" + blocks.length + ", addBlocks length=" + addBlocks.length + ", data length=" + data.length + - ", blockEntities=" + blockEntities + + ", blockEntities=" + getBlockEntities() + '}'; } diff --git a/src/main/java/ru/nanit/limbo/world/schematic/versions/SpongeSchematic.java b/src/main/java/ru/nanit/limbo/world/schematic/versions/SpongeSchematic.java index 5fc7b89..bc6010c 100644 --- a/src/main/java/ru/nanit/limbo/world/schematic/versions/SpongeSchematic.java +++ b/src/main/java/ru/nanit/limbo/world/schematic/versions/SpongeSchematic.java @@ -19,51 +19,29 @@ package ru.nanit.limbo.world.schematic.versions; import ru.nanit.limbo.protocol.registry.Version; import ru.nanit.limbo.world.BlockData; -import ru.nanit.limbo.world.BlockEntity; -import ru.nanit.limbo.world.BlockMap; +import ru.nanit.limbo.world.BlockMappings; import ru.nanit.limbo.world.Location; -import ru.nanit.limbo.world.schematic.Schematic; +import ru.nanit.limbo.world.schematic.AbstractSchematic; -import java.util.List; import java.util.Map; /** * Modern schematic format (Sponge second specification) */ -public class SpongeSchematic implements Schematic { +public class SpongeSchematic extends AbstractSchematic { private int dataVersion; - private int width; - private int height; - private int length; private int paletteMax; private Map palette; private byte[] blockData; - private List blockEntities; - @Override - public int getWidth() { - return width; + public SpongeSchematic(BlockMappings mappings) { + super(mappings); } @Override - public int getHeight() { - return height; - } - - @Override - public int getLength() { - return length; - } - - @Override - public List getBlockEntities() { - return blockEntities; - } - - @Override - public BlockData getBlock(Location loc, Version version, BlockMap mappings) { - int index = loc.getBlockX() + loc.getBlockZ() * width + loc.getBlockY() * width * length; + public BlockData getBlock(Location loc, Version version) { + int index = loc.getBlockX() + loc.getBlockZ() * getWidth() + loc.getBlockY() * getWidth() * getLength(); int id = blockData[index]; String state = palette.get(id); return mappings.convert(state, version); @@ -73,18 +51,6 @@ public class SpongeSchematic implements Schematic { this.dataVersion = dataVersion; } - public void setWidth(int width) { - this.width = width; - } - - public void setHeight(int height) { - this.height = height; - } - - public void setLength(int length) { - this.length = length; - } - public void setPaletteMax(int paletteMax) { this.paletteMax = paletteMax; } @@ -97,7 +63,17 @@ public class SpongeSchematic implements Schematic { this.blockData = blockData; } - public void setBlockEntities(List blockEntities) { - this.blockEntities = blockEntities; + @Override + public String toString() { + return "SpongeSchematic{" + + "dataVersion=" + dataVersion + + ", width=" + getWidth() + + ", height=" + getHeight() + + ", length=" + getLength() + + ", paletteMax=" + paletteMax + + ", palette=" + palette + + ", blockData bytes=" + blockData.length + + ", blockEntities=" + getBlockEntities() + + '}'; } } diff --git a/src/test/java/SchematicTest.java b/src/test/java/SchematicTest.java index a23a054..157e434 100644 --- a/src/test/java/SchematicTest.java +++ b/src/test/java/SchematicTest.java @@ -16,6 +16,7 @@ */ import org.junit.jupiter.api.Test; +import ru.nanit.limbo.world.BlockMappings; import ru.nanit.limbo.world.schematic.SchematicLoader; import java.io.IOException; @@ -25,10 +26,10 @@ public class SchematicTest { @Test public void testLoading() throws IOException { - SchematicLoader loader = new SchematicLoader(); - InputStream stream = getClass().getResourceAsStream("test.schematic"); + BlockMappings mappings = new BlockMappings(); + SchematicLoader loader = new SchematicLoader(mappings); + InputStream stream = getClass().getResourceAsStream("spawn.schem"); System.out.println(loader.load(stream)); } - } diff --git a/src/test/resources/spawn.schem b/src/test/resources/spawn.schem new file mode 100644 index 0000000000000000000000000000000000000000..0a800b50ce1b8fed7f9291330f03d7d4f3c24c9a GIT binary patch literal 6609 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 literal 0 HcmV?d00001