diff --git a/README.MD b/README.MD
index 71023a3..4f0e682 100644
--- a/README.MD
+++ b/README.MD
@@ -1,13 +1,18 @@
# MC-CORE
-
-
+
+
Модульный **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 extends Tag>> tagClass;
+ private final String typeName;
+ private final int id;
+
+ private TagType(Class extends Tag>> 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 extends Tag>> getTagClass() {
+ return tagClass;
+ }
+
+ public String getTypeName() {
+ return typeName;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public static TagType getByTagClass(Class extends Tag>> 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 extends Tag> 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 extends CSPacket> getClientSidePacket(int id) {
+ return clientSidePacketsMap.get(id);
+ }
+
+ public Integer getServerSidePacket(Class extends SCPacket> 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