diff --git a/README.MD b/README.MD index 71023a3..4f0e682 100644 --- a/README.MD +++ b/README.MD @@ -1,13 +1,18 @@ # MC-CORE -![version: v0.1](https://img.shields.io/badge/version-v0.1-0b0.svg?style=flat) -![codename: ZERO](https://img.shields.io/badge/codename-ZERO-000.svg?style=flat) +![version: v0.2](https://img.shields.io/badge/version-v0.2-000.svg?style=flat) +![codename: LIMBO](https://img.shields.io/badge/codename-LIMBO-B00.svg?style=flat) Модульный **Minecraft** сервер. ## Модули * **Core** - ядро сервера +* **Proto 1.12.2** - описание протокола версии [1.12.2 (340)](https://wiki.vg/index.php?title=Protocol&oldid=14204) +* **Proto 1.12.2 Netty** - реализация протокола на сетевом движке [Netty.IO](https://netty.io/) +* **H2 Player manager** - хранение данных игроков в [H2 Database](http://www.h2database.com/) +* **Simple world** - реализация простго генератора плоского (flat) мира +* **Anvil loader** - загрузчик "ванильных" (vanilla, ["Anvil"](https://minecraft.gamepedia.com/Anvil_file_format)) карт Minecraft ## Сборка diff --git a/anvil-loader/README.MD b/anvil-loader/README.MD new file mode 100644 index 0000000..a3751b9 --- /dev/null +++ b/anvil-loader/README.MD @@ -0,0 +1,5 @@ +# Anvil loader + +Загрузчик "ванильных" (vanilla, ["Anvil"](https://minecraft.gamepedia.com/Anvil_file_format)) карт Minecraft. + +Пример настройки можно посмотреть в файле `sample-config.xml` diff --git a/anvil-loader/build.gradle b/anvil-loader/build.gradle new file mode 100644 index 0000000..eb29847 --- /dev/null +++ b/anvil-loader/build.gradle @@ -0,0 +1,8 @@ +version '0.1' + +dependencies { + /* Core */ + compile_excludeCopy project(':core') + + compile (group: 'net.sf.trove4j', name: 'trove4j', version: '3.0.3') +} \ No newline at end of file diff --git a/anvil-loader/sample-config.xml b/anvil-loader/sample-config.xml new file mode 100644 index 0000000..e2a0636 --- /dev/null +++ b/anvil-loader/sample-config.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/anvil-loader/src/main/java/com/flowpowered/nbt/LongArrayTag.java b/anvil-loader/src/main/java/com/flowpowered/nbt/LongArrayTag.java new file mode 100644 index 0000000..47b09ee --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/LongArrayTag.java @@ -0,0 +1,73 @@ +package com.flowpowered.nbt; + +import java.util.Arrays; + +public class LongArrayTag extends Tag { + /** + * The value. + */ + private final long[] value; + + /** + * Creates the tag. + * + * @param name The name. + * @param value The value. + */ + public LongArrayTag(String name, long[] value) { + super(TagType.TAG_LONG_ARRAY, name); + this.value = value; + } + + @Override + public long[] getValue() { + return value; + } + + @Override + public String toString() { + StringBuilder hex = new StringBuilder(); + for (long s : value) { + String hexDigits = Long.toHexString(s).toUpperCase(); + if (hexDigits.length() == 1) { + hex.append("0"); + } + hex.append(hexDigits).append(" "); + } + + String name = getName(); + String append = ""; + if (name != null && !name.equals("")) { + append = "(\"" + this.getName() + "\")"; + } + return "TAG_Long_Array" + append + ": " + hex.toString(); + } + + @Override + public LongArrayTag clone() { + long[] clonedArray = cloneArray(value); + + return new LongArrayTag(getName(), clonedArray); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof IntArrayTag)) { + return false; + } + + LongArrayTag tag = (LongArrayTag) other; + return Arrays.equals(value, tag.value) && getName().equals(tag.getName()); + } + + private long[] cloneArray(long[] longArray) { + if (longArray == null) { + return null; + } else { + int length = longArray.length; + byte[] newArray = new byte[length]; + System.arraycopy(longArray, 0, newArray, 0, length); + return longArray; + } + } +} diff --git a/anvil-loader/src/main/java/com/flowpowered/nbt/NBTConstants.java b/anvil-loader/src/main/java/com/flowpowered/nbt/NBTConstants.java new file mode 100644 index 0000000..f6dd1b4 --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/NBTConstants.java @@ -0,0 +1,38 @@ +package com.flowpowered.nbt; + +import java.nio.charset.Charset; + +/** + * A class which holds constant values. + */ +public final class NBTConstants { + /** + * The character set used by NBT (UTF-8). + */ + public static final Charset CHARSET = Charset.forName("UTF-8"); + /** + * Tag type constants. + */ + @Deprecated + public static final int TYPE_END = TagType.TAG_END.getId(), + TYPE_BYTE = TagType.TAG_BYTE.getId(), + TYPE_SHORT = TagType.TAG_SHORT.getId(), + TYPE_INT = TagType.TAG_INT.getId(), + TYPE_LONG = TagType.TAG_LONG.getId(), + TYPE_FLOAT = TagType.TAG_FLOAT.getId(), + TYPE_DOUBLE = TagType.TAG_DOUBLE.getId(), + TYPE_BYTE_ARRAY = TagType.TAG_BYTE_ARRAY.getId(), + TYPE_STRING = TagType.TAG_STRING.getId(), + TYPE_LIST = TagType.TAG_LIST.getId(), + TYPE_COMPOUND = TagType.TAG_COMPOUND.getId(), + TYPE_INT_ARRAY = TagType.TAG_INT_ARRAY.getId(), + TYPE_SHORT_ARRAY = TagType.TAG_SHORT_ARRAY.getId(), + TYPE_LONG_ARRAY = TagType.TAG_LONG_ARRAY.getId(); + + /** + * Default private constructor. + */ + private NBTConstants() { + } +} + diff --git a/anvil-loader/src/main/java/com/flowpowered/nbt/TagType.java b/anvil-loader/src/main/java/com/flowpowered/nbt/TagType.java new file mode 100644 index 0000000..444e3e2 --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/TagType.java @@ -0,0 +1,94 @@ +package com.flowpowered.nbt; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public enum TagType { + TAG_END(EndTag.class, "TAG_End", 0), + TAG_BYTE(ByteTag.class, "TAG_Byte", 1), + TAG_SHORT(ShortTag.class, "TAG_Short", 2), + TAG_INT(IntTag.class, "TAG_Int", 3), + TAG_LONG(LongTag.class, "TAG_Long", 4), + TAG_FLOAT(FloatTag.class, "TAG_Float", 5), + TAG_DOUBLE(DoubleTag.class, "TAG_Double", 6), + TAG_BYTE_ARRAY(ByteArrayTag.class, "TAG_Byte_Array", 7), + TAG_STRING(StringTag.class, "TAG_String", 8), + @SuppressWarnings("unchecked") + TAG_LIST((Class) ListTag.class, "TAG_List", 9), + // Java generics, y u so suck + TAG_COMPOUND(CompoundTag.class, "TAG_Compound", 10), + TAG_INT_ARRAY(IntArrayTag.class, "TAG_Int_Array", 11), + TAG_LONG_ARRAY(LongArrayTag.class, "TAG_Long_Array", 12), + TAG_SHORT_ARRAY(ShortArrayTag.class, "TAG_Short_Array", 100),; + private static final Map>, TagType> BY_CLASS = new HashMap>, TagType>(); + private static final Map BY_NAME = new HashMap(); + private static final TagType[] BY_ID; + + static { + BY_ID = new TagType[BaseData.maxId + 1]; + for (TagType type : TagType.values()) { + BY_CLASS.put(type.getTagClass(), type); + BY_NAME.put(type.getTypeName(), type); + BY_ID[type.getId()] = type; + } + } + + private final Class> tagClass; + private final String typeName; + private final int id; + + private TagType(Class> tagClass, String typeName, int id) { + this.tagClass = tagClass; + this.typeName = typeName; + this.id = id; + // Such a hack, shame that Java makes this such a pain + if (this.id > BaseData.maxId) { + BaseData.maxId = this.id; + } + } + + public Class> getTagClass() { + return tagClass; + } + + public String getTypeName() { + return typeName; + } + + public int getId() { + return id; + } + + public static TagType getByTagClass(Class> clazz) { + TagType ret = BY_CLASS.get(clazz); + if (ret == null) { + throw new IllegalArgumentException("Tag type " + clazz + " is unknown!"); + } + return ret; + } + + public static TagType getByTypeName(String typeName) { + TagType ret = BY_NAME.get(typeName); + if (ret == null) { + throw new IllegalArgumentException("Tag type " + typeName + " is unknown!"); + } + return ret; + } + + public static TagType getById(int id) { + if (id >= 0 && id < BY_ID.length) { + TagType ret = BY_ID[id]; + if (ret == null) { + throw new IllegalArgumentException("Tag type id " + id + " is unknown!"); + } + return ret; + } else { + throw new IndexOutOfBoundsException("Tag type id " + id + " is out of bounds!"); + } + } + + private static class BaseData { + private static int maxId = 0; + } +} diff --git a/anvil-loader/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java b/anvil-loader/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java new file mode 100644 index 0000000..8298779 --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java @@ -0,0 +1,210 @@ +package com.flowpowered.nbt.stream; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import com.flowpowered.nbt.*; + +/** + * This class reads NBT, or Named Binary Tag streams, and produces an object graph of subclasses of the {@link Tag} object.

The NBT format was created by Markus Persson, and the specification + * may be found at https://flowpowered.com/nbt/spec.txt. + */ +public final class NBTInputStream implements Closeable { + /** + * The data input stream. + */ + private final EndianSwitchableInputStream is; + + /** + * Creates a new {@link NBTInputStream}, which will source its data from the specified input stream. This assumes the stream is compressed. + * + * @param is The input stream. + * @throws java.io.IOException if an I/O error occurs. + */ + public NBTInputStream(InputStream is) throws IOException { + this(is, true, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if the stream is compressed with GZIP or not. This assumes the + * stream uses big endian encoding. + * + * @param is The input stream. + * @param compressed A flag indicating if the stream is compressed. + * @throws java.io.IOException if an I/O error occurs. + */ + public NBTInputStream(InputStream is, boolean compressed) throws IOException { + this(is, compressed, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if the stream is compressed with GZIP or not. + * + * @param is The input stream. + * @param compressed A flag indicating if the stream is compressed. + * @param endianness Whether to read numbers from the InputStream with little endian encoding. + * @throws java.io.IOException if an I/O error occurs. + */ + public NBTInputStream(InputStream is, boolean compressed, ByteOrder endianness) throws IOException { + this.is = new EndianSwitchableInputStream(compressed ? new GZIPInputStream(is) : is, endianness); + } + + /** + * Reads an NBT {@link Tag} from the stream. + * + * @return The tag that was read. + * @throws java.io.IOException if an I/O error occurs. + */ + public Tag readTag() throws IOException { + return readTag(0); + } + + /** + * Reads an NBT {@link Tag} from the stream. + * + * @param depth The depth of this tag. + * @return The tag that was read. + * @throws java.io.IOException if an I/O error occurs. + */ + private Tag readTag(int depth) throws IOException { + int typeId = is.readByte() & 0xFF; + TagType type = TagType.getById(typeId); + + String name; + if (type != TagType.TAG_END) { + int nameLength = is.readShort() & 0xFFFF; + byte[] nameBytes = new byte[nameLength]; + is.readFully(nameBytes); + name = new String(nameBytes, NBTConstants.CHARSET.name()); + } else { + name = ""; + } + + return readTagPayload(type, name, depth); + } + + /** + * Reads the payload of a {@link Tag}, given the name and type. + * + * @param type The type. + * @param name The name. + * @param depth The depth. + * @return The tag. + * @throws java.io.IOException if an I/O error occurs. + */ + @SuppressWarnings ({"unchecked", "rawtypes"}) + private Tag readTagPayload(TagType type, String name, int depth) throws IOException { + switch (type) { + case TAG_END: + if (depth == 0) { + throw new IOException("TAG_End found without a TAG_Compound/TAG_List tag preceding it."); + } else { + return new EndTag(); + } + + case TAG_BYTE: + return new ByteTag(name, is.readByte()); + + case TAG_SHORT: + return new ShortTag(name, is.readShort()); + + case TAG_INT: + return new IntTag(name, is.readInt()); + + case TAG_LONG: + return new LongTag(name, is.readLong()); + + case TAG_FLOAT: + return new FloatTag(name, is.readFloat()); + + case TAG_DOUBLE: + return new DoubleTag(name, is.readDouble()); + + case TAG_BYTE_ARRAY: + int length = is.readInt(); + byte[] bytes = new byte[length]; + is.readFully(bytes); + return new ByteArrayTag(name, bytes); + + case TAG_STRING: + length = is.readShort(); + bytes = new byte[length]; + is.readFully(bytes); + return new StringTag(name, new String(bytes, NBTConstants.CHARSET.name())); + + case TAG_LIST: + TagType childType = TagType.getById(is.readByte()); + length = is.readInt(); + + Class clazz = childType.getTagClass(); + List tagList = new ArrayList(length); + for (int i = 0; i < length; i++) { + Tag tag = readTagPayload(childType, "", depth + 1); + if (tag instanceof EndTag) { + throw new IOException("TAG_End not permitted in a list."); + } else if (!clazz.isInstance(tag)) { + throw new IOException("Mixed tag types within a list."); + } + tagList.add(tag); + } + + return new ListTag(name, clazz, tagList); + + case TAG_COMPOUND: + CompoundMap compoundTagList = new CompoundMap(); + while (true) { + Tag tag = readTag(depth + 1); + if (tag instanceof EndTag) { + break; + } else { + compoundTagList.put(tag); + } + } + + return new CompoundTag(name, compoundTagList); + + case TAG_INT_ARRAY: + length = is.readInt(); + int[] ints = new int[length]; + for (int i = 0; i < length; i++) { + ints[i] = is.readInt(); + } + return new IntArrayTag(name, ints); + + case TAG_SHORT_ARRAY: + length = is.readInt(); + short[] shorts = new short[length]; + for (int i = 0; i < length; i++) { + shorts[i] = is.readShort(); + } + return new ShortArrayTag(name, shorts); + + case TAG_LONG_ARRAY: + length = is.readInt(); + long[] longs = new long[length]; + for (int i = 0; i < length; i++) { + longs[i] = is.readLong(); + } + return new LongArrayTag(name, longs); + + default: + throw new IOException("Invalid tag type: " + type + "."); + } + } + + public void close() throws IOException { + is.close(); + } + + /** + * @return whether this NBTInputStream reads numbers in little-endian format. + */ + public ByteOrder getByteOrder() { + return is.getEndianness(); + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/AnvilBlock.java b/anvil-loader/src/main/java/mc/world/anvil/AnvilBlock.java new file mode 100644 index 0000000..0bdf259 --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/AnvilBlock.java @@ -0,0 +1,72 @@ +package mc.world.anvil; + +import com.flowpowered.nbt.CompoundTag; +import lombok.extern.slf4j.Slf4j; +import mc.core.world.block.Block; +import mc.core.world.block.BlockLocation; +import mc.core.world.block.BlockType; + +@Slf4j +public class AnvilBlock implements Block { + private final AnvilChunkSection chunkSection; + private final BlockLocation location; + private BlockLocation globalLocation; + + public AnvilBlock(AnvilChunkSection chunkSection, int x, int y, int z) { + this.chunkSection = chunkSection; + this.location = new BlockLocation(x, y, z); + } + + @Override + public int getLight() { + return chunkSection.getBlockLight().get(location); + } + + @Override + public void setLight(int light) { + // nope... + } + + @Override + public BlockType getType() { + final byte id = chunkSection.getBlocks().get((location.getY() << 8) + (location.getZ() << 4) + location.getX()); + final int meta = chunkSection.getBlocksMeta().get(location); + return BlockType.getByIdMeta(id & 0xFF, meta); + } + + @Override + public BlockLocation getLocation() { + if (globalLocation == null) { + globalLocation = new BlockLocation( + (chunkSection.getParent().getX() << 4) + location.getX(), + (chunkSection.getY() << 4) + location.getY(), + (chunkSection.getParent().getZ() << 4) + location.getZ() + ); + } + return globalLocation; + } + + @Override + public CompoundTag getNBTData() { + CompoundTag compoundTag = ((AnvilChunk)chunkSection.getParent()).getNbtByGlobalXYZ( + (chunkSection.getParent().getX() << 4) + location.getX(), + (chunkSection.getY() << 4) + location.getY(), + (chunkSection.getParent().getZ() << 4) + location.getZ() + ); + + if (compoundTag != null) { + compoundTag.getValue().remove("Items"); + compoundTag.getValue().remove("Lock"); + } + + return compoundTag; + } + + @Override + public String toString() { + return "AnvilBlock{" + + "location=" + getLocation() + + ", type=" + getType() + + '}'; + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/AnvilChunk.java b/anvil-loader/src/main/java/mc/world/anvil/AnvilChunk.java new file mode 100644 index 0000000..9810f9e --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/AnvilChunk.java @@ -0,0 +1,137 @@ +package mc.world.anvil; + +import com.flowpowered.nbt.ByteArrayTag; +import com.flowpowered.nbt.ByteTag; +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.IntTag; +import com.flowpowered.nbt.ListTag; +import gnu.trove.list.TByteList; +import gnu.trove.list.array.TByteArrayList; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import mc.core.utils.NibbleArray; +import mc.core.world.Biome; +import mc.core.world.block.Block; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Getter +public class AnvilChunk implements Chunk { + private int x; + private int z; + private TByteList biomes = new TByteArrayList(256); + private List sections; + private ListTag tileEntities; + + @SuppressWarnings("unchecked") + AnvilChunk(CompoundTag chunkTag) { + CompoundMap levelTagMap = ((CompoundTag) chunkTag.getValue().get("Level")).getValue(); + + this.x = ((IntTag) levelTagMap.get("xPos")).getValue(); + this.z = ((IntTag) levelTagMap.get("zPos")).getValue(); + + biomes.add(((ByteArrayTag) levelTagMap.get("Biomes")).getValue()); + tileEntities = (ListTag) levelTagMap.get("TileEntities"); + + List sections = ((ListTag) levelTagMap.get("Sections")).getValue(); + this.sections = new ArrayList<>(sections.size()); + + for (CompoundTag sectionTag : sections) { + CompoundMap sectionTagValue = sectionTag.getValue(); + + AnvilChunkSection chunkSection = new AnvilChunkSection(); + chunkSection.setParent(this); + chunkSection.setY(((ByteTag) sectionTagValue.get("Y")).getValue()); + + chunkSection.setBlockLight(new NibbleArray(((ByteArrayTag) sectionTagValue.get("BlockLight")).getValue())); + chunkSection.setSkyLight(new NibbleArray(((ByteArrayTag) sectionTagValue.get("SkyLight")).getValue())); + chunkSection.getBlocks().add(((ByteArrayTag) sectionTagValue.get("Blocks")).getValue()); + chunkSection.setBlocksMeta(new NibbleArray(((ByteArrayTag) sectionTagValue.get("Data")).getValue())); + + this.sections.add(chunkSection); + } + } + + CompoundTag getNbtByGlobalXYZ(int x, int y, int z) { + for (CompoundTag compoundTag : tileEntities.getValue()) { + CompoundMap compoundMap = compoundTag.getValue(); + if (((IntTag)compoundMap.get("x")).getValue() == x + && ((IntTag)compoundMap.get("y")).getValue() == y + && ((IntTag)compoundMap.get("z")).getValue() == z) { + return compoundTag; + } + } + return null; + } + + @Override + public ChunkSection getChunkSection(int height) { + if (height > sections.size()-1) return null; + return sections.get(height); + } + + @Override + public void setChunkSection(int height, ChunkSection chunkSection) { + // nope... + } + + @Override + public Block getBlock(int x, int y, int z) { + final int height = y >> 4; + return sections.get(height).getBlock( + x - getX() << 4, + y - height << 4, + z - getZ() << 4 + ); + } + + @Override + public void setBlock(Block block) { + // nope... + } + + @Override + public int getSkyLight(int x, int y, int z) { + final int height = y >> 4; + return sections.get(height).getSkyLight( + x - getX() << 4, + y - height << 4, + z - getZ() << 4 + ); + } + + @Override + public void setSkyLight(int x, int y, int z, int lightLevel) { + // nope... + } + + @Override + public int getAddition(int x, int y, int z) { + final int height = y >> 4; + return sections.get(height).getAddition( + x - getX() << 4, + y - height << 4, + z - getZ() << 4 + ); + } + + @Override + public void setAddition(int x, int y, int z, int value) { + // nope... + } + + @Override + public Biome getBiome(int x, int z) { + return Biome.getById(biomes.get(((z & 31) >> 4) << 4 | ((x & 31) >> 4)) & 255); + } + + @Override + public void setBiome(int x, int z, Biome biome) { + // nope... + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/AnvilChunkProvider.java b/anvil-loader/src/main/java/mc/world/anvil/AnvilChunkProvider.java new file mode 100644 index 0000000..d3fafc3 --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/AnvilChunkProvider.java @@ -0,0 +1,50 @@ +package mc.world.anvil; + +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkProvider; +import org.springframework.stereotype.Component; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Slf4j +@Component +@NoArgsConstructor +@Setter +public class AnvilChunkProvider implements ChunkProvider { + private RegionManager regionManager; + + public AnvilChunkProvider(String mapPath) { + Path pathMap = Paths.get(mapPath); + if (Files.exists(pathMap)) { + log.info("Use Anvil map from \"{}\"", pathMap); + this.setRegionManager(new RegionManager(pathMap.resolve("region"))); + } else { + log.error("Anvil map: path \"{}\" not found!!!", pathMap); + } + } + + @Override + public Chunk getChunk(int x, int z) { + Region region = regionManager.getRegion(x >> 5, z >> 5); + if (region == null) { + return new EmptyChunk(x, z); + } else { + return region.getChunk(x, z); + } + } + + @Override + public void saveChunk(Chunk chunk) { + // nope + } + + @Override + public void saveChunk(Chunk... chunks) { + // nope + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/AnvilChunkSection.java b/anvil-loader/src/main/java/mc/world/anvil/AnvilChunkSection.java new file mode 100644 index 0000000..c4115ce --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/AnvilChunkSection.java @@ -0,0 +1,58 @@ +package mc.world.anvil; + +import gnu.trove.list.TByteList; +import gnu.trove.list.array.TByteArrayList; +import gnu.trove.list.linked.TByteLinkedList; +import lombok.Getter; +import lombok.Setter; +import mc.core.utils.NibbleArray; +import mc.core.world.block.Block; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +@Getter +public class AnvilChunkSection implements ChunkSection { + @Setter + private Chunk parent; + + @Setter + private int y; + + private TByteList blocks = new TByteArrayList(); + @Setter + private NibbleArray blocksMeta; + @Setter + private NibbleArray blockLight; + @Setter + private NibbleArray skyLight; + + @Override + public Block getBlock(int x, int y, int z) { + return new AnvilBlock(this, x, y, z); + } + + @Override + public void setBlock(Block block) { + // nope... + } + + @Override + public int getSkyLight(int x, int y, int z) { + return skyLight.get(x, y, z); + } + + @Override + public void setSkyLight(int x, int y, int z, int lightLevel) { + // nope... + } + + @Override + public int getAddition(int x, int y, int z) { + return 0; + } + + @Override + public void setAddition(int x, int y, int z, int value) { + // nope... + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/EmptyChunk.java b/anvil-loader/src/main/java/mc/world/anvil/EmptyChunk.java new file mode 100644 index 0000000..0ff6a8f --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/EmptyChunk.java @@ -0,0 +1,153 @@ +package mc.world.anvil; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import mc.core.world.Biome; +import mc.core.world.block.Block; +import mc.core.world.block.BlockLocation; +import mc.core.world.block.BlockType; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +import java.util.LinkedList; +import java.util.List; + +@Getter +public class EmptyChunk implements Chunk { + private int x; + private int z; + private List $sections = new LinkedList<>(); + + public EmptyChunk(int x, int z) { + this.x = x; + this.z = z; + + for (int i = 0; i < 16; i++) { + this.$sections.add(null); + } + } + + @Override + public ChunkSection getChunkSection(int height) { + ChunkSection section; + if ((section = $sections.get(height)) == null) { + section = new EmptySection(height); + $sections.set(height, section); + } + + return section; + } + + @Override + public void setChunkSection(int height, ChunkSection chunkSection) { + } + + @Override + public Block getBlock(int x, int y, int z) { + return getChunkSection(y >> 4).getBlock( + x - getX() << 4, + y - (y >> 4) << 4, + z - getZ() << 4 + ); + } + + @Override + public void setBlock(Block block) { + } + + @Override + public int getSkyLight(int x, int y, int z) { + return getChunkSection(y >> 4).getSkyLight(x, y, z); + } + + @Override + public void setSkyLight(int x, int y, int z, int lightLevel) { + } + + @Override + public int getAddition(int x, int y, int z) { + return getChunkSection(y >> 4).getAddition(x, y, z); + } + + @Override + public void setAddition(int x, int y, int z, int value) { + } + + @Override + public Biome getBiome(int x, int z) { + return Biome.PLAINS; + } + + @Override + public void setBiome(int x, int z, Biome biome) { + } + + @NoArgsConstructor + @Getter + public class EmptySection implements ChunkSection { + private int y; + + EmptySection(int y) { + this.y = y; + } + + @Override + public Chunk getParent() { + return EmptyChunk.this; + } + + @Override + public void setParent(Chunk chunk) { + } + + @Override + public Block getBlock(int localX, int localY, int localZ) { + return new Block() { + @Override + public int getLight() { + return 15; + } + + @Override + public void setLight(int light) { + } + + @Override + public BlockType getType() { + return BlockType.AIR; + } + + @Override + public BlockLocation getLocation() { + return new BlockLocation( + (getParent().getX() << 4) + localX, + (getY() << 4) + localY, + (getParent().getZ() << 4) + localZ + ); + } + }; + } + + @Override + public void setBlock(Block block) { + } + + @Override + public int getSkyLight(int localX, int localY, int localZ) { + return 15; + } + + @Override + public void setSkyLight(int localX, int localY, int localZ, int lightLevel) { + } + + @Override + public int getAddition(int localX, int localY, int localZ) { + return 0; + } + + @Override + public void setAddition(int localX, int localY, int localZ, int value) { + } + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/Region.java b/anvil-loader/src/main/java/mc/world/anvil/Region.java new file mode 100644 index 0000000..b171aee --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/Region.java @@ -0,0 +1,108 @@ +package mc.world.anvil; + +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.Tag; +import com.flowpowered.nbt.stream.NBTInputStream; +import gnu.trove.list.TByteList; +import gnu.trove.list.array.TByteArrayList; +import lombok.extern.slf4j.Slf4j; +import mc.core.world.chunk.Chunk; + +import javax.annotation.Nullable; +import java.io.*; +import java.util.zip.InflaterInputStream; + +@Slf4j +class Region implements Closeable { + private static final byte BYTE_TRUE = 1, + BYTE_FALSE = 0; + + private RandomAccessFile file; + private TByteList sectorFree; + private final int[] offsets = new int[1024]; + + Region(File file) throws IOException { + this.file = new RandomAccessFile(file, "rw"); + + int sizeOfSectorFree = (int)this.file.length() / 4096; + sectorFree = new TByteArrayList(sizeOfSectorFree); + + sectorFree.add(BYTE_FALSE); + sectorFree.add(BYTE_FALSE); + for (int i = 0; i < sizeOfSectorFree-2; i++) { + sectorFree.add(BYTE_TRUE); + } + + for (int i = 0; i < offsets.length; ++i) { + int read = this.file.readInt(); + offsets[i] = read; + + if (read != 0 && (read >> 8) + (read & 255) <= this.sectorFree.size()) { + for (int j = 0; j < (read & 255); ++j) { + this.sectorFree.set((read >> 8) + j, BYTE_FALSE); + } + } + } + + this.file.skipBytes(1024); + } + + @Nullable + Chunk getChunk(int x, int z) { + int offset; + try { + offset = getOffset(x & 31, z & 31); + } catch (Exception e) { + return new EmptyChunk(x, z); + } + + if (offset == 0) { + return new EmptyChunk(x, z); + } + + int v1 = offset >> 8; + int v2 = offset & 255; + + if (v1 + v2 > sectorFree.size()) { + return new EmptyChunk(x, z); + } + + try { + file.seek((long) (v1 * 4096)); + int read = file.readInt(); + if (read <= 0 || read > 4096 * v2) { + return new EmptyChunk(x, z); + } + + boolean gzippedData = (file.readByte() == 0x01); + + if (gzippedData) { + log.warn("GZipped"); + } else { + byte[] buffer = new byte[read - 1]; + file.read(buffer); + InflaterInputStream inputStream = new InflaterInputStream(new ByteArrayInputStream(buffer)); + + NBTInputStream nbtInputStream = new NBTInputStream(inputStream, false); + + Tag rootTag = nbtInputStream.readTag(); + nbtInputStream.close(); + return new AnvilChunk((CompoundTag) rootTag); + } + } catch (IOException e) { + log.error("Get chunk", e); + return null; + } + + return null; + } + + private int getOffset(int x, int z) { + return offsets[x + z * 32]; + } + + @Override + public void close() throws IOException { + if (file != null) file.close(); + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/RegionManager.java b/anvil-loader/src/main/java/mc/world/anvil/RegionManager.java new file mode 100644 index 0000000..b0cdb50 --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/RegionManager.java @@ -0,0 +1,49 @@ +package mc.world.anvil; + +import gnu.trove.map.TIntObjectMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import lombok.extern.slf4j.Slf4j; +import mc.core.utils.CompactedCoords; +import org.springframework.lang.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +@Slf4j +public class RegionManager { + private final Path regionFilesPath; + private final TIntObjectMap regions = new TIntObjectHashMap<>(); + + public RegionManager(String regionFilesPath) { + this(Paths.get(regionFilesPath)); + } + + public RegionManager(Path regionFilesPath) { + this.regionFilesPath = regionFilesPath; + } + + @Nullable + public Region getRegion(int x, int z) { + final int xz = CompactedCoords.compressXZ(x, z); + + if (regions.containsKey(xz)) { + return regions.get(xz); + } else { + Path regionFilePath = regionFilesPath.resolve("r." + x + "." + z + ".mca"); + if (Files.exists(regionFilePath)) { + try { + Region region = new Region(regionFilePath.toFile()); + regions.put(xz, region); + return region; + } catch (IOException e) { + log.error("load region from file", e); + return null; + } + } else { + return null; + } + } + } +} diff --git a/anvil-loader/src/test/java/mc/world/anvil/RegionTest.java b/anvil-loader/src/test/java/mc/world/anvil/RegionTest.java new file mode 100644 index 0000000..81e6caf --- /dev/null +++ b/anvil-loader/src/test/java/mc/world/anvil/RegionTest.java @@ -0,0 +1,213 @@ +package mc.world.anvil; + +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.IntTag; +import com.flowpowered.nbt.StringTag; +import lombok.SneakyThrows; +import mc.core.world.block.Block; +import mc.core.world.block.BlockType; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class RegionTest { + private static RegionManager regionManager; + + @BeforeAll + @SneakyThrows + static void before() { + regionManager = new RegionManager(Paths.get(RegionTest.class.getResource("/region/").toURI())); + } + + private static void assertZeroPlast(int x, int z, Block block, String msg) { + // @formatter:off + if (x == 0 && z == 0) assertEquals(BlockType.STONE, block.getType(), msg); + else if (x == 15 && z == 0) assertEquals(BlockType.GRANITE, block.getType(), msg); + else if (x == 0 && z == 15) assertEquals(BlockType.POLISHED_GRANITE, block.getType(), msg); + else if (x == 15 && z == 15) assertEquals(BlockType.DIORITE, block.getType(), msg); + else assertEquals(BlockType.BEDROCK, block.getType(), msg); + // @formatter:on + } + + private static ChunkChecker chunkChecker00() { + return new ChunkChecker() { + private void checkSection0(ChunkSection chunkSection) { + for (int y = 0; y < 16; y++) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + Block block = chunkSection.getBlock(x, y, z); + String msg = String.format("coords: %d %d %d", x, y, z); + + if (y == 0) { + assertZeroPlast(x, z, block, msg); + } else { + assertEquals(BlockType.STONE, block.getType(), msg); + } + } + } + } + } + + private CompoundTag createExceptedNBT(Block block) { + CompoundMap compoundMap = new CompoundMap(); + compoundMap.put(new IntTag("x", block.getLocation().getX())); + compoundMap.put(new IntTag("y", block.getLocation().getY())); + compoundMap.put(new IntTag("z", block.getLocation().getZ())); + compoundMap.put(new StringTag("id", block.getType().getNamedId())); + + return new CompoundTag("", compoundMap); + } + + private void checkSection1(ChunkSection chunkSection) { + for (int y = 0; y < 16; y++) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + Block block = chunkSection.getBlock(x, y, z); + String msg = String.format("coords: %d %d %d", x, y, z); + + // @formatter:off + if (y == 0) assertEquals(BlockType.DIRT, block.getType(), msg); + else if (y == 1) assertEquals(BlockType.GRASS, block.getType(), msg); + else if (y == 2) { + if ((x == 2 || x == 4 || x == 5) && z == 1) { + assertEquals(BlockType.CHEST_NORTH, block.getType(), msg); + assertEquals(createExceptedNBT(block), block.getNBTData()); + } else if ((x == 2 || x == 3 || x == 5) && z == 6) { + assertEquals(BlockType.CHEST_SOUTH, block.getType(), msg); + assertEquals(createExceptedNBT(block), block.getNBTData()); + } else if (x == 1 && (z == 2 || z == 3 || z == 5)) { + assertEquals(BlockType.CHEST_WEST, block.getType(), msg); + assertEquals(createExceptedNBT(block), block.getNBTData()); + } else if (x == 6 && (z == 2 || z == 4 || z == 5)) { + assertEquals(BlockType.CHEST_EAST, block.getType(), msg); + assertEquals(createExceptedNBT(block), block.getNBTData()); + } else { + assertEquals(BlockType.AIR, block.getType(), msg); + } + } + else assertEquals(BlockType.AIR, block.getType(), msg); + // @formatter:on + } + } + } + } + + @Override + public void check(Chunk chunk) { + ChunkSection chunkSection = chunk.getChunkSection(0); + assertNotNull(chunkSection); + checkSection0(chunkSection); + + chunkSection = chunk.getChunkSection(1); + assertNotNull(chunkSection); + checkSection1(chunkSection); + } + }; + } + + private static ChunkChecker chunkChecker01() { + return new ChunkChecker() { + @Override + public void check(Chunk chunk) { + ChunkSection section = chunk.getChunkSection(0); + assertNotNull(section); + + final List exceptedTypes = Arrays.asList( + BlockType.CLAY, + BlockType.ORE_REDSTONE, + BlockType.ORE_DIAMOND, + BlockType.OBSIDIAN, + BlockType.STONE_MOSS, + BlockType.SANDSTONE, + BlockType.ORE_LAPIS, + BlockType.WOOD_JUNGLE, + BlockType.WOOD_BIRCH, + BlockType.WOOD_SPRUCE, + BlockType.WOOD_OAK, + BlockType.ORE_COAL, + BlockType.ORE_IRON, + BlockType.ORE_GOLD, + BlockType.GRAVEL, + BlockType.SAND + ); + + for (int y = 0; y < 2; y++) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + Block block = section.getBlock(x, y, z); + String msg = String.format("coords: %d %d %d", x, y, z); + + if (y == 0) { + assertZeroPlast(x, z, block, msg); + } else { + assertEquals(exceptedTypes.get(x), block.getType(), msg); + } + } + } + } + } + }; + } + + private static ChunkChecker chunkChecker0N1() { + return new ChunkChecker() { + @Override + public void check(Chunk chunk) { + ChunkSection section = chunk.getChunkSection(0); + assertNotNull(section); + + for (int y = 0; y < 1; y++) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + assertZeroPlast(x, z, + section.getBlock(x, y, z), + String.format("coords: %d %d %d", x, y, z)); + } + } + } + } + }; + } + + private static Stream streamArguments() { + + + return Stream.of( + Arguments.of(0, 0, chunkChecker00()), + Arguments.of(0, 1, chunkChecker01()), + Arguments.of(0, -1, chunkChecker0N1()) + ); + } + + @DisplayName("testGetChunk") + @ParameterizedTest(name = "[{index}] chunk {0},{1}") + @MethodSource("streamArguments") + void testGetChunk(int chunkX, int chunkZ, ChunkChecker chunkChecker) { + final Region region = regionManager.getRegion(chunkX >> 5, chunkZ >> 5); + assertNotNull(region); + + final Chunk chunk = region.getChunk(chunkX, chunkZ); + assertNotNull(chunk); + assertFalse(chunk instanceof EmptyChunk); + + chunkChecker.check(chunk); + } + + interface ChunkChecker { + void check(Chunk chunk); + } +} diff --git a/anvil-loader/src/test/resources/region/r.0.-1.mca b/anvil-loader/src/test/resources/region/r.0.-1.mca new file mode 100644 index 0000000..25001f9 Binary files /dev/null and b/anvil-loader/src/test/resources/region/r.0.-1.mca differ diff --git a/anvil-loader/src/test/resources/region/r.0.0.mca b/anvil-loader/src/test/resources/region/r.0.0.mca new file mode 100644 index 0000000..853add5 Binary files /dev/null and b/anvil-loader/src/test/resources/region/r.0.0.mca differ diff --git a/core/README.MD b/core/README.MD index 173954c..59b1fb7 100644 --- a/core/README.MD +++ b/core/README.MD @@ -1,10 +1,24 @@ # Core -Ядро сервера +Ядро сервера. + +Пример настройки можно посмотреть в файле `sample-config.xml`. ## Spring beans -### ConfigFromSpring +### Разное + +#### CoreEventListener + +Стандартный обработчик системных событий. + +**Bean example:** + +```xml + +``` + +#### ConfigFromSpring Настройка параметров сервера через конфигурацию "спринга". @@ -25,20 +39,36 @@ ``` -### GameLoop +#### GameLoop **Bean example:** Доступные параметры: * `gameTimer` - бин, управляющий ходом времени +* `percentWarnLowTps` - порог "низкого" значения TPS, в процентах ```xml + ``` -### IdleTime +#### SimpleChatProcessor + +Простой обработчик чата. + +**Implements:** `mc.core.chat.ChatProcessor` + +**Bean example:** + +```xml + +``` + +### Время + +#### IdleTime Игровое время суток застывает на указанной отметке. @@ -55,7 +85,7 @@ ``` -### TimePerTick +#### TimePerTick Игровое время суток соответствует игровым тикам (20 tps) @@ -72,7 +102,7 @@ ``` -### RealTime +#### RealTime Игровое время суток соответствует реальному времени diff --git a/core/build.gradle b/core/build.gradle index c2585f1..dba539c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,4 +1,4 @@ -version '0.1' +version '0.2' apply plugin: 'maven' apply plugin: 'application' diff --git a/core/sample-config.xml b/core/sample-config.xml new file mode 100644 index 0000000..a32dec9 --- /dev/null +++ b/core/sample-config.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/java/mc/core/GameLoop.java b/core/src/main/java/mc/core/GameLoop.java index 02eefa0..fff0c12 100644 --- a/core/src/main/java/mc/core/GameLoop.java +++ b/core/src/main/java/mc/core/GameLoop.java @@ -18,7 +18,7 @@ public class GameLoop extends Thread { private TimeProcessor gameTimer; public GameLoop() { - super(); + super("Game Loop Thread"); setTps(20); setPercentWarnLowTps(5); } diff --git a/core/src/main/java/mc/core/chat/CommandExecutor.java b/core/src/main/java/mc/core/chat/CommandExecutor.java deleted file mode 100644 index 39d4b2a..0000000 --- a/core/src/main/java/mc/core/chat/CommandExecutor.java +++ /dev/null @@ -1,13 +0,0 @@ -package mc.core.chat; - -import mc.core.player.Player; - -import java.util.Optional; - -public interface CommandExecutor { - String getName(); - Optional getAliases(); - Optional getUsage(); - String getDescription(); - void execute(Player sender, String... args); -} diff --git a/core/src/main/java/mc/core/chat/CommanderChatProcessor.java b/core/src/main/java/mc/core/chat/CommanderChatProcessor.java deleted file mode 100644 index 7cf5880..0000000 --- a/core/src/main/java/mc/core/chat/CommanderChatProcessor.java +++ /dev/null @@ -1,90 +0,0 @@ -package mc.core.chat; - -import lombok.extern.slf4j.Slf4j; -import mc.core.player.Player; -import mc.core.text.Text; -import mc.core.text.TextColor; -import mc.core.text.TextTemplate; -import org.slf4j.Marker; -import org.slf4j.helpers.BasicMarkerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; - -import javax.annotation.PostConstruct; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -@Slf4j -public class CommanderChatProcessor extends SimpleChatProcessor { - private static final Marker COMMAND_MARKER = new BasicMarkerFactory().getMarker("Command"); - private static final TextTemplate UNKNOW_COMMAND_MSG = TextTemplate.builder() - .append(Text.of("Unknown command \"", TextColor.RED)) - .arg("command", TextColor.WHITE) - .append(Text.of("\"", TextColor.RED)) - .build(); - @Autowired - private ApplicationContext applicationContext; - private Map commands = new HashMap<>(); - - @PostConstruct - public void init() { - Map beans = applicationContext.getBeansOfType(CommandExecutor.class); - beans.values().forEach(commandExecutor -> { - log.trace("Add command \"{}\" ({})", commandExecutor.getName(), commandExecutor.getClass().getName()); - if (commands.containsKey(commandExecutor.getName())) { - log.warn("Override command \"{}\"", commandExecutor.getName()); - log.debug("{} -> {}", - commands.get(commandExecutor.getName()).getClass().getName(), - commandExecutor.getClass().getName() - ); - } - commands.put(commandExecutor.getName(), commandExecutor); - - if (commandExecutor.getAliases().isPresent()) { - Arrays.stream(commandExecutor.getAliases().get()).forEach(aliase -> { - log.trace("Add aliase \"{}\" ({})", aliase, commandExecutor.getClass().getName()); - if (commands.containsKey(aliase)) { - log.warn("Override aliase \"{}\"", aliase); - log.debug("{} -> {}", - commands.get(aliase).getClass().getName(), - commandExecutor.getClass().getName() - ); - } - commands.put(aliase, commandExecutor); - }); - } - }); - - log.debug("Load {} commands", commands.size()); - } - - @Override - public void process(Player player, String message) { - if (message.startsWith("/")) { - log.info(COMMAND_MARKER, "<{}> {}", player.getName(), message); - - int idx = message.indexOf(' '); - if (idx == -1) { - idx = message.length(); - } - - String command = message.substring(1, idx).toLowerCase(); - if (commands.containsKey(command)) { - String[] args = message.substring(idx).split(" "); - commands.get(command).execute(player, args); - } else { - player.getChannel().sendChatMessage( - UNKNOW_COMMAND_MSG.apply("command", command), - MessageType.SYSTEM_MESSAGE); - } - } else { - super.process(player, message); - } - } - - public Collection getAllCommands() { - return commands.values(); - } -} diff --git a/core/src/main/java/mc/core/utils/NibbleArray.java b/core/src/main/java/mc/core/utils/NibbleArray.java index e8514f7..63b936d 100644 --- a/core/src/main/java/mc/core/utils/NibbleArray.java +++ b/core/src/main/java/mc/core/utils/NibbleArray.java @@ -41,10 +41,6 @@ public class NibbleArray { return isLowerNibble(idx) ? this.data[ni] & 0x0F : this.data[ni] >> 4 & 0x0F; } - public void set(BlockLocation location, int value) { - set(location.getX(), location.getY(), location.getZ(), value); - } - public void set(int x, int y, int z, int value) { if (value < 0) value = 0; else if (value > 15) value = 15; diff --git a/core/src/main/java/mc/core/world/block/BlockType.java b/core/src/main/java/mc/core/world/block/BlockType.java index 13eb1e6..2eecead 100644 --- a/core/src/main/java/mc/core/world/block/BlockType.java +++ b/core/src/main/java/mc/core/world/block/BlockType.java @@ -1,10 +1,13 @@ package mc.core.world.block; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; +import java.util.Optional; import java.util.stream.Stream; @Slf4j @@ -491,25 +494,29 @@ public enum BlockType { this.namedId = null; } - public static BlockType getByIdMeta(int id, int meta) { - if (id < 0) { - log.warn("Incorrect id \"{}\"", id); - return BEDROCK; - } - - Stream stream = Arrays.stream(BlockType.values()); - return stream.filter(blockType -> blockType.id == id && blockType.meta == meta) - .findFirst() - .orElseGet(() -> { - log.warn("Unknow block type: {}:{}", id, meta); - return BEDROCK; - }); - } - @Getter private final int id; @Getter private final int meta; @Getter private final String namedId; + + private static final Table typeTable = HashBasedTable.create(); + + static { + Arrays.stream(BlockType.values()) + .forEach(blockType -> typeTable.put(blockType.id, blockType.meta, blockType)); + } + + public static BlockType getByIdMeta(int id, int meta) { + if (id < 0) { + log.warn("Incorrect id \"{}\"", id); + return BEDROCK; + } + + return Optional.ofNullable(typeTable.get(id, meta)).orElseGet(() -> { + log.warn("Unknow block type: {}:{}", id, meta); + return BEDROCK; + }); + } } diff --git a/h2_playermanager/README.MD b/h2_playermanager/README.MD new file mode 100644 index 0000000..2f6b15c --- /dev/null +++ b/h2_playermanager/README.MD @@ -0,0 +1,5 @@ +# H2 Player manager + +Хранилище данных игроков на базе [H2 Database](http://www.h2database.com/). + +Пример настройки можно посмотреть в файле `sample-config.xml` diff --git a/h2_playermanager/build.gradle b/h2_playermanager/build.gradle new file mode 100644 index 0000000..8a27a2e --- /dev/null +++ b/h2_playermanager/build.gradle @@ -0,0 +1,17 @@ +version '0.1' + +ext { + spring_data_version = '2.1.0.RELEASE' +} + +dependencies { + /* Core */ + compile_excludeCopy project(':core') + + /* Spring */ + compile (group: 'org.springframework.data', name: 'spring-data-jpa', version: spring_data_version) + + /* Database */ + compile (group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.3.6.Final') + compile (group: 'com.h2database', name: 'h2', version: '1.4.197') +} \ No newline at end of file diff --git a/h2_playermanager/sample-config.xml b/h2_playermanager/sample-config.xml new file mode 100644 index 0000000..2fe46ec --- /dev/null +++ b/h2_playermanager/sample-config.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + org.hibernate.dialect.H2Dialect + false + update + + + + + + + + \ No newline at end of file diff --git a/h2_playermanager/src/main/java/mc/core/h2db/H2Player.java b/h2_playermanager/src/main/java/mc/core/h2db/H2Player.java new file mode 100644 index 0000000..8880503 --- /dev/null +++ b/h2_playermanager/src/main/java/mc/core/h2db/H2Player.java @@ -0,0 +1,63 @@ +package mc.core.h2db; + +import lombok.Data; +import mc.core.EntityLocation; +import mc.core.exception.ResourceUnloadedException; +import mc.core.network.NetChannel; +import mc.core.player.Player; +import mc.core.player.PlayerSettings; +import mc.core.world.World; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Data +public class H2Player implements Player { + private int id; + private UUID uuid; + private String name; + private boolean online = false; + private List loadedChunks; + private NetChannel channel; + private EntityLocation location; + private Reference $refWorld; + private boolean flying = false; + private PlayerSettings settings; + + @Override + public World getWorld() { + if ($refWorld == null) { + return null; + } else if ($refWorld.get() == null) { + throw new ResourceUnloadedException("You're trying to get unloaded world"); + } else { + return $refWorld.get(); + } + } + + @Override + public void setWorld(World world) { + if (world == null) { + this.$refWorld = null; + } else { + this.$refWorld = new WeakReference<>(world); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + H2Player player = (H2Player) obj; + return id == player.id && + Objects.equals(uuid, player.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(id, uuid); + } +} diff --git a/h2_playermanager/src/main/java/mc/core/h2db/H2PlayerManager.java b/h2_playermanager/src/main/java/mc/core/h2db/H2PlayerManager.java new file mode 100644 index 0000000..b22699c --- /dev/null +++ b/h2_playermanager/src/main/java/mc/core/h2db/H2PlayerManager.java @@ -0,0 +1,95 @@ +package mc.core.h2db; + +import com.google.common.collect.ImmutableList; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.EntityLocation; +import mc.core.h2db.service.H2PlayerService; +import mc.core.network.BroadcastNetChannel; +import mc.core.network.NetChannel; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.player.PlayerSettings; +import mc.core.world.World; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Slf4j +@Component +public class H2PlayerManager implements PlayerManager { + @Setter + @Autowired + private H2PlayerService h2PlayerService; + private Set playerList = Collections.synchronizedSet(new HashSet<>()); + + @Override + public Player createPlayer(String name, EntityLocation location, World world) { + H2Player h2Player = new H2Player(); + h2Player.setName(name); + h2Player.setUuid(UUID.randomUUID()); + h2Player.setLocation(location.clone()); + h2Player.setLoadedChunks(new ArrayList<>()); + h2Player.setWorld(world); + h2Player.setSettings(new PlayerSettings()); + + return h2PlayerService.save(h2Player); + } + + @Override + public void joinServer(Player player) { + //TODO в дальнейшем следует именно этому методу передать функции инсерта в БД + H2Player h2Player = (H2Player) player; + playerList.add(h2Player); + h2Player.setOnline(true); + } + + @Override + public void leftServer(Player player) { + H2Player h2Player = (H2Player) player; + h2PlayerService.save(h2Player); + h2Player.setOnline(false); + h2Player.getLoadedChunks().clear(); + } + + @Override + public Player getPlayer(String name) { + return playerList.stream() + .filter(player -> player.getName().equals(name)) + .filter(H2Player::isOnline) + .findFirst().orElse(null); + } + + @Override + public List getPlayers() { + return playerList.stream() + .filter(H2Player::isOnline) + .collect(ImmutableList.toImmutableList()); + } + + @Override + public int getCountPlayers() { + return (int) playerList.stream() + .filter(H2Player::isOnline) + .count(); + } + + @Override + public NetChannel getBroadcastChannel() { + return new BroadcastNetChannel( + playerList.stream() + .filter(H2Player::isOnline) + .map(player -> (Player)player) + ); + } + + @Override + public Player getOfflinePlayer(String name) { + return playerList.stream() + .filter(player -> player.getName().equals(name)) + .filter(player -> !player.isOnline()) + .findFirst().orElseGet(() -> h2PlayerService.getByName(name)); + } +} diff --git a/h2_playermanager/src/main/java/mc/core/h2db/entity/H2PlayerEntity.java b/h2_playermanager/src/main/java/mc/core/h2db/entity/H2PlayerEntity.java new file mode 100644 index 0000000..86e9cda --- /dev/null +++ b/h2_playermanager/src/main/java/mc/core/h2db/entity/H2PlayerEntity.java @@ -0,0 +1,102 @@ +package mc.core.h2db.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; +import mc.core.EntityLocation; +import mc.core.h2db.H2Player; +import mc.core.world.World; +import org.hibernate.annotations.GenericGenerator; +import org.springframework.context.ApplicationContext; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +@Table(name = "players", + indexes = {@Index(name = "idx_players_uuid", columnList = "uuid", unique = true), + @Index(name = "idx_players_name", columnList = "name")}) +@NoArgsConstructor +@Data +public class H2PlayerEntity { + @Id + @GeneratedValue(generator = "increment") + @GenericGenerator(name= "increment", strategy= "increment") + @Column(nullable = false) + private Long id; + + @Column(length = 36, nullable = false) + private String uuid; + + @Column(length = 16, nullable = false) + private String name; + + @Column(name = "location_x", nullable = false) + private Double locationX; + + @Column(name = "location_y", nullable = false) + private Double locationY; + + @Column(name = "location_z", nullable = false) + private Double locationZ; + + @Column(name = "location_yaw", nullable = false) + private Float locationYaw; + + @Column(name = "location_pitch", nullable = false) + private Float locationPitch; + + @Column(name = "location_world", length = 64, nullable = false) + private String locationWorld; + + public H2PlayerEntity(H2Player player) { + this.id = (long) player.getId(); + setUuid(player.getUuid().toString()); + setName(this.name = player.getName()); + this.locationX = player.getLocation().getX(); + this.locationY = player.getLocation().getY(); + this.locationZ = player.getLocation().getZ(); + this.locationYaw = player.getLocation().getYaw(); + this.locationPitch = player.getLocation().getPitch(); + this.locationWorld = player.getWorld().getName(); + } + + public void setUuid(String uuid) { + if (uuid == null || uuid.trim().isEmpty()) { + this.uuid = null; + } else { + this.uuid = uuid; + } + } + + public void setName(String name) { + if (name == null || name.trim().isEmpty()) { + this.name = null; + } else { + this.name = name; + } + } + + public H2Player toPlayer(ApplicationContext context) { + H2Player player = new H2Player(); + return toPlayer(player, context); + } + + public H2Player toPlayer(H2Player player, ApplicationContext context) { + player.setId(this.id.intValue()); + player.setUuid(UUID.fromString(this.uuid)); + player.setName(this.name); + if (player.getLocation() == null) { + player.setLocation(new EntityLocation( + this.locationX, this.locationY, this.locationZ, + this.locationYaw, this.locationPitch + )); + } else { + player.getLocation().setXYZ(this.locationX, this.locationY, this.locationZ); + player.getLocation().setYawPitch(this.locationYaw, this.locationPitch); + } + + player.setWorld(context.getBean(this.locationWorld, World.class)); + + return player; + } +} diff --git a/h2_playermanager/src/main/java/mc/core/h2db/repository/H2PlayerEntityRepository.java b/h2_playermanager/src/main/java/mc/core/h2db/repository/H2PlayerEntityRepository.java new file mode 100644 index 0000000..47b4f45 --- /dev/null +++ b/h2_playermanager/src/main/java/mc/core/h2db/repository/H2PlayerEntityRepository.java @@ -0,0 +1,12 @@ +package mc.core.h2db.repository; + +import mc.core.h2db.entity.H2PlayerEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface H2PlayerEntityRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/h2_playermanager/src/main/java/mc/core/h2db/service/H2PlayerService.java b/h2_playermanager/src/main/java/mc/core/h2db/service/H2PlayerService.java new file mode 100644 index 0000000..a20fb7b --- /dev/null +++ b/h2_playermanager/src/main/java/mc/core/h2db/service/H2PlayerService.java @@ -0,0 +1,11 @@ +package mc.core.h2db.service; + +import mc.core.h2db.H2Player; + +public interface H2PlayerService { + H2Player save(H2Player player); + void remove(H2Player player); + + H2Player getByName(String name); + H2Player getById(int id); +} diff --git a/h2_playermanager/src/main/java/mc/core/h2db/service/H2PlayerServiceImpl.java b/h2_playermanager/src/main/java/mc/core/h2db/service/H2PlayerServiceImpl.java new file mode 100644 index 0000000..b920fbf --- /dev/null +++ b/h2_playermanager/src/main/java/mc/core/h2db/service/H2PlayerServiceImpl.java @@ -0,0 +1,44 @@ +package mc.core.h2db.service; + +import mc.core.h2db.H2Player; +import mc.core.h2db.entity.H2PlayerEntity; +import mc.core.h2db.repository.H2PlayerEntityRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class H2PlayerServiceImpl implements H2PlayerService { + @Autowired + private ApplicationContext context; + @Autowired + private H2PlayerEntityRepository h2PlayerEntityRepository; + + @Override + public H2Player save(H2Player player) { + H2PlayerEntity entity = new H2PlayerEntity(player); + //TODO возможно имеет смысл здесь оптимизация + //вместо toPlayer() сделать toPlayer(H2Player) который в существующий + //будет дописывать/обновлять данные + return h2PlayerEntityRepository.saveAndFlush(entity).toPlayer(player, context); + } + + @Override + public void remove(H2Player player) { + h2PlayerEntityRepository.deleteById((long) player.getId()); + } + + @Override + public H2Player getByName(String name) { + Optional optEntity = h2PlayerEntityRepository.findByName(name); + return optEntity.map(entiry -> entiry.toPlayer(context)).orElse(null); + } + + @Override + public H2Player getById(int id) { + Optional optEntity = h2PlayerEntityRepository.findById((long) id); + return optEntity.map(entiry -> entiry.toPlayer(context)).orElse(null); + } +} diff --git a/h2_playermanager/src/test/java/mc/core/h2db/H2PlayerManagerTest.java b/h2_playermanager/src/test/java/mc/core/h2db/H2PlayerManagerTest.java new file mode 100644 index 0000000..dd25625 --- /dev/null +++ b/h2_playermanager/src/test/java/mc/core/h2db/H2PlayerManagerTest.java @@ -0,0 +1,159 @@ +package mc.core.h2db; + +import mc.core.EntityLocation; +import mc.core.h2db.service.H2PlayerService; +import mc.core.player.Player; +import mc.core.world.World; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {TestSpringConfig.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class H2PlayerManagerTest { + @Autowired + private H2PlayerService h2PlayerService; + @Autowired + private World mockWorld; + @Autowired + private H2PlayerManager playerManager; + + @Test + void createPlayer() { + final String playerName = "NEW_PLAYER"; + final Player newPlayer = playerManager.createPlayer(playerName, EntityLocation.ZERO(), mockWorld); + + assertNotNull(newPlayer); + assertEquals(H2Player.class, newPlayer.getClass()); + assertTrue(newPlayer.getId() > 0); + + final H2Player queryPlayer = h2PlayerService.getByName(playerName); + assertTrue(queryPlayer.getId() > 0); + + assertEquals(newPlayer, queryPlayer); + assertEquals(newPlayer.getName(), queryPlayer.getName()); + assertEquals(newPlayer.getLocation(), queryPlayer.getLocation()); + assertEquals(newPlayer.getWorld(), queryPlayer.getWorld()); + } + + @Test + void joinServer() { + assertEquals(0, playerManager.getCountPlayers()); + + final String playerName = "NEW_PLAYER"; + final Player player = playerManager.createPlayer(playerName, EntityLocation.ZERO(), mockWorld); + playerManager.joinServer(player); + + assertEquals(1, playerManager.getCountPlayers()); + assertTrue(player.isOnline()); + } + + @Test + void leftServer() { + assertEquals(0, playerManager.getCountPlayers()); + + final String playerName = "NEW_PLAYER"; + final Player player = playerManager.createPlayer(playerName, EntityLocation.ZERO(), mockWorld); + playerManager.joinServer(player); + + assertEquals(1, playerManager.getCountPlayers()); + assertTrue(player.isOnline()); + + final int playerId = player.getId(); + + final String anotherName = "ANOTHER_NAME"; + ((H2Player)player).setName(anotherName); + + playerManager.leftServer(player); + + assertEquals(0, playerManager.getCountPlayers()); + + assertFalse(player.isOnline()); + assertTrue(player.getLoadedChunks().isEmpty()); + + final H2Player queryPlayer = h2PlayerService.getById(playerId); + + assertNotNull(queryPlayer); + ((H2Player)player).setId(playerId); + assertEquals(player, queryPlayer); + } + + @Test + void getPlayer() { + assertEquals(0, playerManager.getCountPlayers()); + + final String playerName = "NEW_PLAYER"; + final Player player = playerManager.createPlayer(playerName, EntityLocation.ZERO(), mockWorld); + assertNotNull(player); + + playerManager.joinServer(player); + + assertEquals(1, playerManager.getCountPlayers()); + + Player queryPlayer = playerManager.getPlayer(playerName); + + assertEquals(player, queryPlayer); + } + + @Test + void getPlayers() { + assertEquals(0, playerManager.getCountPlayers()); + + final String playerName = "NEW_PLAYER"; + final Player player = playerManager.createPlayer(playerName, EntityLocation.ZERO(), mockWorld); + assertNotNull(player); + + playerManager.joinServer(player); + + assertEquals(1, playerManager.getCountPlayers()); + + List players = playerManager.getPlayers(); + assertEquals(1, players.size()); + try { + players.add(new H2Player()); + fail(); + } catch (Exception e) { + assertTrue(true); + } + + assertEquals(player, players.get(0)); + } + + @Test + void getCountPlayers() { + assertEquals(0, playerManager.getCountPlayers()); + + final String playerName = "NEW_PLAYER"; + final Player player = playerManager.createPlayer(playerName, EntityLocation.ZERO(), mockWorld); + assertNotNull(player); + + playerManager.joinServer(player); + + assertEquals(1, playerManager.getCountPlayers()); + + playerManager.leftServer(player); + + assertEquals(0, playerManager.getCountPlayers()); + } + + @Test + void getOfflinePlayer() { + final String playerName = "NEW_PLAYER"; + final Player player = playerManager.createPlayer(playerName, EntityLocation.ZERO(), mockWorld); + playerManager.joinServer(player); + playerManager.leftServer(player); + + assertEquals(0, playerManager.getCountPlayers()); + + Player offlinePlayer = playerManager.getOfflinePlayer(playerName); + assertEquals(player, offlinePlayer); + } +} diff --git a/h2_playermanager/src/test/java/mc/core/h2db/H2PlayerTest.java b/h2_playermanager/src/test/java/mc/core/h2db/H2PlayerTest.java new file mode 100644 index 0000000..271fae7 --- /dev/null +++ b/h2_playermanager/src/test/java/mc/core/h2db/H2PlayerTest.java @@ -0,0 +1,36 @@ +package mc.core.h2db; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class H2PlayerTest { + @Test + void equals_() { + UUID uuid = UUID.randomUUID(); + + H2Player player1 = new H2Player(); + player1.setId(1); + player1.setUuid(uuid); + player1.setName("Player1"); + + H2Player player2 = new H2Player(); + player2.setId(1); + player2.setUuid(uuid); + player2.setName("Player2"); + + assertEquals(player1, player2); + + player2.setId(2); + + assertNotEquals(player1, player2); + + player2.setId(1); + player2.setUuid(UUID.randomUUID()); + + assertNotEquals(player1, player2); + } +} diff --git a/h2_playermanager/src/test/java/mc/core/h2db/TestSpringConfig.java b/h2_playermanager/src/test/java/mc/core/h2db/TestSpringConfig.java new file mode 100644 index 0000000..8dd5f80 --- /dev/null +++ b/h2_playermanager/src/test/java/mc/core/h2db/TestSpringConfig.java @@ -0,0 +1,90 @@ +package mc.core.h2db; + +import mc.core.h2db.service.H2PlayerService; +import mc.core.world.World; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Properties; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Configuration +@EnableJpaRepositories +@EnableTransactionManagement +@ComponentScan("mc.core.h2db") +public class TestSpringConfig { + private static final String DATABASE_DRIVER = "org.h2.Driver"; + private static final String DATABASE_URL = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"; + private static final String DATABASE_USERNAME = "sa"; + private static final String DATABASE_PASSWORD = "s3cReT"; + + static { + System.setProperty("org.jboss.logging.provider", "slf4j"); + } + + private Properties hibernateProp() { + Properties properties = new Properties(); + properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); + properties.put("hibernate.show_sql", "true"); + properties.put("hibernate.format_sql", "true"); + properties.put("hibernate.use_sql_comments", "true"); + properties.put("hibernate.hbm2ddl.auto", "create"); + + return properties; + } + + @Bean("mockWorld") + public World mockWorld() { + World mockWorld = mock(World.class); + when(mockWorld.getName()).thenReturn("mockWorld"); + return mockWorld; + } + + @Bean + public DataSource dataSource() { + DriverManagerDataSource dmds = new DriverManagerDataSource(); + dmds.setDriverClassName(DATABASE_DRIVER); + dmds.setUrl(DATABASE_URL); + dmds.setUsername(DATABASE_USERNAME); + dmds.setPassword(DATABASE_PASSWORD); + + return dmds; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); + entityManagerFactoryBean.setPackagesToScan("mc.core.h2db.entity"); + entityManagerFactoryBean.setJpaProperties(hibernateProp()); + + return entityManagerFactoryBean; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory); + + return transactionManager; + } + + @Bean + public H2PlayerManager h2PlayerManager(H2PlayerService h2PlayerService) { + H2PlayerManager playerManager = new H2PlayerManager(); + playerManager.setH2PlayerService(h2PlayerService); + return playerManager; + } +} diff --git a/h2_playermanager/src/test/java/mc/core/h2db/service/H2PlayerServiceTest.java b/h2_playermanager/src/test/java/mc/core/h2db/service/H2PlayerServiceTest.java new file mode 100644 index 0000000..464e0e4 --- /dev/null +++ b/h2_playermanager/src/test/java/mc/core/h2db/service/H2PlayerServiceTest.java @@ -0,0 +1,155 @@ +package mc.core.h2db.service; + +import mc.core.EntityLocation; +import mc.core.h2db.H2Player; +import mc.core.h2db.TestSpringConfig; +import mc.core.world.World; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {TestSpringConfig.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class H2PlayerServiceTest { + @Autowired + private H2PlayerService h2PlayerService; + @Autowired + private World world; + + private H2Player buildPlayer() { + final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + final double minD = 0.0d, maxD = 10.0d; + final float minF = 0.0f, maxF = 359.9f; + final int minI = 1000, maxI = 9999; + + final H2Player player = new H2Player(); + player.setUuid(UUID.randomUUID()); + player.setName("player" + rnd.nextInt(minI, maxI)); + player.setLocation(new EntityLocation( + rnd.nextDouble(minD, maxD), + rnd.nextDouble(minD, maxD), + rnd.nextDouble(minD, maxD), + rnd.nextFloat() * (maxF - minF) + minF, + rnd.nextFloat() * (maxF - minF) + minF + )); + player.setWorld(world); + + return player; + } + + private void assertPlayers(H2Player expected, H2Player actual) { + assertEquals(expected, actual); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getLocation(), actual.getLocation()); + assertNotNull(actual.getWorld()); + assertEquals(expected.getWorld(), actual.getWorld()); + } + + @Test + void save() { + H2Player player = buildPlayer(); + H2Player savedPlayer = h2PlayerService.save(player); + + player.setId(savedPlayer.getId()); //FIXME костыль, однако + assertPlayers(player, savedPlayer); + } + + @Test + void save_NameEmpty() { + assertThrows(Exception.class, () -> { + H2Player player = buildPlayer(); + player.setName(""); + h2PlayerService.save(player); + }); + } + + @Test + void save_NameNull() { + assertThrows(Exception.class, () -> { + H2Player player = buildPlayer(); + player.setName(null); + h2PlayerService.save(player); + }); + } + + @Test + void save_UuidNull() { + assertThrows(Exception.class, () -> { + H2Player player = buildPlayer(); + player.setUuid(null); + h2PlayerService.save(player); + }); + } + + @Test + void save_LocationNull() { + assertThrows(Exception.class, () -> { + H2Player player = buildPlayer(); + player.setLocation(null); + h2PlayerService.save(player); + }); + } + + @Test + void remove() { + H2Player player = h2PlayerService.save(buildPlayer()); + h2PlayerService.remove(player); + + H2Player player2 = h2PlayerService.getById(player.getId()); + assertNull(player2); + } + + @Test + void remove_NotExists() { + assertThrows(Exception.class, () -> { + H2Player player = h2PlayerService.save(buildPlayer()); + h2PlayerService.remove(player); + h2PlayerService.remove(player); + }); + } + + @Test + void getByName() { + H2Player player = h2PlayerService.save(buildPlayer()); + + H2Player player2 = h2PlayerService.getByName(player.getName()); + assertPlayers(player, player2); + } + + @Test + void getByName_NotExists() { + assertNull(h2PlayerService.getByName("UNKNOW_PLAYER")); + } + + @Test + void getByName_Empty() { + assertNull(h2PlayerService.getByName("")); + } + + @Test + void getByName_Null() { + assertNull(h2PlayerService.getByName(null)); + } + + @Test + void getById() { + H2Player player = h2PlayerService.save(buildPlayer()); + + H2Player player2 = h2PlayerService.getById(player.getId()); + assertPlayers(player, player2); + } + + @Test + void getById_NotExists() { + assertNull(h2PlayerService.getById(9999)); + } +} diff --git a/proto_1.12.2/README.MD b/proto_1.12.2/README.MD new file mode 100644 index 0000000..5b7a85a --- /dev/null +++ b/proto_1.12.2/README.MD @@ -0,0 +1,3 @@ +# Protocol 1.12.2 + +Описание протокола версии [1.12.2 (340)](https://wiki.vg/index.php?title=Protocol&oldid=14204) diff --git a/proto_1.12.2/build.gradle b/proto_1.12.2/build.gradle new file mode 100644 index 0000000..d443034 --- /dev/null +++ b/proto_1.12.2/build.gradle @@ -0,0 +1,9 @@ +version '0.1' + +dependencies { + /* Core */ + compile_excludeCopy project(':core') + + /* Components */ + compile (group: 'com.google.code.gson', name: 'gson', version: '2.8.5') +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStream.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStream.java new file mode 100644 index 0000000..7a2b7d6 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStream.java @@ -0,0 +1,71 @@ +package mc.core.network.proto_1_12_2; + +import java.io.ByteArrayOutputStream; + +public class ByteArrayOutputNetStream extends NetOutputStream_p340 { + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + @Override + public void writeBoolean(boolean value) { + baos.write(value ? 1 : 0); + } + + @Override + public void writeByte(int value) { + baos.write(value); + } + + @Override + public void writeUnsignedByte(int value) { + baos.write((byte)(value & 0xFF)); + } + + @Override + public void writeBytes(byte[] buffer, int offset, int lengtn) { + baos.write(buffer, offset, lengtn); + } + + @Override + public void writeShort(int value) { + baos.write((byte) (value >>> 8)); + baos.write((byte) value); + } + + @Override + public void writeInt(int value) { + baos.write((value >>> 24) & 0xFF); + baos.write((value >>> 16) & 0xFF); + baos.write((value >>> 8) & 0xFF); + baos.write(value & 0xFF); + } + + @Override + public void writeLong(long value) { + baos.write((int) ((value >>> 56) & 0xFF)); + baos.write((int) ((value >>> 48) & 0xFF)); + baos.write((int) ((value >>> 40) & 0xFF)); + baos.write((int) ((value >>> 32) & 0xFF)); + baos.write((int) ((value >>> 24) & 0xFF)); + baos.write((int) ((value >>> 16) & 0xFF)); + baos.write((int) ((value >>> 8) & 0xFF)); + baos.write((int) (value & 0xFF)); + } + + @Override + public void writeFloat(float value) { + writeInt(Float.floatToIntBits(value)); + } + + @Override + public void writeDouble(double value) { + writeLong(Double.doubleToLongBits(value)); + } + + public int size() { + return baos.size(); + } + + public byte[] toByteArray() { + return baos.toByteArray(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/Direction.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/Direction.java new file mode 100644 index 0000000..8deb848 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/Direction.java @@ -0,0 +1,26 @@ +package mc.core.network.proto_1_12_2; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +@RequiredArgsConstructor +public enum Direction { + BOTTOM(0), // -Y + TOP(1), // +Y + NORTH(2), // -Z + SOUTH(3), // +Z + WEST(4), // -X + EAST(5); // +X + + public static Direction getById(final int id) { + return Arrays.stream(Direction.values()) + .filter(direction -> direction.id == id) + .findFirst() + .orElse(null); + } + + @Getter + private final int id; +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/NetInputStream_p340.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/NetInputStream_p340.java new file mode 100644 index 0000000..6be6f48 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/NetInputStream_p340.java @@ -0,0 +1,82 @@ +package mc.core.network.proto_1_12_2; + +import com.flowpowered.nbt.Tag; +import com.flowpowered.nbt.stream.NBTInputStream; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.NetInputStream; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public abstract class NetInputStream_p340 extends NetInputStream { + private NBTInputStream nbtInputStream; + + @Override + public int readVarInt(AtomicInteger countReadBytes) { + int numRead = 0; + int result = 0; + byte read; + do { + if ((numRead+1) > 5) { + log.warn("VarInt is too big"); + break; + } + read = readByte(); + int value = (read & 0b01111111); + result |= (value << (7 * numRead)); + + numRead++; + } while ((read & 0b10000000) != 0); + + if (countReadBytes != null) { + countReadBytes.set(numRead); + } + + return result; + } + + @Override + public int readVarInt() { + return readVarInt(null); + } + + @Override + public String readString() { + int size = readVarInt(); + if (size == 0) { + log.warn("String zero length??"); + return ""; + } + + byte[] bytes = new byte[size]; + readBytes(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public UUID readUUID() { + return new UUID(readLong(), readLong()); + } + + @Override + public Tag readNBT() { + if (nbtInputStream == null) { + try { + nbtInputStream = new NBTInputStream(this, false); + } catch (IOException e) { + log.error("Create NBT stream", e); + return null; + } + } + + try { + return nbtInputStream.readTag(); + } catch (IOException e) { + log.error("Read NBT", e); + return null; + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/NetOutputStream_p340.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/NetOutputStream_p340.java new file mode 100644 index 0000000..396ec74 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/NetOutputStream_p340.java @@ -0,0 +1,63 @@ +package mc.core.network.proto_1_12_2; + +import com.flowpowered.nbt.Tag; +import com.flowpowered.nbt.stream.NBTOutputStream; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.NetOutputStream; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@Slf4j +public abstract class NetOutputStream_p340 extends NetOutputStream { + private NBTOutputStream nbtOutputStream; + + @Override + public void writeVarInt(int value) { + while ((value & -128) != 0) { + writeByte(value & 127 | 128); + value >>>= 7; + } + + writeByte(value); + } + + @Override + public void writeString(String value) { + if (value.length() > Short.MAX_VALUE) { + log.warn("String \"{}\" too long!", value); + byte[] buf = value.substring(0, Short.MAX_VALUE).getBytes(StandardCharsets.UTF_8); + writeVarInt(Short.MAX_VALUE); + writeBytes(buf); + } else { + byte[] buf = value.getBytes(StandardCharsets.UTF_8); + writeVarInt(value.length()); + writeBytes(buf); + } + } + + @Override + public void writeUUID(UUID uuid) { + writeLong(uuid.getMostSignificantBits()); + writeLong(uuid.getLeastSignificantBits()); + } + + @Override + public void writeNBT(Tag tag) { + if (nbtOutputStream == null) { + try { + nbtOutputStream = new NBTOutputStream(this, false); + } catch (IOException e) { + log.error("Create NBT stream", e); + return; + } + } + + try { + nbtOutputStream.writeTag(tag); + } catch (IOException e) { + log.error("Write NBT", e); + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/State.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/State.java new file mode 100644 index 0000000..152dfcb --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/State.java @@ -0,0 +1,124 @@ +package mc.core.network.proto_1_12_2; + +import com.google.common.collect.ImmutableMap; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.CSPacket; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.packets.*; +import mc.core.network.proto_1_12_2.packets.clientside.*; +import mc.core.network.proto_1_12_2.packets.serverside.*; + +import java.util.Map; + +/** + * Для каждого состояния протокола имеется свой набор пакетов. + */ +@Slf4j +@RequiredArgsConstructor +public enum State { + /** + * Не известная стадия. + * Переход на этут стадию является следствием ошибки в работе протокола (умышленного или нет) + */ + UNKNOWN(-1, ImmutableMap.of(), ImmutableMap.of()), + + /** + * Рукопожатие. + * С этого состояния начинается сюбое соединение с сервером. + */ + HANDSHAKE(0, + ImmutableMap.>builder() + .put(0x00, HandshakePacket.class) + .build(), + null + ), + + /** + * Информация о сервере. + * Используется для получения Motd, кол-ва слотов и т.д. + */ + STATUS(1, + ImmutableMap.>builder() + .put(0x00, StatusRequestPacket.class) + .put(0x01, PingPacket.class) + .build(), + ImmutableMap., Integer>builder() + .put(StatusResponsePacket.class, 0x00) + .put(PingPacket.class, 0x01) + .build() + ), + + /** + * Стадия логина/авторизации. + */ + LOGIN(2, + ImmutableMap.>builder() + .put(0x00, LoginStartPacket.class) + .build(), + ImmutableMap., Integer>builder() + .put(DisconnectPacket.class, 0x00) + .put(LoginSuccessPacket.class, 0x02) + .build() + ), + + /** + * Игровая стадия. + * Основная стадия протокола. + */ + PLAY(3, + ImmutableMap.>builder() + .put(0x00, TeleportConfirmPacket.class) + .put(0x02, ChatMessageClientPacket.class) + .put(0x04, ClientSettingsPacket.class) + .put(0x09, PluginMessagePacket.class) + .put(0x0B, KeepAlivePacket.class) + .put(0x0D, PlayerPositionPacket.class) + .put(0x0E, PlayerPositionAndLookPacket.class) + .put(0x0F, PlayerLookPacket.class) + .put(0x13, PlayerAbilitiesPacket.class) + .put(0x1A, HeldItemChangePacket.class) + .build(), + ImmutableMap., Integer>builder() + .put(BossBarPacket.class, 0x0C) + .put(ChatMessageServerPacket.class, 0x0F) + .put(PluginMessagePacket.class, 0x18) + .put(UnloadChunkPacket.class, 0x1D) + .put(KeepAlivePacket.class, 0x1F) + .put(ChunkDataPacket.class, 0x20) + .put(JoinGamePacket.class, 0x23) + .put(PlayerAbilitiesPacket.class, 0x2C) + .put(PlayerListItemPacket.class, 0x2E) + .put(PlayerPositionAndLookPacket.class, 0x2F) + .put(SpawnPositionPacket.class, 0x46) + .put(TimeUpdatePacket.class, 0x47) + .put(TitlePacket.class, 0x48) + .put(PlayerListHeaderAndFooterPacket.class, 0x4A) + .build() + ); + + public static State valueOf(int id) { + if (id == 0) return HANDSHAKE; + else if (id == 1) return STATUS; + else if (id == 2) return LOGIN; + else if (id == 3) return PLAY; + else { + log.warn("Unknown state: {}", id); + return UNKNOWN; + } + } + + @Getter + private final int id; + private final Map> clientSidePacketsMap; + private final Map, Integer> serverSidePacketsMap; + + public Class getClientSidePacket(int id) { + return clientSidePacketsMap.get(id); + } + + public Integer getServerSidePacket(Class clazz) { + return serverSidePacketsMap.get(clazz); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/TeleportManager.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/TeleportManager.java new file mode 100644 index 0000000..50a15da --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/TeleportManager.java @@ -0,0 +1,51 @@ +package mc.core.network.proto_1_12_2; + +import lombok.AllArgsConstructor; +import mc.core.EntityLocation; +import mc.core.player.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class TeleportManager { + private static TeleportManager instance = new TeleportManager(); + + public static TeleportManager getInstance() { + return instance; + } + + @AllArgsConstructor + private class TpData { + public Player player; + public EntityLocation newLocation; + // TODO необходимо добавить TimeStamp, что бы понимать, когда клиент отвергнул телепортацию + // т.е. идея такова: долгое молчание клиента знак отвержения телепортации. + } + + private final Random RAND = new Random(); + private final Map teleportMap = new HashMap<>(); + + private TeleportManager() {} + + public int append(Player player, EntityLocation location) { + int teleportId; + do { + teleportId = RAND.nextInt(9999); + } while (teleportMap.containsKey(teleportId)); + + teleportMap.put(teleportId, new TpData(player, location.clone())); + return teleportId; + } + + public void apply(int teleportId) { + if (teleportMap.containsKey(teleportId)) { + TpData data = teleportMap.remove(teleportId); + data.player.getLocation().set(data.newLocation); + } + } + + public void removeDataPlayer(Player player) { + teleportMap.entrySet().removeIf(entry -> entry.getValue().player.equals(player)); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/KeepAlivePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/KeepAlivePacket.java new file mode 100644 index 0000000..4febdd3 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/KeepAlivePacket.java @@ -0,0 +1,28 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@ToString +public class KeepAlivePacket implements CSPacket, SCPacket { + private long payload; + + @Override + public void readSelf(NetInputStream netStream) { + this.payload = netStream.readLong(); + } + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeLong(this.payload); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PingPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PingPacket.java new file mode 100644 index 0000000..c567838 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PingPacket.java @@ -0,0 +1,22 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@ToString +public class PingPacket implements CSPacket, SCPacket { + private long payload; + + @Override + public void readSelf(NetInputStream netStream) { + this.payload = netStream.readLong(); + } + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeLong(payload); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacket.java new file mode 100644 index 0000000..dbb8b74 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacket.java @@ -0,0 +1,54 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@NoArgsConstructor +@Getter +@Setter +@ToString +public class PlayerAbilitiesPacket implements SCPacket, CSPacket { + private static final byte $GOD_MODE_MASK = 0x01, + $FLYING_MASK = 0x02, + $CAN_FLY_MASK = 0x04, + $IDB_MASK = 0x08; + + private boolean godMode = false; + private boolean flying = false; + private boolean canFly = false; + private boolean instantDestroyBlocks = false; + private float flyingSpeed = 0.05f; + private float fieldOfView = flyingSpeed; + private float walkingSpeed; + + @Override + public void writeSelf(NetOutputStream netStream) { + byte flag = 0; + if (godMode) flag = (byte)(flag | $GOD_MODE_MASK); + if (flying) flag = (byte)(flag | $FLYING_MASK); + if (canFly) flag = (byte)(flag | $CAN_FLY_MASK); + if (instantDestroyBlocks) flag = (byte)(flag | $IDB_MASK); + + netStream.writeByte(flag); + netStream.writeFloat(flyingSpeed); + netStream.writeFloat(fieldOfView); + } + + @Override + public void readSelf(NetInputStream netStream) { + byte flag = netStream.readByte(); + godMode = (flag & $GOD_MODE_MASK) > 0; + canFly = (flag & $CAN_FLY_MASK) > 0; + flying = (flag & $FLYING_MASK) > 0; + instantDestroyBlocks = (flag & $IDB_MASK) > 0; + + flyingSpeed = netStream.readFloat(); + walkingSpeed = netStream.readFloat(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerPositionAndLookPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerPositionAndLookPacket.java new file mode 100644 index 0000000..99b11df --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerPositionAndLookPacket.java @@ -0,0 +1,52 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.EntityLocation; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@NoArgsConstructor +@Getter +@ToString +public class PlayerPositionAndLookPacket implements SCPacket, CSPacket { + @Setter + private EntityLocation location; + @Setter + private int teleportId; + private boolean onGround = false; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeDouble(location.getX()); + netStream.writeDouble(location.getY()); + netStream.writeDouble(location.getZ()); + netStream.writeFloat(location.getYaw()); + netStream.writeFloat(location.getPitch()); + netStream.writeByte(0); // It's a bitfield, X/Y/Z/Y_ROT/X_ROT. If X is setXYZ, the x value is relative and not absolute. + /* X - 0x01 + * Y - 0x02 + * Z - 0x04 + * Y_ROT - 0x08 + * X_ROT - 0x10 + */ + netStream.writeVarInt(teleportId); // Client should confirm this packet with Teleport Confirm containing the same Teleport ID + } + + @Override + public void readSelf(NetInputStream netStream) { + this.location = new EntityLocation( + netStream.readDouble(), + netStream.readDouble(), + netStream.readDouble(), + netStream.readFloat(), + netStream.readFloat() + ); + + this.onGround = netStream.readBoolean(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PluginMessagePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PluginMessagePacket.java new file mode 100644 index 0000000..fe8763c --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PluginMessagePacket.java @@ -0,0 +1,30 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.*; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@ToString +public class PluginMessagePacket implements SCPacket, CSPacket { + private String channelName; + private byte[] data; + + @Override + public void readSelf(NetInputStream netStream) { + channelName = netStream.readString(); + data = new byte[netStream.getDataSize() - channelName.getBytes().length - 1]; + netStream.readBytes(data); + } + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeString(channelName); + netStream.writeBytes(data); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/ChatMessageClientPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/ChatMessageClientPacket.java new file mode 100644 index 0000000..2388a18 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/ChatMessageClientPacket.java @@ -0,0 +1,17 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.Getter; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +@Getter +@ToString +public class ChatMessageClientPacket implements CSPacket { + private String message; + + @Override + public void readSelf(NetInputStream netStream) { + this.message = netStream.readString(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/ClientSettingsPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/ClientSettingsPacket.java new file mode 100644 index 0000000..19588cb --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/ClientSettingsPacket.java @@ -0,0 +1,44 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +@NoArgsConstructor +@Getter +@ToString +public class ClientSettingsPacket implements CSPacket { + private String locale; + private int viewDistance; + private int chatMode; + private boolean chatColors; + private boolean capeEnabled, + jacketEnabled, + leftSleeveEnabled, + rightSleeveEnabled, + leftPantsLegEnabled, + rightPantsLegEnabled, + hatEnabled; + private int mainHand; + + @Override + public void readSelf(NetInputStream netStream) { + locale = netStream.readString(); + viewDistance = netStream.readByte(); + chatMode = netStream.readVarInt(); + chatColors = netStream.readBoolean(); + + int bitmask = netStream.readUnsignedByte(); + capeEnabled = (bitmask & 0x01) > 0; + jacketEnabled = (bitmask & 0x02) > 0; + leftSleeveEnabled = (bitmask & 0x04) > 0; + rightSleeveEnabled = (bitmask & 0x08) > 0; + leftPantsLegEnabled = (bitmask & 0x10) > 0; + rightPantsLegEnabled = (bitmask & 0x20) > 0; + hatEnabled = (bitmask & 0x40) > 0; + + mainHand = netStream.readVarInt(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/HandshakePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/HandshakePacket.java new file mode 100644 index 0000000..5d8ba26 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/HandshakePacket.java @@ -0,0 +1,26 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.proto_1_12_2.State; + +@NoArgsConstructor +@Getter +@ToString +public class HandshakePacket implements CSPacket { + private int protocolVersion; + private String address; + private int serverPort; + private State nextState; + + @Override + public void readSelf(NetInputStream netStream) { + this.protocolVersion = netStream.readVarInt(); + this.address = netStream.readString(); + this.serverPort = netStream.readUnsignedShort(); + this.nextState = State.valueOf(netStream.readVarInt()); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/HeldItemChangePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/HeldItemChangePacket.java new file mode 100644 index 0000000..99ea558 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/HeldItemChangePacket.java @@ -0,0 +1,13 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +public class HeldItemChangePacket implements CSPacket { + private int slot; + + @Override + public void readSelf(NetInputStream netStream) { + this.slot = netStream.readShort(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/LoginStartPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/LoginStartPacket.java new file mode 100644 index 0000000..7c98c8f --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/LoginStartPacket.java @@ -0,0 +1,17 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.Getter; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +@Getter +@ToString +public class LoginStartPacket implements CSPacket { + private String playerName; + + @Override + public void readSelf(NetInputStream netStream) { + this.playerName = netStream.readString(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/PlayerLookPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/PlayerLookPacket.java new file mode 100644 index 0000000..a607038 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/PlayerLookPacket.java @@ -0,0 +1,18 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.Getter; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +@Getter +public class PlayerLookPacket implements CSPacket { + private float yaw, pitch; + private boolean onGround; // True if the client is on the ground, false otherwise + + @Override + public void readSelf(NetInputStream netStream) { + this.yaw = netStream.readFloat(); + this.pitch = netStream.readFloat(); + this.onGround = netStream.readBoolean(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/PlayerPositionPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/PlayerPositionPacket.java new file mode 100644 index 0000000..78b18e1 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/PlayerPositionPacket.java @@ -0,0 +1,19 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.Getter; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +@Getter +public class PlayerPositionPacket implements CSPacket { + private double x, y, z; // Y - is feet position. Normally Head Y - +1.62d + private boolean onGround; // True if the client is on the ground, false otherwise + + @Override + public void readSelf(NetInputStream netStream) { + this.x = netStream.readDouble(); + this.y = netStream.readDouble(); + this.z = netStream.readDouble(); + this.onGround = netStream.readBoolean(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/StatusRequestPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/StatusRequestPacket.java new file mode 100644 index 0000000..ab96afc --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/StatusRequestPacket.java @@ -0,0 +1,12 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +@ToString +public class StatusRequestPacket implements CSPacket { + @Override + public void readSelf(NetInputStream netStream) { + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/TeleportConfirmPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/TeleportConfirmPacket.java new file mode 100644 index 0000000..6eb009e --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/clientside/TeleportConfirmPacket.java @@ -0,0 +1,17 @@ +package mc.core.network.proto_1_12_2.packets.clientside; + +import lombok.Getter; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +@Getter +@ToString +public class TeleportConfirmPacket implements CSPacket { + private int teleportId; + + @Override + public void readSelf(NetInputStream netStream) { + teleportId = netStream.readVarInt(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/BossBarPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/BossBarPacket.java new file mode 100644 index 0000000..5eef306 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/BossBarPacket.java @@ -0,0 +1,113 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.serializers.TextToStringConverter; +import mc.core.text.Text; + +import java.util.UUID; + +@Setter +@ToString +public class BossBarPacket implements SCPacket { + @RequiredArgsConstructor + public enum Action { + ADD(0), + REMOVE(1), + UPDATE_HEALTH(2), + UPDATE_TITLE(3), + UPDATE_STYLE(4), + UPDATE_FLAGS(5); + + @Getter + private final int id; + } + + @RequiredArgsConstructor + public enum Color { + PINK(0), + BLUE(1), + RED(2), + GREEN(3), + YELLOW(4), + PURPLE(5), + WHITE(5); + + @Getter + private final int id; + } + + @RequiredArgsConstructor + public enum Division { + NO(0), + _0(0), + _6(1), + _10(2), + _12(3), + _20(4); + + @Getter + private final int id; + } + + @RequiredArgsConstructor + public enum Flag { + NO(0x00), + DAKR_SKY(0x01), + DRAGON_BAR(0x02); + + @Getter + private final int id; + } + + @Getter + @Setter + public static class BarData { + private Text title; + /* + * From 0 to 1. + * Values greater than 1 do not crash a Notchian client, + * and start rendering part of a second health bar at around 1.5. + * (https://i.johni0702.de/nA.png) + */ + private float health; + private Color color; + private Division division; + private Flag flags; + } + + private UUID uuid; // Unique ID for this bar + private Action action; + private BarData barData; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeUUID(uuid); + netStream.writeVarInt(action.id); + + if (action == Action.REMOVE) { + return; + } + + if (action == Action.ADD || action == Action.UPDATE_TITLE) { + netStream.writeString(TextToStringConverter.getInstance().mapping(barData.title)); + } + + if (action == Action.ADD || action == Action.UPDATE_HEALTH) { + netStream.writeFloat(barData.health); + } + + if (action == Action.ADD || action == Action.UPDATE_STYLE) { + netStream.writeVarInt(barData.color.id); + netStream.writeVarInt(barData.division.id); + } + + if (action == Action.ADD || action == Action.UPDATE_FLAGS) { + netStream.writeUnsignedByte(barData.flags.id); + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/ChatMessageServerPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/ChatMessageServerPacket.java new file mode 100644 index 0000000..bb465a5 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/ChatMessageServerPacket.java @@ -0,0 +1,26 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.chat.MessageType; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.serializers.TextToStringConverter; +import mc.core.text.Text; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@ToString +public class ChatMessageServerPacket implements SCPacket { + private Text text; + private MessageType type; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeString(TextToStringConverter.getInstance().mapping(text)); + netStream.writeByte(type.getId()); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/ChunkDataPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/ChunkDataPacket.java new file mode 100644 index 0000000..55d0d7a --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/ChunkDataPacket.java @@ -0,0 +1,309 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import com.flowpowered.nbt.CompoundTag; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream; +import mc.core.utils.NibbleArray; +import mc.core.world.block.Block; +import mc.core.world.block.BlockType; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/* +Packet structure + +- https://wiki.vg/Chunk_Format#Packet_structure + ++------------------------------------------------------+ +| Field | Type | +|--------------------------|---------------------------| +| Chunk X | int | +|--------------------------|---------------------------| +| Chunk Y | int | +|--------------------------|---------------------------| +| Init Chunk | boolean | ("Ground-Up Continuous") +|--------------------------|---------------------------| +| Primary Bit Mask | VarInt | +|--------------------------|---------------------------| +| Size of Data | VarInt | +|--------------------------|---------------------------| +| Data | Byte array | - https://wiki.vg/Chunk_Format#Data_structure +| +------------------------------------------------+ | +| | Chunk Section | Byte array | | - https://wiki.vg/Chunk_Format#Chunk_Section_structure +| | +------------------------------------------+ | | +| | | Bits Per Block | Unsigned Byte | | | (we use 4 bits per block) +| | |--------------------|---------------------| | | +| | | Palette | Byte array | | | - https://wiki.vg/Chunk_Format#Palettes +| | | +------------------------------------+ | | | (we use Indirect type palette) +| | | | Size of palette | VarInt | | | | +| | | |-----------------|------------------| | | | +| | | | Palette | Array of VarInt | | | | +| | | +------------------------------------+ | | | +| | |--------------------|---------------------| | | +| | | Size of Data Array | VarInt | | | +| | |--------------------|---------------------| | | +| | | Data Array | Array of Long | | | +| | |--------------------|---------------------| | | +| | | Block Light | Byte Array | | | (Half byte per block) +| | |--------------------|---------------------| | | +| | | Sky Light | Optional Byte Array | | | (Only if in the Overworld; half byte per block) +| | +------------------------------------------+ | | +| |-----------------------|------------------------| | +| | Biomes | Optional Byte array | | +| +------------------------------------------------+ | +|--------------------------|---------------------------| +| Number of block entities | VarInt | +|--------------------------|---------------------------| +| Block entities | Array of NBT | ++------------------------------------------------------+ + */ + +@Slf4j +@NoArgsConstructor +public class ChunkDataPacket implements SCPacket { + @Setter + private int x; + @Setter + private int z; + @Setter + private boolean initChunk = true; // "Ground-Up Continuous" + private Chunk chunk; + private List sectionList; + + public void setChunk(Chunk chunk) { + this.sectionList = null; + this.chunk = chunk; + } + + public void setChunkSectionList(List sectionList) { + this.chunk = null; + this.sectionList = sectionList; + } + + @Override + public void writeSelf(NetOutputStream netStream) { + if (sectionList == null && chunk == null) { + log.warn("Empty chunk data!"); //TODO для такого нужна заглушка + return; + } + + netStream.writeInt(x); // Chunk X + netStream.writeInt(z); // Chunk Y + netStream.writeBoolean(initChunk); // Init Chunk + + int maxH = 0; + int bitMask = 0; + if (sectionList == null && chunk != null) { + for (int h = 15; h >= 0; h--) { + bitMask = bitMask << 1; + ChunkSection chunkSection = chunk.getChunkSection(h); + if (chunkSection != null && chunkSection.getY() == h) { + bitMask |= 0x01; + maxH++; + } else { + bitMask |= 0x00; + } + } + } else if (sectionList != null && chunk == null) { + sectionList.sort(Comparator.comparingInt(ChunkSection::getY)); + for (int h = 15, i = 0; h >= 0; h--) { + bitMask = bitMask << 1; + ChunkSection chunkSection = sectionList.get(i); + if (chunkSection != null && chunkSection.getY() == h) { + bitMask |= 0x01; + maxH++; + } else { + bitMask |= 0x00; + } + } + } + netStream.writeVarInt(bitMask); // Primary Bit Mask + + final ByteArrayOutputNetStream data = new ByteArrayOutputNetStream(); + + final ByteArrayOutputNetStream biomes = new ByteArrayOutputNetStream(); + boolean biomeWrite = true; + + List nbtList = new ArrayList<>(); + + for (int h = 0; h < maxH; h++) { + ChunkSection chunkSection = null; + + if (chunk != null) { + chunkSection = chunk.getChunkSection(h); + } else if (sectionList != null) { + chunkSection = sectionList.remove(0); + } + + if (chunkSection == null) { + continue; + } + + final PalettedChunkSection palettedChunkSection = new PalettedChunkSection(); + + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + Block block = chunkSection.getBlock(x, y, z); + + palettedChunkSection.addBlock( + block, + chunkSection.getSkyLight(x, y, z) + ); + + CompoundTag nbt = block.getNBTData(); + if (nbt != null) { + nbtList.add(nbt); + } + + if (biomeWrite) { + biomes.writeByte(chunk.getBiome( + (chunk.getX() << 4) + x, + (chunk.getZ() << 4) + z + ).getId()); + if (x == 15 && z == 15) { + biomeWrite = false; + } + } + } + } + } + + // + palettedChunkSection.writeToNetStream(data); + // + } + // + data.writeBytes(biomes.toByteArray()); + // + + netStream.writeVarInt(data.size()); // Size of Data + netStream.writeBytes(data.toByteArray()); // Data + netStream.writeVarInt(nbtList.size()); // Number of block entities + // + for (CompoundTag compoundTag : nbtList) { + netStream.writeNBT(compoundTag); + } + // + } + + @Override + public String toString() { + return "ChunkDataPacket{" + + "x=" + x + + ", z=" + z + + ", chunk=" + chunk + + '}'; + } + + private class PalettedChunkSection { + private List palette = new ArrayList<>(); + private byte[] blocks = new byte[4096]; + private NibbleArray blockLight = new NibbleArray(); + private NibbleArray skyLight = new NibbleArray(); + + private int coordsToIndex(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + private int serializeBlockState(BlockType blockType) { + return (blockType.getId() << 4) | blockType.getMeta(); + } + + byte addBlockType(BlockType blockType) { + int blockState = serializeBlockState(blockType); + + int idx = palette.indexOf(blockState); + if (idx == -1) { + palette.add(blockState); + idx = palette.size()-1; + } + + return (byte) idx; + } + + void addBlock(Block block, int skyLight) { + final int bx = block.getLocation().getX() - ((block.getLocation().getX() >> 4) << 4); + final int by = block.getLocation().getY() - ((block.getLocation().getY() >> 4) << 4); + final int bz = block.getLocation().getZ() - ((block.getLocation().getZ() >> 4) << 4); + + blocks[coordsToIndex(bx, by, bz)] = addBlockType(block.getType()); + blockLight.set(bx, by, bz, block.getLight()); + this.skyLight.set(bx, by, bz, skyLight); + } + + void writeToNetStream(final NetOutputStream netOutputStream) { + int bitsPerBlock = 4; + if (palette.size() > 15) { + if (palette.size() <= 31) + bitsPerBlock = 5; + else if (palette.size() <= 63) + bitsPerBlock = 6; + else if (palette.size() <= 127) + bitsPerBlock = 7; + else if (palette.size() <= 255) + bitsPerBlock = 8; + } + + // + netOutputStream.writeUnsignedByte(bitsPerBlock); // Bits Per Block + netOutputStream.writeVarInt(palette.size()); // Size of palette + palette.forEach(netOutputStream::writeVarInt); // Palette + // + // + final int dataLength = (4096/*16*16*16*/ * bitsPerBlock) / 64/*size of long in bits*/; + netOutputStream.writeVarInt(dataLength); // Size of Data Array + // + long value = 0; + int lastPos = 0; + boolean fairy = false; + long fairyValue = 0; + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + final int blockNumber = (((y << 4) + z) << 4) + x; + final int startLong = ( blockNumber * bitsPerBlock ) / 64; + final int startOffset = ( blockNumber * bitsPerBlock ) % 64; + final int endLong = ((blockNumber + 1) * bitsPerBlock - 1) / 64; + + final long idxBlockInPalette = blocks[coordsToIndex(x, y, z)]; + + if (startLong != lastPos) { + netOutputStream.writeLong(value); + lastPos = startLong; + if (fairy) { + value = fairyValue; + fairy = false; + } else { + value = 0; + } + } + value |= (idxBlockInPalette << startOffset); + + if (startLong != endLong) { + fairyValue = idxBlockInPalette >> (64 - startOffset); + fairy = true; + } + } + } + } + netOutputStream.writeLong(value); + // + // + // + netOutputStream.writeBytes(blockLight.getRawData()); + // + // + netOutputStream.writeBytes(skyLight.getRawData()); + // + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/DisconnectPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/DisconnectPacket.java new file mode 100644 index 0000000..aa9d0a6 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/DisconnectPacket.java @@ -0,0 +1,21 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.serializers.TextToStringConverter; +import mc.core.text.Text; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +public class DisconnectPacket implements SCPacket { + private Text reason; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeString(TextToStringConverter.getInstance().mapping(reason)); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/JoinGamePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/JoinGamePacket.java new file mode 100644 index 0000000..2772287 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/JoinGamePacket.java @@ -0,0 +1,30 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.player.PlayerMode; + +@NoArgsConstructor +@Setter +@ToString +public class JoinGamePacket implements SCPacket { + private int entityId; + private PlayerMode mode; + private int dimension; + private int difficulty; + private String levelType; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeInt(entityId); + netStream.writeUnsignedByte(mode.getId()); + netStream.writeInt(dimension); + netStream.writeUnsignedByte(difficulty); + netStream.writeUnsignedByte(0); // Max Players, unused + netStream.writeString(levelType); + netStream.writeBoolean(false); // Reduced Debug Info + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/LoginSuccessPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/LoginSuccessPacket.java new file mode 100644 index 0000000..62faf07 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/LoginSuccessPacket.java @@ -0,0 +1,25 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +import java.util.UUID; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@ToString +public class LoginSuccessPacket implements SCPacket { + private UUID uuid; + private String playerName; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeString(uuid.toString()); + netStream.writeString(playerName); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/PlayerListHeaderAndFooterPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/PlayerListHeaderAndFooterPacket.java new file mode 100644 index 0000000..3d9780a --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/PlayerListHeaderAndFooterPacket.java @@ -0,0 +1,31 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.serializers.TextToStringConverter; +import mc.core.text.Text; + +@Setter +@ToString +public class PlayerListHeaderAndFooterPacket implements SCPacket { + // To remove the header/footer, send a empty translatable component: {"translate":""} + private Text header; + private Text footer; + + @Override + public void writeSelf(NetOutputStream netStream) { + if (header == null) { + netStream.writeString("{\"translate\":\"\"}"); + } else { + netStream.writeString(TextToStringConverter.getInstance().mapping(header)); + } + + if (footer == null) { + netStream.writeString("{\"translate\":\"\"}"); + } else { + netStream.writeString(TextToStringConverter.getInstance().mapping(footer)); + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/PlayerListItemPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/PlayerListItemPacket.java new file mode 100644 index 0000000..6699865 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/PlayerListItemPacket.java @@ -0,0 +1,80 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.serializers.TextToStringConverter; +import mc.core.player.PlayerMode; +import mc.core.text.Text; + +import java.util.*; + +@Slf4j +@Getter +@Setter +@ToString +public class PlayerListItemPacket implements SCPacket { + @RequiredArgsConstructor + public enum Action { + ADD_PLAYER(0), + UPDATE_GAMEMODE(1), + UPDATE_LATENCY(2), + UPDATE_DISPLAY_NAME(3), + REMOVE_PLAYER(4); + + @Getter + private final int id; + } + + @Data + @ToString + public static class PlayerData { + private UUID uuid; + private String name; + private Properties properties = new Properties(); + private PlayerMode gameMode; + private int ping; + private boolean hasDisplayName = false; + private Text displayName; + } + + private Action action; + private List listPlayers = new ArrayList<>(); + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeVarInt(action.id); + netStream.writeVarInt(listPlayers.size()); + + for (PlayerData playerData : listPlayers) { + netStream.writeUUID(playerData.uuid); + + if (action == Action.ADD_PLAYER) { + netStream.writeString(playerData.name); + netStream.writeVarInt(playerData.properties.size()); + + for (Map.Entry entry : playerData.properties.entrySet()) { + netStream.writeString(entry.getKey().toString()); + netStream.writeString(entry.getValue().toString()); + netStream.writeBoolean(false); // Is Signed + } + } + + if (action == Action.ADD_PLAYER || action == Action.UPDATE_GAMEMODE) { + netStream.writeVarInt(playerData.gameMode.getId()); + } + + if (action == Action.ADD_PLAYER || action == Action.UPDATE_LATENCY) { + netStream.writeVarInt(playerData.ping); + } + + if (action == Action.ADD_PLAYER || action == Action.UPDATE_DISPLAY_NAME) { + netStream.writeBoolean(playerData.hasDisplayName); + if (playerData.hasDisplayName) { + netStream.writeString(TextToStringConverter.getInstance().mapping(playerData.displayName)); + } + } + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/SpawnPositionPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/SpawnPositionPacket.java new file mode 100644 index 0000000..0aa379f --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/SpawnPositionPacket.java @@ -0,0 +1,28 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.EntityLocation; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@ToString +public class SpawnPositionPacket implements SCPacket { + private EntityLocation location; + + private long location2long(EntityLocation entityLocation) { + return (((long) entityLocation.getBlockX() & 0x3FFFFFF) << 38) + | (((long) entityLocation.getBlockY() & 0x0000FFF) << 26) + | ((long) entityLocation.getBlockZ() & 0x3FFFFFF); + } + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeLong(location2long(location)); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/StatusResponsePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/StatusResponsePacket.java new file mode 100644 index 0000000..15a4352 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/StatusResponsePacket.java @@ -0,0 +1,49 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import com.google.gson.JsonObject; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@Setter +@ToString +public class StatusResponsePacket implements SCPacket { + public static final String NAME = "1.12.2"; + public static final int PROTOCOL = 340; + private static final JsonObject versionObj; + + static { + versionObj = new JsonObject(); + versionObj.addProperty("name", NAME); + versionObj.addProperty("protocol", PROTOCOL); + } + + private int maxOnline; + private int online; + private String description; + private byte[] faviconBase64; + + @Override + public void writeSelf(NetOutputStream netStream) { + JsonObject playersObj = new JsonObject(); + playersObj.addProperty("max", maxOnline); + playersObj.addProperty("online", online); + + JsonObject descriptionObj = new JsonObject(); + descriptionObj.addProperty("text", description); + + JsonObject rootObj = new JsonObject(); + rootObj.add("version", versionObj); + rootObj.add("players", playersObj); + rootObj.add("description", descriptionObj); + + if (faviconBase64 != null && faviconBase64.length > 0) { + rootObj.addProperty("favicon", + "data:image/png;base64," + new String(faviconBase64) + ); + } + + netStream.writeString(rootObj.toString()); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/TimeUpdatePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/TimeUpdatePacket.java new file mode 100644 index 0000000..dedda2e --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/TimeUpdatePacket.java @@ -0,0 +1,23 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@ToString +public class TimeUpdatePacket implements SCPacket { + private long time; + private long worldage; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeLong(worldage); + netStream.writeLong(time); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/TitlePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/TitlePacket.java new file mode 100644 index 0000000..f829b5a --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/TitlePacket.java @@ -0,0 +1,113 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.serializers.TextToStringConverter; +import mc.core.text.Text; + +@RequiredArgsConstructor +@Setter +@ToString +public class TitlePacket implements SCPacket { + private static final int TICKS_PER_SEC = 20, + MIN_MS = (1000 / TICKS_PER_SEC); + + @RequiredArgsConstructor + public enum Action { + SET_TITLE(0), + SET_SUBTITLE(1), + SET_ACTION_BAR(2), + SET_DISPLAY_TIME(3), + HIDE(4), + RESET(5); + + @Getter + private final int id; + } + + private final Action action; + private Text text = null; + private Integer fadeInTime = null; + private Integer stayTime = null; + private Integer fadeOutTime = null; + + public TitlePacket(Action action, Object... values) { + if (values.length == 0 && (action != Action.HIDE && action != Action.RESET)) { + this.action = Action.HIDE; + return; + } + + this.action = action; + + switch (this.action) { + case SET_TITLE: + case SET_SUBTITLE: + case SET_ACTION_BAR: + if (values[0] == null) { + this.text = Text.of(); + } else if (values[0] instanceof Text) { + this.text = (Text) values[0]; + } else { + this.text = Text.of(values[0].toString()); + } + break; + case SET_DISPLAY_TIME: + if (values.length < 3) { + this.fadeInTime = 0; + this.stayTime = 0; + this.fadeOutTime = 0; + } else { + if (values[0] instanceof Integer) { + if (((Integer) values[0]) < MIN_MS) { + this.fadeInTime = 1; + } else { + this.fadeInTime = ((Integer) values[0]) / MIN_MS; + } + } else { + this.fadeInTime = 0; + } + + if (values[1] instanceof Integer) { + if (((Integer) values[1]) < MIN_MS) { + this.stayTime = 1; + } else { + this.stayTime = ((Integer) values[1]) / MIN_MS; + } + } else { + this.stayTime = 0; + } + + if (values[2] instanceof Integer) { + if (((Integer) values[2]) < MIN_MS) { + this.fadeOutTime = 1; + } else { + this.fadeOutTime = ((Integer) values[2]) / MIN_MS; + } + } else { + this.fadeOutTime = 0; + } + } + } + } + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeVarInt(action.id); + + switch (action) { + case SET_TITLE: + case SET_SUBTITLE: + case SET_ACTION_BAR: + netStream.writeString(TextToStringConverter.getInstance().mapping(this.text)); + break; + case SET_DISPLAY_TIME: + netStream.writeInt(this.fadeInTime); + netStream.writeInt(this.stayTime); + netStream.writeInt(this.fadeOutTime); + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/UnloadChunkPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/UnloadChunkPacket.java new file mode 100644 index 0000000..4fdbea5 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/serverside/UnloadChunkPacket.java @@ -0,0 +1,16 @@ +package mc.core.network.proto_1_12_2.packets.serverside; + +import lombok.Setter; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +public class UnloadChunkPacket implements SCPacket { + @Setter + private int x, z; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeInt(x); + netStream.writeInt(z); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializer.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializer.java new file mode 100644 index 0000000..9aea2f5 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializer.java @@ -0,0 +1,63 @@ +package mc.core.network.proto_1_12_2.serializers; + +import mc.core.world.block.BlockLocation; + +import static com.google.common.math.IntMath.isPowerOfTwo; + +public class BlockLocationSerializer { + private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[] {0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; + private static final int NUM_X_BITS = 1 + log2(smallestEncompassingPowerOfTwo(30000000)); + private static final int NUM_Z_BITS = NUM_X_BITS; + private static final int NUM_Y_BITS = 64 - NUM_X_BITS - NUM_Z_BITS; + private static final int Y_SHIFT = NUM_Z_BITS; + private static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS; + private static final long X_MASK = (1L << NUM_X_BITS) - 1L; + private static final long Y_MASK = (1L << NUM_Y_BITS) - 1L; + private static final long Z_MASK = (1L << NUM_Z_BITS) - 1L; + + /* + * net.minecraft.util.math.MathHelper#log2(int) + */ + private static int log2(int value) { + return log2DeBruijn(value) - (isPowerOfTwo(value) ? 0 : 1); + } + + /* + * net.minecraft.util.math.MathHelper#log2DeBruijn(int) + */ + private static int log2DeBruijn(int value) { + value = isPowerOfTwo(value) ? value : smallestEncompassingPowerOfTwo(value); + return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int)((long)value * 125613361L >> 27) & 31]; + } + + /* + * net.minecraft.util.math.MathHelper#smallestEncompassingPowerOfTwo(int) + */ + private static int smallestEncompassingPowerOfTwo(int value) { + int i = value - 1; + i = i | i >> 1; + i = i | i >> 2; + i = i | i >> 4; + i = i | i >> 8; + i = i | i >> 16; + return i + 1; + } + + public static long toLong(BlockLocation location) { + return ((long)location.getX() & X_MASK) << X_SHIFT | + ((long)location.getY() & Y_MASK) << Y_SHIFT | + ((long)location.getZ() & Z_MASK); + } + + public static BlockLocation fromLong(long value) { + BlockLocation location = BlockLocation.ZERO(); + fromLong(value, location); + return location; + } + + public static void fromLong(long value, BlockLocation location) { + location.setX((int)(value << 64 - X_SHIFT - NUM_X_BITS >> 64 - NUM_X_BITS)); + location.setY((int)(value << 64 - Y_SHIFT - NUM_Y_BITS >> 64 - NUM_Y_BITS)); + location.setZ((int)(value << 64 - NUM_Z_BITS >> 64 - NUM_Z_BITS)); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/Converter.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/Converter.java new file mode 100644 index 0000000..95a73a6 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/Converter.java @@ -0,0 +1,5 @@ +package mc.core.network.proto_1_12_2.serializers; + +public interface Converter { + T mapping(F fromObject); +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/TextToStringConverter.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/TextToStringConverter.java new file mode 100644 index 0000000..68b2897 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/TextToStringConverter.java @@ -0,0 +1,51 @@ +package mc.core.network.proto_1_12_2.serializers; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Getter; +import mc.core.text.Text; + +public class TextToStringConverter implements Converter { + @Getter + private static TextToStringConverter instance = new TextToStringConverter(); + + private JsonObject serialize(Text text) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("text", text.getContent()); + + if (text.getColor() != null) { + jsonObject.addProperty("color", text.getColor().getName()); + } + + if (text.getStyle() != null) { + if (text.getStyle().getBold().isPresent()) { + jsonObject.addProperty("bold", text.getStyle().getBold().get()); + } + if (text.getStyle().getItalic().isPresent()) { + jsonObject.addProperty("italic", text.getStyle().getItalic().get()); + } + if (text.getStyle().getObfuscated().isPresent()) { + jsonObject.addProperty("obfuscated", text.getStyle().getObfuscated().get()); + } + if (text.getStyle().getStrikethrough().isPresent()) { + jsonObject.addProperty("strikethrough", text.getStyle().getStrikethrough().get()); + } + if (text.getStyle().getUnderline().isPresent()) { + jsonObject.addProperty("underlined", text.getStyle().getUnderline().get()); + } + } + + if (text.getChildren() != null) { + JsonArray extra = new JsonArray(); + text.getChildren().forEach(child -> extra.add(serialize(child))); + jsonObject.add("extra", extra); + } + + return jsonObject; + } + + @Override + public String mapping(Text fromObject) { + return serialize(fromObject).toString(); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStream.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStream.java new file mode 100644 index 0000000..4b0c90a --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStream.java @@ -0,0 +1,84 @@ +package mc.core.network.proto_1_12_2; + +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +@Slf4j +public class ByteArrayInputNetStream extends NetInputStream_p340 { + private ByteArrayInputStream bais; + + public ByteArrayInputNetStream(byte[] buff) { + bais = new ByteArrayInputStream(buff); + } + + @Override + public boolean readBoolean() { + return readByte() != 0; + } + + @Override + public byte readByte() { + return (byte) bais.read(); + } + + @Override + public int readBytes(byte[] buffer, int offset, int length) { + try { + int read = bais.read(buffer, offset, length); + if (read < length) { + throw new IOException("not enough data"); + } + return read; + } catch (IOException e) { + log.error("", e); + return -1; + } + } + + @Override + public int readUnsignedByte() { + return bais.read() & 0xFF; + } + + @Override + public int readUnsignedShort() { + throw new UnsupportedOperationException(); + } + + @Override + public short readShort() { + throw new UnsupportedOperationException(); + } + + @Override + public int readInt() { + int ch1 = bais.read(); + int ch2 = bais.read(); + int ch3 = bais.read(); + int ch4 = bais.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) return 0; + return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4)); + } + + @Override + public long readLong() { + throw new UnsupportedOperationException(); + } + + @Override + public float readFloat() { + return Float.intBitsToFloat(readInt()); + } + + @Override + public double readDouble() { + throw new UnsupportedOperationException(); + } + + @Override + public void skipBytes(int count) { + throw new UnsupportedOperationException(); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStreamTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStreamTest.java new file mode 100644 index 0000000..7c3c32a --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStreamTest.java @@ -0,0 +1,135 @@ +package mc.core.network.proto_1_12_2; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ByteArrayInputNetStreamTest { + private Random random; + + @BeforeEach + void before() { + random = new Random(System.currentTimeMillis()); + } + + @Test + void testReadBoolean() { + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(true); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertTrue(byteArrayInputNetStream.readBoolean()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(false); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertFalse(byteArrayInputNetStream.readBoolean()); + } + + @Test + void testReadByte() throws IOException { + final byte[] bytes = new byte[1]; + random.nextBytes(bytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeByte(bytes[0]); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(bytes[0], byteArrayInputNetStream.readByte()); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(bytes[0], byteArrayInputNetStream.read()); + } + + @Test + void testReadBytes() throws IOException { + final byte[] expectedBytes = new byte[10]; + random.nextBytes(expectedBytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBytes(expectedBytes); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + byte[] actualBytes = new byte[10]; + byteArrayInputNetStream.readBytes(actualBytes); + + assertArrayEquals(expectedBytes, actualBytes); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + actualBytes = new byte[10]; + int r = byteArrayInputNetStream.read(actualBytes); + + assertArrayEquals(expectedBytes, actualBytes); + assertEquals(expectedBytes.length, r); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + actualBytes = new byte[10]; + byteArrayInputNetStream.readBytes(actualBytes, 2, 5); + byte[] nibbleExpectedBytes = new byte[10]; + System.arraycopy(expectedBytes, 0, nibbleExpectedBytes, 2, 5); + + assertArrayEquals(nibbleExpectedBytes, actualBytes); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + actualBytes = new byte[10]; + r = byteArrayInputNetStream.read(actualBytes, 2, 5); + nibbleExpectedBytes = new byte[10]; + System.arraycopy(expectedBytes, 0, nibbleExpectedBytes, 2, 5); + + assertArrayEquals(nibbleExpectedBytes, actualBytes); + assertEquals(5, r); + } + + @Test + void testReadUnsignedByte() { + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(30); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(30, byteArrayInputNetStream.readUnsignedByte()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(130); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(130, byteArrayInputNetStream.readUnsignedByte()); + } + + @Test + void testReadInt() { + final int integerDig = random.nextInt(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeInt(integerDig); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(integerDig, byteArrayInputNetStream.readInt()); + } + + @Test + void readFloat() { + final float floatDig = random.nextFloat(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeFloat(floatDig); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(floatDig, byteArrayInputNetStream.readFloat()); + } +} \ No newline at end of file diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStreamTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStreamTest.java new file mode 100644 index 0000000..3d34bb1 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStreamTest.java @@ -0,0 +1,259 @@ +package mc.core.network.proto_1_12_2; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class ByteArrayOutputNetStreamTest { + private Random random; + + @BeforeEach + void before() { + random = new Random(System.currentTimeMillis()); + } + + @Test + void testWriteBoolean() { + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(true); + + assertArrayEquals(new byte[]{0x01}, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(false); + + assertArrayEquals(new byte[]{0x00}, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteByte() throws IOException { + final byte[] bytes = new byte[1]; + random.nextBytes(bytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeByte(bytes[0]); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.write(bytes[0]); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteUnsignedByte() { + final byte[] bytes = new byte[1]; + random.nextBytes(bytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(bytes[0]); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(0xFF); + + assertArrayEquals(new byte[]{(byte) 0xFF}, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteBytes() throws IOException { + final byte[] expectedBytes = new byte[10]; + random.nextBytes(expectedBytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBytes(expectedBytes); + + assertArrayEquals(expectedBytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.write(expectedBytes); + + assertArrayEquals(expectedBytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBytes(expectedBytes, 2, 5); + byte[] nibbleExpectedBytes = new byte[5]; + System.arraycopy(expectedBytes, 2, nibbleExpectedBytes, 0, 5); + + assertArrayEquals(nibbleExpectedBytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.write(expectedBytes, 2, 5); + nibbleExpectedBytes = new byte[5]; + System.arraycopy(expectedBytes, 2, nibbleExpectedBytes, 0, 5); + + assertArrayEquals(nibbleExpectedBytes, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteShort() { + int smallInt; + do { + smallInt = random.nextInt(); + } while (smallInt > Short.MAX_VALUE || smallInt < Short.MIN_VALUE); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeShort(smallInt); + + assertArrayEquals(new byte[]{ (byte) (smallInt >>> 8), + (byte) smallInt }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteInt() { + final int integerDig = random.nextInt(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeInt(integerDig); + + assertArrayEquals(new byte[]{ (byte) ((integerDig >>> 24) & 0xFF), + (byte) ((integerDig >>> 16) & 0xFF), + (byte) ((integerDig >>> 8) & 0xFF), + (byte) (integerDig & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteLong() { + final long longDig = random.nextLong(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeLong(longDig); + + assertArrayEquals(new byte[]{ (byte) ((longDig >>> 56) & 0xFF), + (byte) ((longDig >>> 48) & 0xFF), + (byte) ((longDig >>> 40) & 0xFF), + (byte) ((longDig >>> 32) & 0xFF), + (byte) ((longDig >>> 24) & 0xFF), + (byte) ((longDig >>> 16) & 0xFF), + (byte) ((longDig >>> 8) & 0xFF), + (byte) (longDig & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteFloat() { + final float floatDig = random.nextFloat(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeFloat(floatDig); + final int floatBits = Float.floatToIntBits(floatDig); + + assertArrayEquals(new byte[]{ (byte) ((floatBits >>> 24) & 0xFF), + (byte) ((floatBits >>> 16) & 0xFF), + (byte) ((floatBits >>> 8) & 0xFF), + (byte) (floatBits & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteDouble() { + final double doubleDig = random.nextDouble(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeDouble(doubleDig); + final long doubleBits = Double.doubleToLongBits(doubleDig); + + assertArrayEquals(new byte[]{ (byte) ((doubleBits >>> 56) & 0xFF), + (byte) ((doubleBits >>> 48) & 0xFF), + (byte) ((doubleBits >>> 40) & 0xFF), + (byte) ((doubleBits >>> 32) & 0xFF), + (byte) ((doubleBits >>> 24) & 0xFF), + (byte) ((doubleBits >>> 16) & 0xFF), + (byte) ((doubleBits >>> 8) & 0xFF), + (byte) (doubleBits & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteVarInt() { + final int b1Int = 120; + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b1Int); + + assertArrayEquals(new byte[]{ 0x78 }, + byteArrayOutputNetStream.toByteArray()); + + final int b2Int = 12000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b2Int); + + assertArrayEquals(new byte[]{ (byte) 0xE0, 0x5D }, + byteArrayOutputNetStream.toByteArray()); + + final int b3Int = 120000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b3Int); + + assertArrayEquals(new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }, + byteArrayOutputNetStream.toByteArray()); + + final int b4Int = 120000000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b4Int); + + assertArrayEquals(new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }, + byteArrayOutputNetStream.toByteArray()); + + final int b5Int = 1200000000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b5Int); + + assertArrayEquals(new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteString() { + final String string = "Hello? Есть тут кто?"; + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeString(string); + + final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8); + final byte[] bytes = new byte[strBytes.length + 1]; + bytes[0] = (byte) string.length(); // здесь считается, что размер поместится в один байт + System.arraycopy(strBytes, 0, bytes, 1, strBytes.length); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteUUID() { + final UUID uuid = UUID.randomUUID(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUUID(uuid); + + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + + assertArrayEquals(new byte[]{ (byte) ((mostSignificantBits >>> 56) & 0xFF), + (byte) ((mostSignificantBits >>> 48) & 0xFF), + (byte) ((mostSignificantBits >>> 40) & 0xFF), + (byte) ((mostSignificantBits >>> 32) & 0xFF), + (byte) ((mostSignificantBits >>> 24) & 0xFF), + (byte) ((mostSignificantBits >>> 16) & 0xFF), + (byte) ((mostSignificantBits >>> 8) & 0xFF), + (byte) (mostSignificantBits & 0xFF), + + (byte) ((leastSignificantBits >>> 56) & 0xFF), + (byte) ((leastSignificantBits >>> 48) & 0xFF), + (byte) ((leastSignificantBits >>> 40) & 0xFF), + (byte) ((leastSignificantBits >>> 32) & 0xFF), + (byte) ((leastSignificantBits >>> 24) & 0xFF), + (byte) ((leastSignificantBits >>> 16) & 0xFF), + (byte) ((leastSignificantBits >>> 8) & 0xFF), + (byte) (leastSignificantBits & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacketTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacketTest.java new file mode 100644 index 0000000..0089bc7 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacketTest.java @@ -0,0 +1,313 @@ +package mc.core.network.proto_1_12_2.packets; + +import com.flowpowered.nbt.*; +import javafx.util.Pair; +import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream; +import mc.core.network.proto_1_12_2.packets.DumbChunkData.DumbChunkSection; +import mc.core.network.proto_1_12_2.packets.serverside.ChunkDataPacket; +import mc.core.world.Biome; +import mc.core.world.block.*; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.*; + +class ChunkDataPacketTest { + private static List> listOfParams; + + private static DumbChunkData createExpectedData(String xz) throws IOException { + InputStream inputStream = ChunkDataPacketTest.class.getResourceAsStream(String.format("ChunkDataPacket%s.bin", xz)); + assertNotNull(inputStream); + return DumbChunkData.ReadFromNetInputStream(IOUtils.toByteArray(inputStream)); + } + + private static Block createChestBlock00(BlockType type, int x, int y, int z, int height) { + final BlockLocation location = new BlockLocation(x, y, z); + + final CompoundMap compoundMap = new CompoundMap(); + compoundMap.put(new IntTag("x", x)); + compoundMap.put(new IntTag("y", (height << 4) + y)); + compoundMap.put(new IntTag("z", z)); + compoundMap.put(new StringTag("id", type.getNamedId())); + final CompoundTag compoundTag = new CompoundTag("", compoundMap); + + return new AbstractBlock(type) { + @Override + public BlockLocation getLocation() { + return location; + } + + @Override + public CompoundTag getNBTData() { + return compoundTag; + } + }; + } + + private static ChunkSection createChunkSection00(int height) { + final ChunkSection chunkSection = mock(ChunkSection.class); + when(chunkSection.getSkyLight(anyInt(), anyInt(), anyInt())).thenReturn(0); + when(chunkSection.getY()).thenReturn(height); + + if (height == 0) { + when(chunkSection.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + final int x = (int) args[0]; + final int y = (int) args[1]; + final int z = (int) args[2]; + + BlockFactory blockFactory = new BlockFactory(); + + if (y == 0) { + // @formatter:off + if (x == 0 && z == 0) return blockFactory.create(BlockType.STONE, x, y, z); + else if (x == 15 && z == 0) return blockFactory.create(BlockType.GRANITE, x, y, z); + else if (x == 0 && z == 15) return blockFactory.create(BlockType.POLISHED_GRANITE, x, y, z); + else if (x == 15 && z == 15) return blockFactory.create(BlockType.DIORITE, x, y, z); + else return blockFactory.create(BlockType.BEDROCK, x, y, z); + // @formatter:on + } else { + return blockFactory.create(BlockType.STONE, x, y, z); + } + }); + } else { + when(chunkSection.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + final int x = (int) args[0]; + final int y = (int) args[1]; + final int z = (int) args[2]; + + BlockFactory blockFactory = new BlockFactory(); + + // @formatter:off + if (y == 0) return blockFactory.create(BlockType.DIRT, x, y, z); + else if (y == 1) return blockFactory.create(BlockType.GRASS, x, y, z); + else if (y == 2) { + if ((x == 2 || x == 4 || x == 5) && z == 1) + return createChestBlock00(BlockType.CHEST_NORTH, x, y, z, height); + else if ((x == 2 || x == 3 || x == 5) && z == 6) + return createChestBlock00(BlockType.CHEST_SOUTH, x, y, z, height); + else if (x == 1 && (z == 2 || z == 3 || z == 5)) + return createChestBlock00(BlockType.CHEST_WEST, x, y, z, height); + else if (x == 6 && (z == 2 || z == 4 || z == 5)) + return createChestBlock00(BlockType.CHEST_EAST, x, y, z, height); + else + return blockFactory.create(BlockType.AIR, x, y, z); + } + else return blockFactory.create(BlockType.AIR, x, y, z); + // @formatter:on + }); + } + + return chunkSection; + } + + private static Chunk createMockChunk00() { + final ChunkSection chunkSection0 = createChunkSection00(0); + final ChunkSection chunkSection1 = createChunkSection00(1); + + final Chunk chunk = mock(Chunk.class); + when(chunk.getX()).thenReturn(0); + when(chunk.getZ()).thenReturn(0); + when(chunk.getBiome(anyInt(), anyInt())).thenReturn(Biome.PLAINS); + when(chunk.getChunkSection(0)).thenReturn(chunkSection0); + when(chunk.getChunkSection(1)).thenReturn(chunkSection1); + + return chunk; + } + + private static ChunkSection createChunkSection01() { + final ChunkSection chunkSection = mock(ChunkSection.class); + when(chunkSection.getSkyLight(anyInt(), anyInt(), anyInt())).thenReturn(0); + when(chunkSection.getY()).thenReturn(0); + + final List types = Arrays.asList( + BlockType.CLAY, + BlockType.ORE_REDSTONE, + BlockType.ORE_DIAMOND, + BlockType.OBSIDIAN, + BlockType.STONE_MOSS, + BlockType.SANDSTONE, + BlockType.ORE_LAPIS, + BlockType.WOOD_JUNGLE, + BlockType.WOOD_BIRCH, + BlockType.WOOD_SPRUCE, + BlockType.WOOD_OAK, + BlockType.ORE_COAL, + BlockType.ORE_IRON, + BlockType.ORE_GOLD, + BlockType.GRAVEL, + BlockType.SAND + ); + + when(chunkSection.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + final int x = (int) args[0]; + final int y = (int) args[1]; + final int z = (int) args[2]; + + BlockFactory blockFactory = new BlockFactory(); + + if (y == 0) { + // @formatter:off + if (x == 0 && z == 0) return blockFactory.create(BlockType.STONE, x, y, z); + else if (x == 15 && z == 0) return blockFactory.create(BlockType.GRANITE, x, y, z); + else if (x == 0 && z == 15) return blockFactory.create(BlockType.POLISHED_GRANITE, x, y, z); + else if (x == 15 && z == 15) return blockFactory.create(BlockType.DIORITE, x, y, z); + else return blockFactory.create(BlockType.BEDROCK, x, y, z); + // @formatter:on + } else if (y == 1) { + return blockFactory.create(types.get(x), x, y, z); + } else { + return blockFactory.create(BlockType.AIR, x, y, z); + } + }); + + return chunkSection; + } + + private static Chunk createMockChunk01() { + final ChunkSection chunkSection0 = createChunkSection01(); + + final Chunk chunk = mock(Chunk.class); + when(chunk.getX()).thenReturn(0); + when(chunk.getZ()).thenReturn(1); + when(chunk.getBiome(anyInt(), anyInt())).thenReturn(Biome.PLAINS); + when(chunk.getChunkSection(0)).thenReturn(chunkSection0); + + return chunk; + } + + private static void verifyMock(Chunk chunk) { + verify(chunk, atLeast(1)).getX(); + verify(chunk, atLeast(1)).getZ(); + verify(chunk, times(256)).getBiome(anyInt(), anyInt()); + verify(chunk, atLeast(2)).getChunkSection(anyInt()); + } + + private static DumbChunkData createActualData(Chunk chunk) { + ChunkDataPacket packet = new ChunkDataPacket(); + packet.setX(chunk.getX()); + packet.setZ(chunk.getZ()); + packet.setChunk(chunk); + packet.setInitChunk(true); + + ByteArrayOutputNetStream netStream = new ByteArrayOutputNetStream(); + packet.writeSelf(netStream); + + verifyMock(chunk); + + return DumbChunkData.ReadFromNetInputStream(netStream.toByteArray()); + } + + @BeforeAll + static void beforeClassTest() throws IOException { + listOfParams = Arrays.asList( + new Pair<>(createExpectedData("00"), createActualData(createMockChunk00())), + new Pair<>(createExpectedData("01"), createActualData(createMockChunk01())) + ); + } + + private static Stream streamArguments() { + return listOfParams.stream().map(pair -> Arguments.of(pair.getKey(), pair.getValue())); + } + + @DisplayName("testGeneral") + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("streamArguments") + void testGeneral(DumbChunkData expected, DumbChunkData actual) { + assertEquals(expected.getX(), actual.getX(), "X coord not equals"); + assertEquals(expected.getZ(), actual.getZ(), "Z coord not equals"); + assertEquals(expected.isInitChunk(), actual.isInitChunk(), "Flag init chunk not equals"); + assertEquals(expected.getBitMask(), actual.getBitMask(), "BitMask not equals"); + assertArrayEquals(expected.getBiomes(), actual.getBiomes(), "Biomes not equals"); + } + + @DisplayName("testNBT") + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("streamArguments") + void testNBT(DumbChunkData expected, DumbChunkData actual) { + assertEquals(expected.getNumberNBT(), actual.getNumberNBT()); + assertEquals(expected.getNbt().size(), actual.getNbt().size()); + + for (Tag tag : actual.getNbt()) { + assertTrue(expected.getNbt().contains(tag)); + } + } + + @DisplayName("testData (disabled light test)") + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("streamArguments") + void testData(DumbChunkData expected, DumbChunkData actual) { + assertEquals(expected.getData().length, actual.getData().length); + + for (int numberSection = 0; numberSection < expected.getData().length; numberSection++) { + final DumbChunkSection expectedSection = expected.getData()[numberSection]; + final DumbChunkSection actualSection = actual.getData()[numberSection]; + + // Palette + testPalette(expectedSection, actualSection, numberSection); + + // Data + testDataBlock(expectedSection, actualSection, numberSection); + + // Block and Sky light + // DISABLE // + //testLight(expectedSection, actualSection, numberSection); + } + } + + private void testPalette(DumbChunkSection expected, DumbChunkSection actual, int numberSection) { + assertEquals(expected.getBitsPerBlock(), actual.getBitsPerBlock()); + + if (expected.getPalette().size() > actual.getPalette().size()) { + for (int j = 0; j < actual.getPalette().size(); j++) { + assertTrue(expected.getPalette().contains( + actual.getPalette().get(j) + ), String.format("[%d] Palette not contains %s", numberSection, actual.getPalette().get(j))); + } + } else { + for (int j = 0; j < expected.getPalette().size(); j++) { + assertTrue(actual.getPalette().contains( + expected.getPalette().get(j) + ), String.format("[%d] Palette not contains %s", numberSection, actual.getPalette().get(j))); + } + } + } + + private void testDataBlock(DumbChunkSection expected, DumbChunkSection actual, int numberSection) { + assertEquals(expected.getData().size(), actual.getData().size()); + + for (int j = 0; j < expected.getData().size(); j++) { + assertEquals( + expected.getData().get(j), + actual.getData().get(j), + String.format("[%d] Data (blocks)", numberSection) + ); + } + } + + private void testLight(DumbChunkSection expected, DumbChunkSection actual, int numberSection) { + // Block light + assertArrayEquals(expected.getBlockLight(), actual.getBlockLight(), + String.format("[%d] Block light", numberSection)); + + // Sky light + assertArrayEquals(expected.getSkyLight(), actual.getSkyLight(), + String.format("[%d] Sky light", numberSection)); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/DumbChunkData.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/DumbChunkData.java new file mode 100644 index 0000000..bd472c8 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/DumbChunkData.java @@ -0,0 +1,133 @@ +package mc.core.network.proto_1_12_2.packets; + +import com.flowpowered.nbt.Tag; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mc.core.network.proto_1_12_2.ByteArrayInputNetStream; +import mc.core.world.block.BlockType; + +import java.nio.ByteBuffer; +import java.nio.LongBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +class DumbChunkData { + private int x; + private int z; + private boolean initChunk; + private int bitMask; + + private int sizeOfData; + private DumbChunkSection[] data; + private byte[] biomes; + + private int numberNBT; + private List> nbt; + + private static BlockType deserializeBlockState(int blockState) { + return BlockType.getByIdMeta(blockState >> 4, blockState & 0x0F); + } + + static DumbChunkData ReadFromNetInputStream(byte[] bytes) { + ByteArrayInputNetStream netStream = new ByteArrayInputNetStream(bytes); + + DumbChunkData dumbChunkData = new DumbChunkData(); + + dumbChunkData.x = netStream.readInt(); + dumbChunkData.z = netStream.readInt(); + dumbChunkData.initChunk = netStream.readBoolean(); + + dumbChunkData.bitMask = netStream.readVarInt(); + int countOfSections = 0; + for (int shift = 0; shift < 8; shift++) { + countOfSections += ((dumbChunkData.bitMask >> shift) & 0x01) > 0 ? 1 : 0; + } + + dumbChunkData.sizeOfData = netStream.readVarInt(); + + dumbChunkData.data = new DumbChunkSection[countOfSections]; + for (int c = 0; c < countOfSections; c++) { + DumbChunkSection dumbChunkSection = new DumbChunkSection(); + + dumbChunkSection.bitsPerBlock = netStream.readUnsignedByte(); + int sizePalette = netStream.readVarInt(); + dumbChunkSection.palette = new ArrayList<>(sizePalette); + for (int i = 0; i < sizePalette; i++) { + dumbChunkSection.palette.add(deserializeBlockState(netStream.readVarInt())); + } + + final byte[] rawData = new byte[netStream.readVarInt() * 8]; + netStream.readBytes(rawData); + LongBuffer data = ByteBuffer.wrap(rawData).asLongBuffer(); + + final int bitMask = (1 << dumbChunkSection.bitsPerBlock) - 1; + dumbChunkSection.data = new ArrayList<>(4096); + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + final int blockNumber = (((y << 4) + z) << 4) + x; + final int startLong = ( blockNumber * dumbChunkSection.bitsPerBlock ) / 64; + final int startOffset = ( blockNumber * dumbChunkSection.bitsPerBlock ) % 64; + final int endLong = ((blockNumber + 1) * dumbChunkSection.bitsPerBlock - 1) / 64; + + int idxBlock; + if (startLong == endLong) { + idxBlock = (int)(data.get(startLong) >> startOffset); + } else { + int endOffset = 64 - startOffset; + long mask = (1 << endOffset) - 1; + idxBlock = (int)(((data.get(startLong) >> startOffset) & mask) | data.get(endLong) << endOffset); + } + + dumbChunkSection.data.add(dumbChunkSection.palette.get(idxBlock & bitMask)); + } + } + } + + dumbChunkSection.blockLight = new byte[2048]; + netStream.readBytes(dumbChunkSection.blockLight); + dumbChunkSection.skyLight = new byte[2048]; + netStream.readBytes(dumbChunkSection.skyLight); + + dumbChunkData.data[c] = dumbChunkSection; + } + + dumbChunkData.biomes = new byte[256]; + netStream.readBytes(dumbChunkData.biomes); + + dumbChunkData.numberNBT = netStream.readVarInt(); + if (dumbChunkData.numberNBT > 0) { + dumbChunkData.nbt = new ArrayList<>(dumbChunkData.numberNBT); + for (int i = 0; i < dumbChunkData.numberNBT; i++) { + dumbChunkData.nbt.add(netStream.readNBT()); + } + } else { + dumbChunkData.nbt = Collections.emptyList(); + } + + return dumbChunkData; + } + + @Override + public String toString() { + return "DumbChunkData{" + + "x=" + x + + ", z=" + z + + '}'; + } + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @Getter + static class DumbChunkSection { + private int bitsPerBlock; + private List palette; + + private List data; + private byte[] blockLight; + private byte[] skyLight; + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacketTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacketTest.java new file mode 100644 index 0000000..62d70c7 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacketTest.java @@ -0,0 +1,41 @@ +package mc.core.network.proto_1_12_2.packets; + +import mc.core.network.proto_1_12_2.ByteArrayInputNetStream; +import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PlayerAbilitiesPacketTest { + private Random rnd = new Random(); + private PlayerAbilitiesPacket packet; + + @BeforeEach + void before() { + packet = new PlayerAbilitiesPacket(); + packet.setGodMode(rnd.nextBoolean()); + packet.setFlying(rnd.nextBoolean()); + packet.setCanFly(rnd.nextBoolean()); + packet.setInstantDestroyBlocks(rnd.nextBoolean()); + packet.setFlyingSpeed(rnd.nextFloat()); + } + + @Test + void writePacket() { + ByteArrayOutputNetStream netOutputStream = new ByteArrayOutputNetStream(); + packet.writeSelf(netOutputStream); + + ByteArrayInputNetStream netInputStream = new ByteArrayInputNetStream(netOutputStream.toByteArray()); + PlayerAbilitiesPacket outPkt = new PlayerAbilitiesPacket(); + outPkt.readSelf(netInputStream); + + assertEquals(packet.isGodMode(), outPkt.isGodMode(), "god mode"); + assertEquals(packet.isFlying(), outPkt.isFlying(), "flying"); + assertEquals(packet.isCanFly(), outPkt.isCanFly(), "can fly"); + assertEquals(packet.isInstantDestroyBlocks(), outPkt.isInstantDestroyBlocks(), "instant destroy block"); + assertEquals(packet.getFlyingSpeed(), outPkt.getFlyingSpeed(), 0.00001f, "flying speed"); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializerTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializerTest.java new file mode 100644 index 0000000..dda7500 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializerTest.java @@ -0,0 +1,32 @@ +package mc.core.network.proto_1_12_2.serializers; + +import mc.core.world.block.BlockLocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BlockLocationSerializerTest { + private static final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + private static final int minI = 0, maxI = 10; + private int x, y, z; + + @BeforeEach + void before() { + x = rnd.nextInt(minI, maxI); + y = rnd.nextInt(minI, maxI); + z = rnd.nextInt(minI, maxI); + } + + @Test + void serialize() { + BlockLocation location = new BlockLocation(x, y, z); + final long serializedCoords = BlockLocationSerializer.toLong(location); + + BlockLocation deserLoc = BlockLocationSerializer.fromLong(serializedCoords); + + assertEquals(location, deserLoc); + } +} \ No newline at end of file diff --git a/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket00.bin b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket00.bin new file mode 100644 index 0000000..6125544 Binary files /dev/null and b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket00.bin differ diff --git a/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket01.bin b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket01.bin new file mode 100644 index 0000000..6c12d37 Binary files /dev/null and b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket01.bin differ diff --git a/proto_1.12.2_netty/README.MD b/proto_1.12.2_netty/README.MD new file mode 100644 index 0000000..92e9638 --- /dev/null +++ b/proto_1.12.2_netty/README.MD @@ -0,0 +1,7 @@ +# Protocol 1.12.2: Netty impl. + +Реализация протокола на сетевом движке [Netty.IO](https://netty.io/). + +Пример настройки можно посмотреть в файле `sample-config.xml` + + diff --git a/proto_1.12.2_netty/build.gradle b/proto_1.12.2_netty/build.gradle new file mode 100644 index 0000000..cfefd73 --- /dev/null +++ b/proto_1.12.2_netty/build.gradle @@ -0,0 +1,13 @@ +version '0.1' + +ext { + netty_version = '4.1.22.Final' +} + +dependencies { + /* Protocol 1.12.2 */ + compile_excludeCopy project(':proto_1.12.2') + + /* Netty */ + compile (group: 'io.netty', name: 'netty-all', version: netty_version) +} diff --git a/proto_1.12.2_netty/sample-config.xml b/proto_1.12.2_netty/sample-config.xml new file mode 100644 index 0000000..0ff4b97 --- /dev/null +++ b/proto_1.12.2_netty/sample-config.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/KeepAliveThread.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/KeepAliveThread.java new file mode 100644 index 0000000..9e02708 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/KeepAliveThread.java @@ -0,0 +1,52 @@ +package mc.core.network.proto_1_12_2.netty; + +import lombok.Setter; +import mc.core.network.proto_1_12_2.packets.KeepAlivePacket; +import mc.core.player.PlayerManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class KeepAliveThread extends Thread { + private final Object lock = new Object(); + @Autowired + private PlayerManager playerManager; + @Setter + private int interval = 10; + + public KeepAliveThread() { + super("Keep-Alive Thread"); + } + + public void notifyLock() { + synchronized (lock) { + lock.notify(); + } + } + + @Override + public void run() { + final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(); + + while (!Thread.currentThread().isInterrupted()) { + while(playerManager.getCountPlayers() == 0) { + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { + return; + } + } + } + + keepAlivePacket.setPayload(System.currentTimeMillis()); + playerManager.getBroadcastChannel().writeAndFlush(keepAlivePacket); + + try { + Thread.sleep(interval); + } catch (InterruptedException e) { + return; + } + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/NettyServer.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/NettyServer.java new file mode 100644 index 0000000..44590c4 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/NettyServer.java @@ -0,0 +1,92 @@ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.util.AttributeKey; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.EventBus; +import mc.core.network.Server; +import mc.core.network.StartServerException; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.packets.serverside.StatusResponsePacket; +import mc.core.player.Player; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +public class NettyServer implements Server { + public static final AttributeKey ATTR_STATE = AttributeKey.newInstance("ATTR_STATE"); + public static final AttributeKey ATTR_PLAYER = AttributeKey.newInstance("ATTR_PLAYER"); + + @Autowired + private ApplicationContext context; + @Autowired + private KeepAliveThread keepAliveThread; + @Setter + private String host; + @Setter + private int port; + @Setter + private int workerGroupCount = 0; + private EventLoopGroup bossGroup, workerGroup; + + private ChannelInitializer buildChannelInitializer() { + return new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) { + Map beans = context.getBeansOfType(ChannelHandler.class); + beans.forEach(socketChannel.pipeline()::addLast); + } + }; + } + + private ServerBootstrap buildServerBootstrap() { + ServerBootstrap bootstrap = new ServerBootstrap(); + + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(buildChannelInitializer()); + + return bootstrap; + } + + @Override + public void start() throws StartServerException { + log.info("Use protocol {}", StatusResponsePacket.NAME); + + EventBus.getInstance().registerSubscribes(new PlayerEventListener()); + + bossGroup = new NioEventLoopGroup(1); + workerGroup = new NioEventLoopGroup(workerGroupCount); + + ServerBootstrap serverBootstrap = buildServerBootstrap(); + + log.info("Start server: {}:{}", host, port); + try { + ChannelFuture channelFuture = serverBootstrap.bind(host, port).sync(); + keepAliveThread.start(); + channelFuture.channel().closeFuture().sync(); + } catch (InterruptedException e) { + throw new StartServerException(e); + } + } + + @Override + public void stop() { + log.info("Server shutdown"); + keepAliveThread.interrupt(); + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketDecoder.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketDecoder.java new file mode 100644 index 0000000..2e0ee69 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketDecoder.java @@ -0,0 +1,66 @@ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ReplayingDecoder; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetInputStream; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +public class PacketDecoder extends ReplayingDecoder { + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.channel().attr(ATTR_STATE).set(State.HANDSHAKE); + ctx.fireChannelActive(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.channel().attr(ATTR_STATE).set(null); + ctx.fireChannelInactive(); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + State state = ctx.channel().attr(ATTR_STATE).get(); + NetInputStream netStream = new WrapperNetInputStream(in); + + int packetSize = netStream.readVarInt(); + log.debug("Packet size: {}", packetSize); + int leftDataPacket = packetSize; + + final AtomicInteger countReadBytes = new AtomicInteger(0); + int packetId = netStream.readVarInt(countReadBytes); + String hexPacketId = Integer.toHexString(packetId).toUpperCase(); + if (hexPacketId.length() == 1) hexPacketId = "0" + hexPacketId; + log.debug("Packet id: 0x{}", hexPacketId); + leftDataPacket = leftDataPacket - countReadBytes.get(); + + Class packetClass = state.getClientSidePacket(packetId); + if (packetClass == null) { + log.warn("Unknown packet: {}:0x{}", state.name(), hexPacketId); //TODO + in.skipBytes(leftDataPacket); + } else { + netStream.setDataSize(leftDataPacket); + CSPacket packet = packetClass.newInstance(); + try { + packet.readSelf(netStream); + log.debug("Known packet: {}:{}", state.name(), packet.toString()); + out.add(packet); + } catch (Exception e) { + log.warn("Known packet: {}:{}. But throw exception. See debug log.", state.name(), packet.getClass().getSimpleName()); + log.debug("Read packet", e); + in.skipBytes(leftDataPacket); + } + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketEncoder.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketEncoder.java new file mode 100644 index 0000000..5198f01 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketEncoder.java @@ -0,0 +1,37 @@ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetOutputStream; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; +import static org.slf4j.helpers.MessageFormatter.format; + +@Slf4j +public class PacketEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, SCPacket packet, ByteBuf out) { + State state = ctx.channel().attr(ATTR_STATE).get(); + Integer id = state.getServerSidePacket(packet.getClass()); + if (id == null) { + log.error("Not defined ID packet: {}:{}", state.name(), packet.getClass()); + return; + } + + log.debug("Send {}:{}", state, packet); + + try { + NetOutputStream netStream = new WrapperNetOutputStream(out); + netStream.writeVarInt(id); + packet.writeSelf(netStream); + } catch (Throwable t) { + log.error(format("Error encoding packet {}:{}", state, packet).getMessage(), t); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketHandler.java new file mode 100644 index 0000000..46a6549 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketHandler.java @@ -0,0 +1,72 @@ +package mc.core.network.proto_1_12_2.netty; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; +import mc.core.chat.ChatProcessor; +import mc.core.network.CSPacket; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.netty.handlers.*; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +import javax.annotation.PostConstruct; +import java.util.Collection; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_PLAYER; +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +@ComponentScan("mc.core.network.proto_1_12_2.netty.handlers") +public class PacketHandler extends SimpleChannelInboundHandler { + @Autowired + private ApplicationContext applicationContext; + @Autowired + private PlayerManager playerManager; + @Autowired + private ChatProcessor chatProcessor; + private ImmutableMap> handlersMap = ImmutableMap.of(); + + @PostConstruct + public void init() { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + Collection beans1 = applicationContext.getBeansOfType(HandshakeStateHandler.class).values(); + builder.put(State.HANDSHAKE, ImmutableList.copyOf(beans1)); + + Collection beans2 = applicationContext.getBeansOfType(StatusStateHandler.class).values(); + builder.put(State.STATUS, ImmutableList.copyOf(beans2)); + + Collection beans3 = applicationContext.getBeansOfType(LoginStateHandler.class).values(); + builder.put(State.LOGIN, ImmutableList.copyOf(beans3)); + + Collection beans4 = applicationContext.getBeansOfType(PlayStateHandler.class).values(); + builder.put(State.PLAY, ImmutableList.copyOf(beans4)); + + this.handlersMap = builder.build(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + Player player = ctx.channel().attr(ATTR_PLAYER).get(); + if (player != null) { + playerManager.leftServer(player); + player.setChannel(null); + } + ctx.channel().attr(ATTR_PLAYER).set(null); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, CSPacket packet) throws Exception { + ImmutableList stateHandlers = this.handlersMap.get(ctx.channel().attr(ATTR_STATE).get()); + for (StateHandler handler : stateHandlers) { + handler.handle(ctx.channel(), packet); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketPostEncoder.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketPostEncoder.java new file mode 100644 index 0000000..7e7b848 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketPostEncoder.java @@ -0,0 +1,38 @@ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetOutputStream; + +/** + * Подсчет размера отправляемого пакета + */ +@Slf4j +public class PacketPostEncoder extends MessageToByteEncoder { + + private static int getVarIntSize(int input) { + for (int i = 1; i < 5; ++i) { + if ((input & -1 << i * 7) == 0) { + return i; + } + } + + return 5; + } + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) { + final int pktSize = msg.readableBytes(); + final int sizeOfPktSize = getVarIntSize(pktSize); + + if (sizeOfPktSize > 3) { + log.warn("unable to fit {} into {}", pktSize, 3); + } + + out.ensureWritable(sizeOfPktSize + pktSize); + (new WrapperNetOutputStream(out)).writeVarInt(pktSize); + out.writeBytes(msg); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PlayerEventListener.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PlayerEventListener.java new file mode 100644 index 0000000..927ac82 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PlayerEventListener.java @@ -0,0 +1,70 @@ +package mc.core.network.proto_1_12_2.netty; + +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.Subscriber; +import mc.core.eventbus.events.SC_ChunkLoadEvent; +import mc.core.eventbus.events.SC_ChunkUnloadEvent; +import mc.core.eventbus.events.SC_PlayerMoveEvent; +import mc.core.network.NetChannel; +import mc.core.network.proto_1_12_2.TeleportManager; +import mc.core.network.proto_1_12_2.packets.serverside.ChunkDataPacket; +import mc.core.network.proto_1_12_2.packets.PlayerPositionAndLookPacket; +import mc.core.network.proto_1_12_2.packets.serverside.UnloadChunkPacket; +import mc.core.utils.CompactedCoords; +import mc.core.world.chunk.Chunk; + +@Slf4j +public class PlayerEventListener { + @Subscriber + public void playerMoveEventHandler(SC_PlayerMoveEvent event) { + log.debug("(SC) playerMoveEventHandler()"); + PlayerPositionAndLookPacket packet = new PlayerPositionAndLookPacket(); + packet.setLocation(event.getNewLocation()); + int tpId = TeleportManager.getInstance().append(event.getPlayer(), event.getNewLocation()); + packet.setTeleportId(tpId); + + event.getPlayer().getChannel().writeAndFlush(packet); + } + + @Subscriber + public void playerChunkLoadHandler(SC_ChunkLoadEvent event) { + if (event.getNeedLoadChunks().size() == 0) return; + + final NetChannel channel = event.getPlayer().getChannel(); + + for(Integer compressXZ : event.getNeedLoadChunks()) { + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + Chunk chunk = event.getPlayer().getWorld().getChunk(xz[0], xz[1]); + if (chunk == null) continue; + + ChunkDataPacket packet = new ChunkDataPacket(); + packet.setX(xz[0]); + packet.setZ(xz[1]); + packet.setInitChunk(true); + packet.setChunk(chunk); + + channel.write(packet); + } + + channel.flush(); + } + + @Subscriber + public void playerChunkUnloadHandler(SC_ChunkUnloadEvent event) { + if (event.getNeedUnloadChunks().size() == 0) return; + + final NetChannel channel = event.getPlayer().getChannel(); + + for(Integer compressXZ : event.getNeedUnloadChunks()) { + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + + UnloadChunkPacket packet = new UnloadChunkPacket(); + packet.setX(xz[0]); + packet.setZ(xz[1]); + + channel.write(packet); + } + + channel.flush(); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/AbstractStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/AbstractStateHandler.java new file mode 100644 index 0000000..637e440 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/AbstractStateHandler.java @@ -0,0 +1,50 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import com.google.common.collect.ImmutableMap; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.CSPacket; + +import javax.annotation.PostConstruct; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.function.Function; +import java.util.stream.Stream; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +public abstract class AbstractStateHandler implements StateHandler { + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + protected @interface Handler { + } + + private ImmutableMap, Method> methods = ImmutableMap.of(); + + @PostConstruct + protected void init() { + this.methods = Stream.of(this.getClass().getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(Handler.class) + && method.getParameterCount() == 2 + && method.getParameterTypes()[0].isAssignableFrom(Channel.class) + && CSPacket.class.isAssignableFrom(method.getParameterTypes()[1])) + .collect(ImmutableMap.toImmutableMap( + method -> method.getParameterTypes()[1], + Function.identity())); + } + + @Override + public void handle(Channel channel, CSPacket packet) throws Exception { + if (this.methods.containsKey(packet.getClass())) { + this.methods.get(packet.getClass()).invoke(this, channel, packet); + } else { + log.trace("No def listener of {}:{}", + channel.attr(ATTR_STATE).get(), + packet.getClass().getSimpleName()); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeHandler.java new file mode 100644 index 0000000..58de87b --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeHandler.java @@ -0,0 +1,21 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.packets.clientside.HandshakePacket; +import org.springframework.stereotype.Component; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +@Component +public class HandshakeHandler extends AbstractStateHandler implements HandshakeStateHandler { + @Handler + public void onHandshake(Channel channel, HandshakePacket packet) { + if (packet.getNextState().equals(State.UNKNOWN)) return; + //FIXME обрати внимание: хацкер может намеренно передать state:03 и тогда может начатся пиздец + log.debug("New state: {}", packet.getNextState()); + channel.attr(ATTR_STATE).set(packet.getNextState()); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeStateHandler.java new file mode 100644 index 0000000..d36b7d8 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeStateHandler.java @@ -0,0 +1,7 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface HandshakeStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java new file mode 100644 index 0000000..897e195 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java @@ -0,0 +1,129 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.events.CS_PlayerMoveEvent; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.TeleportManager; +import mc.core.network.proto_1_12_2.netty.KeepAliveThread; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetChannel; +import mc.core.network.proto_1_12_2.packets.*; +import mc.core.network.proto_1_12_2.packets.clientside.LoginStartPacket; +import mc.core.network.proto_1_12_2.packets.serverside.*; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.player.PlayerMode; +import mc.core.text.Text; +import mc.core.text.TextColor; +import mc.core.text.TextStyle; +import mc.core.world.World; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_PLAYER; +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Component +public class LoginHandler extends AbstractStateHandler implements LoginStateHandler { + @Autowired + private PlayerManager playerManager; + @Autowired + private KeepAliveThread keepAliveThread; + @Autowired + private World world; + + @Handler + public void onLoginStart(Channel channel, LoginStartPacket packet) { + Player player = playerManager.getPlayer(packet.getPlayerName()); + if (player != null) { + channel.writeAndFlush(new DisconnectPacket( + Text.builder("Player \"") + .append(Text.of(packet.getPlayerName(), TextColor.YELLOW)) + .append(Text.of("\" is online", TextColor.WHITE)) + .build())) + .addListener(ChannelFutureListener.CLOSE); + } else { + player = playerManager.getOfflinePlayer(packet.getPlayerName()); + + if (player == null) { + player = playerManager.createPlayer( + packet.getPlayerName(), + world.getSpawn(), + world + ); + + if (player == null) { + channel.writeAndFlush(new DisconnectPacket( + Text.of("Internal server error: can't create new player")) + ).addListener(ChannelFutureListener.CLOSE); + return; + } + } + + channel.writeAndFlush(new LoginSuccessPacket( + player.getUuid(), + packet.getPlayerName())); + channel.attr(ATTR_PLAYER).set(player); + channel.attr(ATTR_STATE).set(State.PLAY); + + // Join Game + JoinGamePacket pkt1 = new JoinGamePacket(); + pkt1.setEntityId(player.getId()); //TODO отделить системный ID от EntityID + pkt1.setMode(PlayerMode.CREATIVE); //TODO перенести в Config + pkt1.setDimension(0/*Overworld*/); //TODO перенести в World + pkt1.setDifficulty(0/*Peaceful*/); //TODO перенести в Config + pkt1.setLevelType(world.getType().getName()); + channel.write(pkt1); + + // Spawn Position + SpawnPositionPacket pkt2 = new SpawnPositionPacket(); + pkt2.setLocation(world.getSpawn()); + channel.write(pkt2); + + // Player Abilities + PlayerAbilitiesPacket pkt3 = new PlayerAbilitiesPacket(); //TODO перенести в Player + pkt3.setCanFly(true); + pkt3.setFlying(true); + pkt3.setGodMode(true); + pkt3.setInstantDestroyBlocks(true); + channel.write(pkt3); + + channel.flush(); + + // Player Position And Look + PlayerPositionAndLookPacket pkt4 = new PlayerPositionAndLookPacket(); + pkt4.setLocation(player.getLocation()); + pkt4.setTeleportId(TeleportManager.getInstance().append(player, player.getLocation())); + channel.writeAndFlush(pkt4); + + player.setChannel(new WrapperNetChannel(channel)); + + // Send items + //TODO обновление должно приходить всем игрокам на сервере + PlayerListItemPacket pkt5 = new PlayerListItemPacket(); + pkt5.setAction(PlayerListItemPacket.Action.ADD_PLAYER); + PlayerListItemPacket.PlayerData playerData = new PlayerListItemPacket.PlayerData(); + playerData.setUuid(player.getUuid()); + playerData.setName(player.getName()); + playerData.setGameMode(PlayerMode.CREATIVE); + playerData.setPing(0); + playerData.setHasDisplayName(true); + playerData.setDisplayName(Text.builder() + .append(Text.of(TextColor.RED, TextStyle.BOLD, player.getName().substring(0,1))) + .append(Text.of(TextColor.WHITE, player.getName().substring(1))) + .build() + ); + pkt5.getListPlayers().add(playerData); + channel.writeAndFlush(pkt5); + + playerManager.joinServer(player); + keepAliveThread.notifyLock(); + + CS_PlayerMoveEvent event = new CS_PlayerMoveEvent(player, player.getLocation()); + event.setNewLocation(player.getLocation()); + event.setRecalcChunk(true); + EventBus.getInstance().post(event); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginStateHandler.java new file mode 100644 index 0000000..60af6cc --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginStateHandler.java @@ -0,0 +1,7 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface LoginStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayHandler.java new file mode 100644 index 0000000..90c4ce5 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayHandler.java @@ -0,0 +1,91 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import mc.core.EntityLocation; +import mc.core.chat.ChatProcessor; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.events.CS_PlayerMoveEvent; +import mc.core.network.proto_1_12_2.TeleportManager; +import mc.core.network.proto_1_12_2.packets.*; +import mc.core.network.proto_1_12_2.packets.clientside.*; +import mc.core.player.Player; +import mc.core.player.PlayerSettings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_PLAYER; + +@Slf4j +@Component +public class PlayHandler extends AbstractStateHandler implements PlayStateHandler { + @Autowired + private ChatProcessor chatProcessor; + + private TeleportManager teleport = TeleportManager.getInstance(); + + @Handler + public void onClientSettings(Channel channel, ClientSettingsPacket packet) { + Player player = channel.attr(ATTR_PLAYER).get(); + + player.getSettings().setLocate(packet.getLocale()); + player.getSettings().setViewDistance(packet.getViewDistance()); + player.getSettings().setChatMode(PlayerSettings.ChatMode.getById(packet.getChatMode())); + player.getSettings().setChatColors(packet.isChatColors()); + + player.getSettings().setCapeEnabled(packet.isCapeEnabled()); + player.getSettings().setJacketEnabled(packet.isJacketEnabled()); + player.getSettings().setLeftSleeveEnabled(packet.isLeftSleeveEnabled()); + player.getSettings().setRightSleeveEnabled(packet.isRightSleeveEnabled()); + player.getSettings().setLeftPantsLegEnabled(packet.isLeftPantsLegEnabled()); + player.getSettings().setRightPantsLegEnabled(packet.isRightPantsLegEnabled()); + player.getSettings().setHatEnabled(packet.isHatEnabled()); + + player.getSettings().setMainHand(PlayerSettings.Hand.getById(packet.getMainHand())); + } + + @Handler + public void onTeleportConfirm(Channel channel, TeleportConfirmPacket packet) { + this.teleport.apply(packet.getTeleportId()); + } + + @Handler + public void onPositionAndLook(Channel channel, PlayerPositionAndLookPacket packet) { + Player player = channel.attr(ATTR_PLAYER).get(); + player.getLocation().set(packet.getLocation()); + } + + @Handler + public void onChat(Channel channel, ChatMessageClientPacket packet) { + chatProcessor.process( + channel.attr(ATTR_PLAYER).get(), + packet.getMessage() + ); + } + + @Handler + public void onPlayerMove(Channel channel, PlayerPositionPacket packet) { + log.debug("(Netty) onPlayerMove()"); + Player player = channel.attr(ATTR_PLAYER).get(); + + if (player.getLocation().getX() == packet.getX() && + player.getLocation().getY() == packet.getY() && + player.getLocation().getZ() == packet.getZ()) { + return; + } + + CS_PlayerMoveEvent event = new CS_PlayerMoveEvent(player, player.getLocation().clone()); + event.setNewLocation(new EntityLocation( + packet.getX(), packet.getY(), packet.getZ(), + player.getLocation().getYaw(), + player.getLocation().getPitch() + )); + EventBus.getInstance().post(event); + } + + @Handler + public void onPlayerLook(Channel channel, PlayerLookPacket packet) { + Player player = channel.attr(ATTR_PLAYER).get(); + player.getLocation().setYawPitch(packet.getYaw(), packet.getPitch()); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayStateHandler.java new file mode 100644 index 0000000..d4486fb --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayStateHandler.java @@ -0,0 +1,7 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface PlayStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StateHandler.java new file mode 100644 index 0000000..45c00e1 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StateHandler.java @@ -0,0 +1,8 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import mc.core.network.CSPacket; + +public interface StateHandler { + void handle(Channel channel, CSPacket packet) throws Exception; +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusHandler.java new file mode 100644 index 0000000..7afc730 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusHandler.java @@ -0,0 +1,34 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import mc.core.Config; +import mc.core.network.proto_1_12_2.packets.PingPacket; +import mc.core.network.proto_1_12_2.packets.clientside.StatusRequestPacket; +import mc.core.network.proto_1_12_2.packets.serverside.StatusResponsePacket; +import mc.core.player.PlayerManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class StatusHandler extends AbstractStateHandler implements StatusStateHandler { + @Autowired + private Config config; + @Autowired + private PlayerManager playerManager; + + @Handler + public void onStatusRequest(Channel channel, StatusRequestPacket packet) { + StatusResponsePacket responsePacket = new StatusResponsePacket(); + responsePacket.setMaxOnline(config.getMaxPlayers()); + responsePacket.setDescription(config.getDescriptionServer()); + responsePacket.setFaviconBase64(config.getFaviconBase64()); + responsePacket.setOnline(playerManager.getCountPlayers()); + + channel.writeAndFlush(responsePacket); + } + + @Handler + public void onPing(Channel channel, PingPacket packet) { + channel.writeAndFlush(packet); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusStateHandler.java new file mode 100644 index 0000000..b306f6d --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusStateHandler.java @@ -0,0 +1,7 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface StatusStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetChannel.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetChannel.java new file mode 100644 index 0000000..f1c5af4 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetChannel.java @@ -0,0 +1,69 @@ +package mc.core.network.proto_1_12_2.netty.wrappers; + +import io.netty.channel.Channel; +import lombok.RequiredArgsConstructor; +import mc.core.chat.MessageType; +import mc.core.network.NetChannel; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.packets.serverside.ChatMessageServerPacket; +import mc.core.network.proto_1_12_2.packets.serverside.TimeUpdatePacket; +import mc.core.network.proto_1_12_2.packets.serverside.TitlePacket; +import mc.core.text.Text; +import mc.core.text.Title; + +@RequiredArgsConstructor +public class WrapperNetChannel implements NetChannel { + private final Channel channel; + + @Override + public void sendTimeUpdate(long time, long age) { + writeAndFlush(new TimeUpdatePacket(time, age)); + } + + @Override + public void sendChatMessage(Text text, MessageType type) { + writeAndFlush(new ChatMessageServerPacket(text, type)); + } + + @Override + public void sendTitle(Title title) { + Text text = title.getTitle(); + if (text != null) write(new TitlePacket(TitlePacket.Action.SET_TITLE, text)); + + text = title.getSubtitle(); + if (text != null) write(new TitlePacket(TitlePacket.Action.SET_SUBTITLE, text)); + + text = title.getTextActionBar(); + if (text != null) write(new TitlePacket(TitlePacket.Action.SET_ACTION_BAR, text)); + + Integer fadeIn = title.getFadeInTime(); + Integer stay = title.getStayTime(); + Integer fadeOut = title.getFadeOutTime(); + if (fadeIn != null && stay != null && fadeOut != null) { + write(new TitlePacket(TitlePacket.Action.SET_DISPLAY_TIME, fadeIn, stay, fadeOut)); + } + + Boolean bool = title.getHide(); + if (bool != null && bool) write(new TitlePacket(TitlePacket.Action.HIDE)); + + bool = title.getReset(); + if (bool != null && bool) write(new TitlePacket(TitlePacket.Action.RESET)); + + flush(); + } + + @Override + public void writeAndFlush(SCPacket pkt) { + channel.writeAndFlush(pkt, channel.voidPromise()); + } + + @Override + public void write(SCPacket pkt) { + channel.write(pkt); + } + + @Override + public void flush() { + channel.flush(); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetInputStream.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetInputStream.java new file mode 100644 index 0000000..4658c5b --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetInputStream.java @@ -0,0 +1,68 @@ +package mc.core.network.proto_1_12_2.netty.wrappers; + +import io.netty.buffer.ByteBuf; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.NetInputStream_p340; + +@Slf4j +@RequiredArgsConstructor +public class WrapperNetInputStream extends NetInputStream_p340 { + private final ByteBuf byteBuf; + + @Override + public boolean readBoolean() { + return byteBuf.readBoolean(); + } + + @Override + public byte readByte() { + return byteBuf.readByte(); + } + + @Override + public int readBytes(byte[] buffer, int offset, int length) { + byteBuf.readBytes(buffer, offset, length); + return length; + } + + @Override + public int readUnsignedByte() { + return byteBuf.readUnsignedByte(); + } + + @Override + public int readUnsignedShort() { + return byteBuf.readUnsignedShort(); + } + + @Override + public short readShort() { + return byteBuf.readShort(); + } + + @Override + public int readInt() { + return byteBuf.readInt(); + } + + @Override + public long readLong() { + return byteBuf.readLong(); + } + + @Override + public float readFloat() { + return byteBuf.readFloat(); + } + + @Override + public double readDouble() { + return byteBuf.readDouble(); + } + + @Override + public void skipBytes(int count) { + byteBuf.skipBytes(count); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetOutputStream.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetOutputStream.java new file mode 100644 index 0000000..b227994 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetOutputStream.java @@ -0,0 +1,57 @@ +package mc.core.network.proto_1_12_2.netty.wrappers; + +import io.netty.buffer.ByteBuf; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.NetOutputStream_p340; + +@Slf4j +@RequiredArgsConstructor +public class WrapperNetOutputStream extends NetOutputStream_p340 { + private final ByteBuf byteBuf; + + @Override + public void writeBoolean(boolean value) { + byteBuf.writeBoolean(value); + } + + @Override + public void writeByte(int value) { + byteBuf.writeByte(value); + } + + @Override + public void writeUnsignedByte(int value) { + byteBuf.writeByte((byte)(value & 0xFF)); + } + + @Override + public void writeBytes(byte[] buffer, int offset, int lengtn) { + byteBuf.writeBytes(buffer, offset, lengtn); + } + + @Override + public void writeShort(int value) { + byteBuf.writeShort(value); + } + + @Override + public void writeInt(int value) { + byteBuf.writeInt(value); + } + + @Override + public void writeLong(long value) { + byteBuf.writeLong(value); + } + + @Override + public void writeFloat(float value) { + byteBuf.writeFloat(value); + } + + @Override + public void writeDouble(double value) { + byteBuf.writeDouble(value); + } +} diff --git a/settings.gradle b/settings.gradle index aee0b52..4559862 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,8 @@ rootProject.name = 'mc-server' include('core') // Core +include('simple_world') +include('h2_playermanager') +include('proto_1.12.2') // Protocol 1.12.2 +include('proto_1.12.2_netty') // Protocol 1.12.2 (Netty impl.) +include('anvil-loader') // Vanilla world loader (aka Anvil) diff --git a/simple_world/README.MD b/simple_world/README.MD new file mode 100644 index 0000000..02871ed --- /dev/null +++ b/simple_world/README.MD @@ -0,0 +1,39 @@ +# Simple world + +Простая реализация мира + +## Spring bean + +```xml + + + + + + + + + + + + + + + + 1;BEDROCK + 2;DIRT + 1;GRASS + + + +``` + +`spawn` - точка спавна. + +При указании точки спавна, указывать шестой параметр `World` не имеет смысла, +т.к. `SimpleWorld` всё равно перезапишет этот параметр. + +`layersBlock` - слои блоков. + +В качестве значения указывается спиток строк, каждая из которых описывает слой блоков. +Формат строк такой: `кол-во_слоёв;тип_блока`. Порядок строк такой: сверху нижние слои, а снизу - верхние. diff --git a/simple_world/build.gradle b/simple_world/build.gradle new file mode 100644 index 0000000..6d1bcee --- /dev/null +++ b/simple_world/build.gradle @@ -0,0 +1,6 @@ +version '1.0-SNAPSHOT' + +dependencies { + /* Core */ + compile_excludeCopy project(':core') +} diff --git a/simple_world/src/main/java/mc/world/simple/FlatChunkProvider.java b/simple_world/src/main/java/mc/world/simple/FlatChunkProvider.java new file mode 100644 index 0000000..45a3cd0 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/FlatChunkProvider.java @@ -0,0 +1,55 @@ +package mc.world.simple; + +import mc.core.world.block.BlockType; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkProvider; +import mc.core.world.chunk.ChunkSection; + +import java.util.ArrayList; +import java.util.List; + +public class FlatChunkProvider implements ChunkProvider { + private ChunkSection chunkSection; + + public void setLayersBlockAsString(List listOfLayers) { + List layoutsBlock = new ArrayList<>(); + + for (String value : listOfLayers) { + String[] splitValue = value.split(";"); + + BlockType blockType; + try { + blockType = BlockType.valueOf(splitValue[1]); + } catch (IllegalArgumentException e) { + continue; + } + + for (int i = 0; i < Integer.parseInt(splitValue[0]); i++) { + layoutsBlock.add(blockType); + } + } + + setLayersBlock(layoutsBlock); + } + + public void setLayersBlock(List layoutsBlock) { + this.chunkSection = new SimpleChunkSection(layoutsBlock); + } + + @Override + public Chunk getChunk(int x, int z) { + Chunk chunk = new SimpleChunk(x, z); + chunk.setChunkSection(0, chunkSection); + return chunk; + } + + @Override + public void saveChunk(Chunk chunk) { + //FIXME nope... + } + + @Override + public void saveChunk(Chunk... chunks) { + //FIXME nope... + } +} diff --git a/simple_world/src/main/java/mc/world/simple/SimpleChunk.java b/simple_world/src/main/java/mc/world/simple/SimpleChunk.java new file mode 100644 index 0000000..d6a7427 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/SimpleChunk.java @@ -0,0 +1,79 @@ +package mc.world.simple; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.core.world.Biome; +import mc.core.world.block.Block; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +@Slf4j +@RequiredArgsConstructor +public class SimpleChunk implements Chunk { + @Getter + private final int x, z; + private ChunkSection chunkSection; + + @Override + public ChunkSection getChunkSection(int height) { + return chunkSection; + } + + @Override + public void setChunkSection(int height, ChunkSection chunkSection) { + this.chunkSection = chunkSection; + this.chunkSection.setParent(this); + } + + public Block getBlock(int x, int y, int z) { + return chunkSection.getBlock( + x - (x >> 4) << 4, + y - (y >> 4) << 4, + z - (z >> 4) << 4 + ); + } + + @Override + public void setBlock(Block block) { + // ignore + } + + @Override + public int getSkyLight(int x, int y, int z) { + return chunkSection.getSkyLight( + x - (x >> 4) << 4, + y - (y >> 4) << 4, + z - (z >> 4) << 4 + ); + } + + @Override + public void setSkyLight(int x, int y, int z, int lightLevel) { + // ignore + } + + @Override + public int getAddition(int x, int y, int z) { + return chunkSection.getAddition( + x - (x >> 4) << 4, + y - (y >> 4) << 4, + z - (z >> 4) << 4 + ); + } + + @Override + public void setAddition(int x, int y, int z, int value) { + // ignore + } + + @Override + public Biome getBiome(int x, int z) { + return Biome.PLAINS; + } + + @Override + public void setBiome(int x, int z, Biome biome) { + // ignore + } +} diff --git a/simple_world/src/main/java/mc/world/simple/SimpleChunkSection.java b/simple_world/src/main/java/mc/world/simple/SimpleChunkSection.java new file mode 100644 index 0000000..cde4005 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/SimpleChunkSection.java @@ -0,0 +1,70 @@ +package mc.world.simple; + +import lombok.Getter; +import lombok.Setter; +import mc.core.world.block.Block; +import mc.core.world.block.BlockFactory; +import mc.core.world.block.BlockType; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +import java.util.List; + +public class SimpleChunkSection implements ChunkSection { + @Getter + @Setter + private Chunk parent; + private final BlockFactory blockFactory = new BlockFactory(); + private final List layersBlock; + + public SimpleChunkSection(List layersBlock) { + this.layersBlock = layersBlock; + } + + @Override + public int getSkyLight(int localX, int localY, int localZ) { + if (localY <= 3) return 0; + else return 15; + } + + @Override + public void setSkyLight(int localX, int localY, int localZ, int lightLevel) { + } + + @Override + public int getAddition(int localX, int localY, int localZ) { + return 0; + } + + @Override + public void setAddition(int localX, int localY, int localZ, int value) { + } + + @Override + public int getY() { + return 0; + } + + @Override + public void setBlock(Block block) { + } + + @Override + public Block getBlock(int localX, int localY, int localZ) { + if (localX < 0) localX = 0; + else if (localX > 15) localX = 15; + if (localY < 0) localY = 0; + else if (localY > 15) localY = 15; + if (localZ < 0) localZ = 0; + else if (localZ > 15) localZ = 15; + + if (localY >= layersBlock.size()) { + return blockFactory.create(BlockType.AIR, localX, localY, localZ); + } + + BlockType blockType = layersBlock.get(localY); + if (blockType == null) return blockFactory.create(BlockType.AIR, localX, localY, localZ); + + return blockFactory.create(blockType, localX, localY, localZ); + } +} diff --git a/simple_world/src/main/java/mc/world/simple/SimpleWorld.java b/simple_world/src/main/java/mc/world/simple/SimpleWorld.java new file mode 100644 index 0000000..4a6e345 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/SimpleWorld.java @@ -0,0 +1,67 @@ +package mc.world.simple; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.EntityLocation; +import mc.core.world.World; +import mc.core.world.WorldType; +import mc.core.world.block.Block; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkProvider; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; + +@Slf4j +@Component +public class SimpleWorld implements World, BeanNameAware { + @Getter + private String name; + @Getter + private final WorldType type = WorldType.FLAT; + private EntityLocation spawn; + @Setter + private ChunkProvider chunkProvider; + + @Override + public EntityLocation getSpawn() { + if (this.spawn == null) { + log.warn("Spawn is not defined! Set spawn [0, 6, 0]"); + setSpawn(0d, 6d, 0d); + } + + return this.spawn; + } + + @Override + public void setSpawn(EntityLocation location) { + this.spawn = location; + } + + @Override + public Chunk getChunk(int x, int z) { + return chunkProvider.getChunk(x, z); + } + + @Override + public void setChunk(int x, int z, Chunk chunk) { + throw new UnsupportedOperationException(); + } + + @Override + public Block getBlock(int x, int y, int z) { + return chunkProvider.getChunk(x >> 4, z >> 4).getBlock(x, y, z); + } + + @Override + public void setBlock(Block block) { + // nope... + } + + @Override + public void setBeanName(@Nonnull String name) { + this.name = name; + } +} diff --git a/simple_world/src/test/java/mc/world/simple/SimpleChunkSectionTest.java b/simple_world/src/test/java/mc/world/simple/SimpleChunkSectionTest.java new file mode 100644 index 0000000..b0486b9 --- /dev/null +++ b/simple_world/src/test/java/mc/world/simple/SimpleChunkSectionTest.java @@ -0,0 +1,46 @@ +package mc.world.simple; + +import com.google.common.collect.Lists; +import mc.core.world.block.Block; +import mc.core.world.block.BlockType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SimpleChunkSectionTest { + private SimpleChunkSection chunkSection; + private List layersBlock; + + @BeforeEach + void before() { + layersBlock = Lists.newArrayList( + BlockType.BEDROCK, + BlockType.DIRT, + BlockType.DIRT, + BlockType.GRASS + ); + + chunkSection = new SimpleChunkSection(layersBlock); + } + + @Test + @Disabled + void getBlock() { + for (int y = 15; y >= 0; y--) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + Block block = chunkSection.getBlock(x, y, z); + if (y > layersBlock.size()-1) { + assertEquals(block.getType(), BlockType.AIR); + } else { + assertEquals(block.getType(), layersBlock.get(y)); + } + } + } + } + } +} \ No newline at end of file diff --git a/simple_world/src/test/java/mc/world/simple/SimpleWorldTest.java b/simple_world/src/test/java/mc/world/simple/SimpleWorldTest.java new file mode 100644 index 0000000..110a830 --- /dev/null +++ b/simple_world/src/test/java/mc/world/simple/SimpleWorldTest.java @@ -0,0 +1,37 @@ +package mc.world.simple; + +import mc.core.EntityLocation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {TestSpringConfig.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class SimpleWorldTest { + @Autowired + private SimpleWorld world; + + @Test + void spawn() { + final EntityLocation location = new EntityLocation(1d, 2d, 3d,4f, 5f); + + world.setSpawn(location); + assertEquals(location, world.getSpawn()); + assertSame(location, world.getSpawn()); + + world.setSpawn(1d, 2d, 3d, 4f, 5f); + assertEquals(location, world.getSpawn()); + assertNotSame(location, world.getSpawn()); + + location.setYawPitch(0, 0); + world.setSpawn(1d, 2d, 3d); + assertEquals(location, world.getSpawn()); + assertNotSame(location, world.getSpawn()); + } +} \ No newline at end of file diff --git a/simple_world/src/test/java/mc/world/simple/TestSpringConfig.java b/simple_world/src/test/java/mc/world/simple/TestSpringConfig.java new file mode 100644 index 0000000..865fdd9 --- /dev/null +++ b/simple_world/src/test/java/mc/world/simple/TestSpringConfig.java @@ -0,0 +1,38 @@ +package mc.world.simple; + +import com.google.common.collect.Lists; +import mc.core.world.block.BlockType; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ComponentScan("mc.world.simple") +public class TestSpringConfig { + @Bean + public List layersBlock() { + return Lists.newArrayList( + BlockType.BEDROCK, + BlockType.DIRT, + BlockType.DIRT, + BlockType.GRASS + ); + } + + @Bean + public SimpleChunkSection chunkSection(List layersBlock) { + return new SimpleChunkSection(layersBlock); + } + + @Bean + public SimpleWorld simpleWorld(List layersBlock) { + FlatChunkProvider chunkProvider = new FlatChunkProvider(); + chunkProvider.setLayersBlock(layersBlock); + + SimpleWorld world = new SimpleWorld(); + world.setChunkProvider(chunkProvider); + return world; + } +}