diff --git a/.gitignore b/.gitignore index a43309c..311183a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,14 @@ out/ *.ipr *.iws *.ids + +## GRADLE ## +.gradle/ +build/ +gradle/ +gradlew +gradlew.* + +## PROJECT ## +libs/ +*.log diff --git a/anvil-loader/build.gradle b/anvil-loader/build.gradle new file mode 100644 index 0000000..2328060 --- /dev/null +++ b/anvil-loader/build.gradle @@ -0,0 +1,9 @@ +group 'mc' +version '1.0-SNAPSHOT' + +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/src/main/java/com/flowpowered/nbt/LongArrayTag.java b/anvil-loader/src/main/java/com/flowpowered/nbt/LongArrayTag.java new file mode 100644 index 0000000..47b09ee --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/LongArrayTag.java @@ -0,0 +1,73 @@ +package com.flowpowered.nbt; + +import java.util.Arrays; + +public class LongArrayTag extends Tag { + /** + * The value. + */ + private final long[] value; + + /** + * Creates the tag. + * + * @param name The name. + * @param value The value. + */ + public LongArrayTag(String name, long[] value) { + super(TagType.TAG_LONG_ARRAY, name); + this.value = value; + } + + @Override + public long[] getValue() { + return value; + } + + @Override + public String toString() { + StringBuilder hex = new StringBuilder(); + for (long s : value) { + String hexDigits = Long.toHexString(s).toUpperCase(); + if (hexDigits.length() == 1) { + hex.append("0"); + } + hex.append(hexDigits).append(" "); + } + + String name = getName(); + String append = ""; + if (name != null && !name.equals("")) { + append = "(\"" + this.getName() + "\")"; + } + return "TAG_Long_Array" + append + ": " + hex.toString(); + } + + @Override + public LongArrayTag clone() { + long[] clonedArray = cloneArray(value); + + return new LongArrayTag(getName(), clonedArray); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof IntArrayTag)) { + return false; + } + + LongArrayTag tag = (LongArrayTag) other; + return Arrays.equals(value, tag.value) && getName().equals(tag.getName()); + } + + private long[] cloneArray(long[] longArray) { + if (longArray == null) { + return null; + } else { + int length = longArray.length; + byte[] newArray = new byte[length]; + System.arraycopy(longArray, 0, newArray, 0, length); + return longArray; + } + } +} diff --git a/anvil-loader/src/main/java/com/flowpowered/nbt/NBTConstants.java b/anvil-loader/src/main/java/com/flowpowered/nbt/NBTConstants.java new file mode 100644 index 0000000..f6dd1b4 --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/NBTConstants.java @@ -0,0 +1,38 @@ +package com.flowpowered.nbt; + +import java.nio.charset.Charset; + +/** + * A class which holds constant values. + */ +public final class NBTConstants { + /** + * The character set used by NBT (UTF-8). + */ + public static final Charset CHARSET = Charset.forName("UTF-8"); + /** + * Tag type constants. + */ + @Deprecated + public static final int TYPE_END = TagType.TAG_END.getId(), + TYPE_BYTE = TagType.TAG_BYTE.getId(), + TYPE_SHORT = TagType.TAG_SHORT.getId(), + TYPE_INT = TagType.TAG_INT.getId(), + TYPE_LONG = TagType.TAG_LONG.getId(), + TYPE_FLOAT = TagType.TAG_FLOAT.getId(), + TYPE_DOUBLE = TagType.TAG_DOUBLE.getId(), + TYPE_BYTE_ARRAY = TagType.TAG_BYTE_ARRAY.getId(), + TYPE_STRING = TagType.TAG_STRING.getId(), + TYPE_LIST = TagType.TAG_LIST.getId(), + TYPE_COMPOUND = TagType.TAG_COMPOUND.getId(), + TYPE_INT_ARRAY = TagType.TAG_INT_ARRAY.getId(), + TYPE_SHORT_ARRAY = TagType.TAG_SHORT_ARRAY.getId(), + TYPE_LONG_ARRAY = TagType.TAG_LONG_ARRAY.getId(); + + /** + * Default private constructor. + */ + private NBTConstants() { + } +} + diff --git a/anvil-loader/src/main/java/com/flowpowered/nbt/TagType.java b/anvil-loader/src/main/java/com/flowpowered/nbt/TagType.java new file mode 100644 index 0000000..444e3e2 --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/TagType.java @@ -0,0 +1,94 @@ +package com.flowpowered.nbt; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public enum TagType { + TAG_END(EndTag.class, "TAG_End", 0), + TAG_BYTE(ByteTag.class, "TAG_Byte", 1), + TAG_SHORT(ShortTag.class, "TAG_Short", 2), + TAG_INT(IntTag.class, "TAG_Int", 3), + TAG_LONG(LongTag.class, "TAG_Long", 4), + TAG_FLOAT(FloatTag.class, "TAG_Float", 5), + TAG_DOUBLE(DoubleTag.class, "TAG_Double", 6), + TAG_BYTE_ARRAY(ByteArrayTag.class, "TAG_Byte_Array", 7), + TAG_STRING(StringTag.class, "TAG_String", 8), + @SuppressWarnings("unchecked") + TAG_LIST((Class) ListTag.class, "TAG_List", 9), + // Java generics, y u so suck + TAG_COMPOUND(CompoundTag.class, "TAG_Compound", 10), + TAG_INT_ARRAY(IntArrayTag.class, "TAG_Int_Array", 11), + TAG_LONG_ARRAY(LongArrayTag.class, "TAG_Long_Array", 12), + TAG_SHORT_ARRAY(ShortArrayTag.class, "TAG_Short_Array", 100),; + private static final Map>, TagType> BY_CLASS = new HashMap>, TagType>(); + private static final Map BY_NAME = new HashMap(); + private static final TagType[] BY_ID; + + static { + BY_ID = new TagType[BaseData.maxId + 1]; + for (TagType type : TagType.values()) { + BY_CLASS.put(type.getTagClass(), type); + BY_NAME.put(type.getTypeName(), type); + BY_ID[type.getId()] = type; + } + } + + private final Class> tagClass; + private final String typeName; + private final int id; + + private TagType(Class> tagClass, String typeName, int id) { + this.tagClass = tagClass; + this.typeName = typeName; + this.id = id; + // Such a hack, shame that Java makes this such a pain + if (this.id > BaseData.maxId) { + BaseData.maxId = this.id; + } + } + + public Class> getTagClass() { + return tagClass; + } + + public String getTypeName() { + return typeName; + } + + public int getId() { + return id; + } + + public static TagType getByTagClass(Class> clazz) { + TagType ret = BY_CLASS.get(clazz); + if (ret == null) { + throw new IllegalArgumentException("Tag type " + clazz + " is unknown!"); + } + return ret; + } + + public static TagType getByTypeName(String typeName) { + TagType ret = BY_NAME.get(typeName); + if (ret == null) { + throw new IllegalArgumentException("Tag type " + typeName + " is unknown!"); + } + return ret; + } + + public static TagType getById(int id) { + if (id >= 0 && id < BY_ID.length) { + TagType ret = BY_ID[id]; + if (ret == null) { + throw new IllegalArgumentException("Tag type id " + id + " is unknown!"); + } + return ret; + } else { + throw new IndexOutOfBoundsException("Tag type id " + id + " is out of bounds!"); + } + } + + private static class BaseData { + private static int maxId = 0; + } +} diff --git a/anvil-loader/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java b/anvil-loader/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java new file mode 100644 index 0000000..8298779 --- /dev/null +++ b/anvil-loader/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java @@ -0,0 +1,210 @@ +package com.flowpowered.nbt.stream; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import com.flowpowered.nbt.*; + +/** + * This class reads NBT, or Named Binary Tag streams, and produces an object graph of subclasses of the {@link Tag} object.

The NBT format was created by Markus Persson, and the specification + * may be found at https://flowpowered.com/nbt/spec.txt. + */ +public final class NBTInputStream implements Closeable { + /** + * The data input stream. + */ + private final EndianSwitchableInputStream is; + + /** + * Creates a new {@link NBTInputStream}, which will source its data from the specified input stream. This assumes the stream is compressed. + * + * @param is The input stream. + * @throws java.io.IOException if an I/O error occurs. + */ + public NBTInputStream(InputStream is) throws IOException { + this(is, true, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if the stream is compressed with GZIP or not. This assumes the + * stream uses big endian encoding. + * + * @param is The input stream. + * @param compressed A flag indicating if the stream is compressed. + * @throws java.io.IOException if an I/O error occurs. + */ + public NBTInputStream(InputStream is, boolean compressed) throws IOException { + this(is, compressed, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if the stream is compressed with GZIP or not. + * + * @param is The input stream. + * @param compressed A flag indicating if the stream is compressed. + * @param endianness Whether to read numbers from the InputStream with little endian encoding. + * @throws java.io.IOException if an I/O error occurs. + */ + public NBTInputStream(InputStream is, boolean compressed, ByteOrder endianness) throws IOException { + this.is = new EndianSwitchableInputStream(compressed ? new GZIPInputStream(is) : is, endianness); + } + + /** + * Reads an NBT {@link Tag} from the stream. + * + * @return The tag that was read. + * @throws java.io.IOException if an I/O error occurs. + */ + public Tag readTag() throws IOException { + return readTag(0); + } + + /** + * Reads an NBT {@link Tag} from the stream. + * + * @param depth The depth of this tag. + * @return The tag that was read. + * @throws java.io.IOException if an I/O error occurs. + */ + private Tag readTag(int depth) throws IOException { + int typeId = is.readByte() & 0xFF; + TagType type = TagType.getById(typeId); + + String name; + if (type != TagType.TAG_END) { + int nameLength = is.readShort() & 0xFFFF; + byte[] nameBytes = new byte[nameLength]; + is.readFully(nameBytes); + name = new String(nameBytes, NBTConstants.CHARSET.name()); + } else { + name = ""; + } + + return readTagPayload(type, name, depth); + } + + /** + * Reads the payload of a {@link Tag}, given the name and type. + * + * @param type The type. + * @param name The name. + * @param depth The depth. + * @return The tag. + * @throws java.io.IOException if an I/O error occurs. + */ + @SuppressWarnings ({"unchecked", "rawtypes"}) + private Tag readTagPayload(TagType type, String name, int depth) throws IOException { + switch (type) { + case TAG_END: + if (depth == 0) { + throw new IOException("TAG_End found without a TAG_Compound/TAG_List tag preceding it."); + } else { + return new EndTag(); + } + + case TAG_BYTE: + return new ByteTag(name, is.readByte()); + + case TAG_SHORT: + return new ShortTag(name, is.readShort()); + + case TAG_INT: + return new IntTag(name, is.readInt()); + + case TAG_LONG: + return new LongTag(name, is.readLong()); + + case TAG_FLOAT: + return new FloatTag(name, is.readFloat()); + + case TAG_DOUBLE: + return new DoubleTag(name, is.readDouble()); + + case TAG_BYTE_ARRAY: + int length = is.readInt(); + byte[] bytes = new byte[length]; + is.readFully(bytes); + return new ByteArrayTag(name, bytes); + + case TAG_STRING: + length = is.readShort(); + bytes = new byte[length]; + is.readFully(bytes); + return new StringTag(name, new String(bytes, NBTConstants.CHARSET.name())); + + case TAG_LIST: + TagType childType = TagType.getById(is.readByte()); + length = is.readInt(); + + Class clazz = childType.getTagClass(); + List tagList = new ArrayList(length); + for (int i = 0; i < length; i++) { + Tag tag = readTagPayload(childType, "", depth + 1); + if (tag instanceof EndTag) { + throw new IOException("TAG_End not permitted in a list."); + } else if (!clazz.isInstance(tag)) { + throw new IOException("Mixed tag types within a list."); + } + tagList.add(tag); + } + + return new ListTag(name, clazz, tagList); + + case TAG_COMPOUND: + CompoundMap compoundTagList = new CompoundMap(); + while (true) { + Tag tag = readTag(depth + 1); + if (tag instanceof EndTag) { + break; + } else { + compoundTagList.put(tag); + } + } + + return new CompoundTag(name, compoundTagList); + + case TAG_INT_ARRAY: + length = is.readInt(); + int[] ints = new int[length]; + for (int i = 0; i < length; i++) { + ints[i] = is.readInt(); + } + return new IntArrayTag(name, ints); + + case TAG_SHORT_ARRAY: + length = is.readInt(); + short[] shorts = new short[length]; + for (int i = 0; i < length; i++) { + shorts[i] = is.readShort(); + } + return new ShortArrayTag(name, shorts); + + case TAG_LONG_ARRAY: + length = is.readInt(); + long[] longs = new long[length]; + for (int i = 0; i < length; i++) { + longs[i] = is.readLong(); + } + return new LongArrayTag(name, longs); + + default: + throw new IOException("Invalid tag type: " + type + "."); + } + } + + public void close() throws IOException { + is.close(); + } + + /** + * @return whether this NBTInputStream reads numbers in little-endian format. + */ + public ByteOrder getByteOrder() { + return is.getEndianness(); + } +} diff --git a/anvil-loader/src/main/java/mc/world/anvil/AnvilBlock.java b/anvil-loader/src/main/java/mc/world/anvil/AnvilBlock.java new file mode 100644 index 0000000..990e6e9 --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/AnvilBlock.java @@ -0,0 +1,80 @@ +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); + BlockType type = BlockType.getByIdMeta(id & 0xFF, meta); + if (type.equals(BlockType.BEDROCK) && id != 7) { + log.warn("ChunkSection: {},{},{} | Block: {}", + chunkSection.getParent().getX(), + chunkSection.getY(), + chunkSection.getParent().getZ(), + location.toString()); + } + return type; + } + + @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..a0a985d --- /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 >> 4) << 4 | (x >> 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..1892e57 --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/AnvilChunkSection.java @@ -0,0 +1,57 @@ +package mc.world.anvil; + +import gnu.trove.list.TByteList; +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 TByteLinkedList(); + @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/LevelInfo.java b/anvil-loader/src/main/java/mc/world/anvil/LevelInfo.java new file mode 100644 index 0000000..a2da8ba --- /dev/null +++ b/anvil-loader/src/main/java/mc/world/anvil/LevelInfo.java @@ -0,0 +1,26 @@ +package mc.world.anvil; + +import com.flowpowered.nbt.*; +import lombok.Getter; +import lombok.ToString; +import mc.core.world.block.BlockLocation; + +@Getter +@ToString +class LevelInfo { + private long seed; + private BlockLocation spawn; + private int version; + + LevelInfo(CompoundTag levelDatTag) { + CompoundMap dataMapTag = ((CompoundTag) levelDatTag.getValue().get("Data")).getValue(); + + seed = ((LongTag) dataMapTag.get("RandomSeed")).getValue(); + spawn = new BlockLocation( + ((IntTag) dataMapTag.get("SpawnX")).getValue(), + ((IntTag) dataMapTag.get("SpawnY")).getValue(), + ((IntTag) dataMapTag.get("SpawnZ")).getValue() + ); + version = ((IntTag) dataMapTag.get("version")).getValue(); + } +} 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/build.gradle b/build.gradle new file mode 100644 index 0000000..676a480 --- /dev/null +++ b/build.gradle @@ -0,0 +1,162 @@ +import java.nio.file.Files +import java.nio.file.Paths + +buildscript { + repositories { + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath (group: 'org.sonarsource.scanner.gradle', name: 'sonarqube-gradle-plugin', version: '2.6.2') + } +} + +/** + * Проверка кода в SonarQube. + * Для запуска локальной проверки кода, используются следующий command line: + * gradle sonarqube \ + * -Dsonar.host.url=http://127.0.0.1:9000 + * -Dsonar.login= + * где + * - - сгенерированный токен учетки "сонара" + */ +plugins { + id "org.sonarqube" version "2.6.2" +} + +allprojects { + apply plugin: 'java' + + compileJava { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + options.encoding = 'UTF-8' + } + + repositories { + mavenCentral() + maven { url 'https://oss.sonatype.org/content/groups/public/' } + } +} + +subprojects { + group 'mc' + + ext { + slf4j_version = '1.7.25' + spring_version = '5.1.0.RELEASE' + lombok_version = '1.18.4' + junit_version = '5.3.1' + } + + configurations { + compile_excludeCopy + compile.extendsFrom compile_excludeCopy + } + + dependencies { + compile (group: 'org.jetbrains', name: 'annotations', version: '16.0.3') + + /* Logger */ + compile (group: 'org.slf4j', name: 'slf4j-api', version: slf4j_version) + compile (group: 'org.slf4j', name: 'jcl-over-slf4j', version: slf4j_version) + + /* Spring */ + compile (group: 'org.springframework', name: 'spring-context', version: spring_version) + + /* Lombok */ + annotationProcessor (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + compile (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + testAnnotationProcessor (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + testCompile (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + + /* Testing */ + testImplementation (group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junit_version) + testRuntimeOnly(group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit_version) + testImplementation(group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit_version) + testCompile (group: 'org.slf4j', name: 'slf4j-simple', version: slf4j_version) + testCompile (group: 'org.mockito', name: 'mockito-core', version: '1.10.19') + testCompile (group: 'org.springframework', name: 'spring-test', version: spring_version) + } + + test { + useJUnitPlatform() + } + + task copyDep(type: Copy) { + into 'libs' + from configurations.compile + configurations.runtime - configurations.compile_excludeCopy + } + + task cleanDep(type: Delete) { + delete 'libs' + } + + /** + * Сборка + */ + task deploy() { + dependsOn { jar } + + doLast { + def deployDir = System.getProperty("deploy") + if (deployDir == null) { + println "Need param -Ddeploy=path/to/deploy" + throw new Exception("Need param -Ddir=path/to/deploy") + } + + def target = Paths.get(deployDir, jar.archivePath.getName()) + if (Files.notExists(target)) { + println jar.archivePath + Files.copy(jar.archivePath.toPath(), target) + } + + + + def libsDir = System.getProperty("libs", deployDir+File.separator+"libs") + if (Files.notExists(Paths.get(libsDir))) { + (new File(libsDir)).mkdirs() + } + + def libsCollection = configurations.compile + configurations.runtime - configurations.compile_excludeCopy + libsCollection.each{ libFile -> + target = Paths.get(libsDir, libFile.getName()) + if (Files.notExists(target)) { + println libFile + Files.copy(libFile.toPath(), target) + } + } + } + } +} + +/** + * Запуск сервера. + * Для указания рабочей папки, указываем JVM параметр + * -DworkDir=path\to\workdir + * Если используется отдельная папка для имплементации логгера, то указываем + * -DlogImplDir=path\to\logimpldir + * Если необходимо передать дополнительные JVM параметры серверу, то указываем их с двойной "D", например: + * -DDspringConfig=spring.xml + * -DDlog4j.configurationFile=log4j2.xml + */ +task runServer(type: JavaExec) { + main = 'mc.core.Main' + + workingDir = System.getProperty("workDir", ".") + + subprojects.findAll().each{ prj -> + classpath += prj.sourceSets.main.runtimeClasspath + } + + if (System.getProperty("logImplDir") != null) { + classpath += files(fileTree(dir: new File(System.getProperty("logImplDir")))) + } else { + classpath += files(fileTree(dir: new File(workingDir, "log-impl"))) + } + + System.getProperties().stringPropertyNames().stream() + .filter{propName -> propName.startsWith("D")} + .forEach{propName -> jvmArgs += "-D" + propName.substring(1) + "=" + System.getProperty(propName)} + + ignoreExitValue = true +} diff --git a/core/README.MD b/core/README.MD new file mode 100644 index 0000000..cd77165 --- /dev/null +++ b/core/README.MD @@ -0,0 +1,69 @@ +# Core + +Ядро сервера + +## Spring beans + +### ConfigFromSpring + +Implements: `mc.core.Config` + +Bean: + +```xml + + + + + +``` + +### IdleTime + +Implements: `mc.core.time.TimeProcessor` + +Bean: + +```xml + + + +``` + +в качестве параметра конструктора указывается стартовое время. + +### TimePerTick + +Implements: `mc.core.time.TimeProcessor` + +Bean: + +```xml + + + +``` + +в качестве параметра указывается стартовое время. + +### RealTime + +Implements: `mc.core.time.TimeProcessor` + +Bean: + +```xml + +``` + +### GameLoop + +Bean: + +```xml + + + +``` + +`gameTimer` - бин, управляющий ходом времени diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..1b923c1 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,14 @@ +version '1.0-SNAPSHOT' + +apply plugin: 'maven' +apply plugin: 'application' + +mainClassName = "mc.core.Main" + +dependencies { + /* Components */ + compile (group: 'commons-io', name: 'commons-io', version: '2.6') + compile (group: 'com.google.guava', name: 'guava', version: '26.0-jre') + /* Named Binary Tags */ + compile (group: 'com.flowpowered', name: 'flow-nbt', version: '1.0.1-SNAPSHOT') +} diff --git a/core/src/main/java/mc/core/Config.java b/core/src/main/java/mc/core/Config.java new file mode 100644 index 0000000..9e67fdc --- /dev/null +++ b/core/src/main/java/mc/core/Config.java @@ -0,0 +1,11 @@ +/* + * DmitriyMX + * 2018-04-08 + */ +package mc.core; + +public interface Config { + int getMaxPlayers(); + String getDescriptionServer(); + byte[] getFaviconBase64(); +} diff --git a/core/src/main/java/mc/core/CoreEventListener.java b/core/src/main/java/mc/core/CoreEventListener.java new file mode 100644 index 0000000..cb2fd1e --- /dev/null +++ b/core/src/main/java/mc/core/CoreEventListener.java @@ -0,0 +1,85 @@ +package mc.core; + +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.Subscriber; +import mc.core.eventbus.events.CS_PlayerMoveEvent; +import mc.core.eventbus.events.SC_ChunkLoadEvent; +import mc.core.eventbus.events.SC_ChunkUnloadEvent; +import mc.core.utils.CompactedCoords; +import mc.core.world.chunk.Chunk; + +import javax.annotation.PostConstruct; +import java.util.Iterator; + +@Slf4j +public class CoreEventListener { + @PostConstruct + public void registerEventHandlers() { + EventBus.getInstance().registerSubscribes(this); + } + + @Subscriber + public void handlerPlayerMoveEvent(CS_PlayerMoveEvent event) { + Chunk chunk; + chunk = event.getPlayer().getWorld().getChunk(event.getOldLocation().toBlockLocation()); // Old chunk + if (chunk == null) return; + int ccX = chunk.getX(); + int ccZ = chunk.getZ(); + chunk = event.getPlayer().getWorld().getChunk(event.getNewLocation().toBlockLocation()); // Next chunk + if (chunk == null) return; + int ncX = chunk.getX(); + int ncZ = chunk.getZ(); + + if (event.isRecalcChunk() || (ncX != ccX || ncZ != ccZ)) { + final int viewDistance = event.getPlayer().getSettings().getViewDistance() + 1; + int cMinX = chunk.getX() - viewDistance; + int cMaxX = chunk.getX() + viewDistance; + int cMinZ = chunk.getZ() - viewDistance; + int cMaxZ = chunk.getZ() + viewDistance; + + SC_ChunkLoadEvent eventChunkLoad = new SC_ChunkLoadEvent(event.getPlayer()); + for (int cZ = cMinZ; cZ <= cMaxZ; cZ++) { + for (int cX = cMinX; cX <= cMaxX; cX++) { + int compressXZ = CompactedCoords.compressXZ(cX, cZ); + if (!event.getPlayer().getLoadedChunks().contains(compressXZ)) { + if (!event.getPlayer().getLoadedChunks().contains(compressXZ)) { + eventChunkLoad.getNeedLoadChunks().add(compressXZ); + event.getPlayer().getLoadedChunks().add(compressXZ); + } + } + } + } + + if (!eventChunkLoad.getNeedLoadChunks().isEmpty()) { + EventBus.getInstance().post(eventChunkLoad); + } + + SC_ChunkUnloadEvent eventChunkUnload = new SC_ChunkUnloadEvent(event.getPlayer()); + Iterator itr = event.getPlayer().getLoadedChunks().iterator(); + while(itr.hasNext()) { + int compressXZ = itr.next(); + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + if (xz[0] > cMaxX || xz[0] < cMinX || xz[1] > cMaxZ || xz[1] < cMinZ) { + eventChunkUnload.getNeedUnloadChunks().add(compressXZ); + itr.remove(); + } + } + + if (!eventChunkUnload.getNeedUnloadChunks().isEmpty()) { + EventBus.getInstance().post(eventChunkUnload); + } + } + + event.getPlayer().getLocation().setXYZ( + event.getNewLocation().getX(), + event.getNewLocation().getY(), + event.getNewLocation().getZ() + ); + + // TODO отсылать клиенту только(!) для корректировки позиции + // SC_PlayerMoveEvent nextEvent = new SC_PlayerMoveEvent(event.getPlayer()); + // nextEvent.setNewLocation(event.getNewLocation()); + // EventBusGetter.INSTANCE.post(nextEvent); + } +} diff --git a/core/src/main/java/mc/core/EntityLocation.java b/core/src/main/java/mc/core/EntityLocation.java new file mode 100644 index 0000000..53fadcb --- /dev/null +++ b/core/src/main/java/mc/core/EntityLocation.java @@ -0,0 +1,60 @@ +package mc.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import mc.core.world.block.BlockLocation; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class EntityLocation implements Cloneable { + private double x, y, z; + private float yaw, pitch; + + public static EntityLocation ZERO() { + return new EntityLocation(0d,0d,0d,0f,0f); + } + + public void set(EntityLocation location) { + setXYZ(location.x, location.y, location.z); + setYawPitch(location.yaw, location.pitch); + } + + public void setXYZ(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public void setYawPitch(float yaw, float pitch) { + this.yaw = yaw; + this.pitch = pitch; + } + + public int getBlockX() { + return (int) Math.floor(x); + } + + public int getBlockY() { + return (int) Math.floor(y); + } + + public int getBlockZ() { + return (int) Math.floor(z); + } + + public BlockLocation toBlockLocation() { + return new BlockLocation(getBlockX(), getBlockY(), getBlockZ()); + } + + @Override + public EntityLocation clone() { + try { + return (EntityLocation) super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + return ZERO(); + } + } +} diff --git a/core/src/main/java/mc/core/GameLoop.java b/core/src/main/java/mc/core/GameLoop.java new file mode 100644 index 0000000..7838fee --- /dev/null +++ b/core/src/main/java/mc/core/GameLoop.java @@ -0,0 +1,64 @@ +/* + * DmitriyMX + * 2018-04-21 + */ +package mc.core; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.EventBus; +import mc.core.player.PlayerManager; +import mc.core.time.TimeProcessor; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public class GameLoop extends Thread { + private final TpsWatcher TPS_WATCHER = TpsWatcher.getInstance(); + @Autowired + private PlayerManager playerManager; + + /* Time */ + @Setter + private TimeProcessor gameTimer; + + public GameLoop() { + super(); + setTps(20); + setPercentWarnLowTps(5); + } + + public void setPercentWarnLowTps(int value) { + TPS_WATCHER.setPercentWarnLowTps(value); + } + + public void setTps(int tps) { + TPS_WATCHER.setTps(tps); + } + + public void setTps(boolean value) { + TPS_WATCHER.setTraceTPS(value); + } + + @Override + public void run() { + TPS_WATCHER.startWatch(); + + while (!isInterrupted()) { + TPS_WATCHER.check(); + + /* --- --- --- */ + + EventBus.getInstance().process(); + + /* TODO нужно перенести этот функционал на Network */ + playerManager.getBroadcastChannel().sendTimeUpdate( + gameTimer.getGameTime(), + gameTimer.getWorldAge() + ); + + /* --- --- --- */ + + TPS_WATCHER.tick(); + } + } +} diff --git a/core/src/main/java/mc/core/ImmutableEntityLocation.java b/core/src/main/java/mc/core/ImmutableEntityLocation.java new file mode 100644 index 0000000..f71c1e3 --- /dev/null +++ b/core/src/main/java/mc/core/ImmutableEntityLocation.java @@ -0,0 +1,57 @@ +package mc.core; + +public class ImmutableEntityLocation extends EntityLocation { + public ImmutableEntityLocation(double x, double y, double z, float yaw, float pitch) { + super(x, y, z, yaw, pitch); + } + + public ImmutableEntityLocation(EntityLocation location) { + this( + location.getX(), + location.getY(), + location.getZ(), + location.getYaw(), + location.getPitch() + ); + } + + @Override + public void setX(double x) { + throw new UnsupportedOperationException(); + } + + @Override + public void setY(double y) { + throw new UnsupportedOperationException(); + } + + @Override + public void setZ(double z) { + throw new UnsupportedOperationException(); + } + + @Override + public void setYaw(float yaw) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPitch(float pitch) { + throw new UnsupportedOperationException(); + } + + @Override + public void set(EntityLocation location) { + throw new UnsupportedOperationException(); + } + + @Override + public void setXYZ(double x, double y, double z) { + throw new UnsupportedOperationException(); + } + + @Override + public void setYawPitch(float yaw, float pitch) { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/mc/core/Main.java b/core/src/main/java/mc/core/Main.java new file mode 100644 index 0000000..211f210 --- /dev/null +++ b/core/src/main/java/mc/core/Main.java @@ -0,0 +1,53 @@ +/* + * DmitriyMX + * 2018-03-25 + */ +package mc.core; + +import lombok.extern.slf4j.Slf4j; +import mc.core.network.Server; +import mc.core.network.StartServerException; +import org.apache.commons.io.IOUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.FileSystemXmlApplicationContext; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +@Slf4j +public class Main { + private static ApplicationContext createContext() { + final String springXml = System.getProperty("springConfig", "./spring.xml"); + + if (!Files.exists(Paths.get(springXml))) { + log.info("File \"{}\" not found. Get default config.", springXml); + try (FileOutputStream fos = new FileOutputStream(springXml)) { + IOUtils.copy(Main.class.getResourceAsStream("/spring.xml"), fos); + } catch (IOException e) { + log.error("Get default spring config", e); + System.exit(-1); + } + } + + return new FileSystemXmlApplicationContext(springXml); + } + + public static void main(String[] args) { + ApplicationContext appContext = createContext(); + + GameLoop gameLoop = appContext.getBean(GameLoop.class); + gameLoop.start(); + + Server server = appContext.getBean("server", Server.class); + Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); + try { + server.start(); + } catch (StartServerException e) { + log.error("Can't start server", e); + } + + gameLoop.interrupt(); + } +} diff --git a/core/src/main/java/mc/core/TpsWatcher.java b/core/src/main/java/mc/core/TpsWatcher.java new file mode 100644 index 0000000..25c43e7 --- /dev/null +++ b/core/src/main/java/mc/core/TpsWatcher.java @@ -0,0 +1,82 @@ +/* + * DmitriyMX + * 2018-06-12 + */ +package mc.core; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class TpsWatcher { + private static final TpsWatcher instance = new TpsWatcher(); + + private boolean traceTps = false; + + private int tps; + private long pause; + private int lowTps; + private int percentLowTps; + + private int factTps; + private long lastTime; + private long futureTime; + + public static TpsWatcher getInstance() { + return instance; + } + + private TpsWatcher(){ } + + public void setTps(int value) { + if (value > 1000) { + log.warn("TPS can't be '{}'. Set 1000", value); + value = 1000; + } + + tps = value; + pause = (1000 / value); + } + + public void setPercentWarnLowTps(int value) { + if (value > 100) { + log.warn("Percent warn low TPS can't be '{}'. Set 100", value); + value = 100; + } + + lowTps = tps - (int)(tps * (value / 100f)); + percentLowTps = value; + } + + public void setTraceTPS(boolean value) { + traceTps = value; + } + + public void startWatch() { + log.info("Target TPS: {}; Low TPS: {}({}%)", tps, lowTps, percentLowTps); + factTps = 0; + lastTime = System.currentTimeMillis(); + } + + public void check() { + if ((System.currentTimeMillis() - lastTime) > 1000) { + lastTime = System.currentTimeMillis(); + if (factTps < lowTps) { + log.warn("Low TPS: {}/{}", factTps, tps); + } else if (traceTps) { + log.trace("TPS: {}/{}", factTps, tps); + } + factTps = 0; + } + + futureTime = System.currentTimeMillis() + pause; + } + + public void tick() { + factTps++; + try { + long pause = futureTime - System.currentTimeMillis(); + Thread.sleep((pause <= 0 ? 0 : pause)); + } catch (InterruptedException ignored) { + } + } +} diff --git a/core/src/main/java/mc/core/chat/ChatProcessor.java b/core/src/main/java/mc/core/chat/ChatProcessor.java new file mode 100644 index 0000000..b4d6a36 --- /dev/null +++ b/core/src/main/java/mc/core/chat/ChatProcessor.java @@ -0,0 +1,15 @@ +/* + * DmitriyMX + * 2018-05-06 + */ +package mc.core.chat; + +import mc.core.player.Player; +import org.slf4j.Marker; +import org.slf4j.helpers.BasicMarkerFactory; + +public abstract class ChatProcessor { + protected static final Marker CHAT_MARKER = new BasicMarkerFactory().getMarker("Chat"); + + public abstract void process(Player player, String message); +} diff --git a/core/src/main/java/mc/core/chat/CommandExecutor.java b/core/src/main/java/mc/core/chat/CommandExecutor.java new file mode 100644 index 0000000..e0d6e66 --- /dev/null +++ b/core/src/main/java/mc/core/chat/CommandExecutor.java @@ -0,0 +1,17 @@ +/* + * DmitriyMX + * 2018-05-22 + */ +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 new file mode 100644 index 0000000..f497a58 --- /dev/null +++ b/core/src/main/java/mc/core/chat/CommanderChatProcessor.java @@ -0,0 +1,94 @@ +/* + * DmitriyMX + * 2018-05-23 + */ +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/chat/MessageType.java b/core/src/main/java/mc/core/chat/MessageType.java new file mode 100644 index 0000000..c100632 --- /dev/null +++ b/core/src/main/java/mc/core/chat/MessageType.java @@ -0,0 +1,18 @@ +/* + * DmitriyMX + * 2018-06-24 + */ +package mc.core.chat; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum MessageType { + CHAT_MESSAGE(0), // chat box + SYSTEM_MESSAGE(1), // chat box + GAME_INFO(2); // above hotbar + + private final int id; +} diff --git a/core/src/main/java/mc/core/chat/SimpleChatProcessor.java b/core/src/main/java/mc/core/chat/SimpleChatProcessor.java new file mode 100644 index 0000000..6964f16 --- /dev/null +++ b/core/src/main/java/mc/core/chat/SimpleChatProcessor.java @@ -0,0 +1,29 @@ +/* + * DmitriyMX + * 2018-05-06 + */ +package mc.core.chat; + +import lombok.extern.slf4j.Slf4j; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.text.Text; +import mc.core.text.TextColor; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +public class SimpleChatProcessor extends ChatProcessor { + @Autowired + private PlayerManager playerManager; + + @Override + public void process(Player player, String message) { + log.info(CHAT_MARKER, "<{}> {}", player.getName(), message); + playerManager.getBroadcastChannel().sendChatMessage( + Text.builder(TextColor.GOLD, player.getName()) + .append(Text.of(TextColor.GRAY, ": ")) + .append(Text.of(TextColor.WHITE, message)) + .build() + ); + } +} diff --git a/core/src/main/java/mc/core/embedded/ConfigFromSpring.java b/core/src/main/java/mc/core/embedded/ConfigFromSpring.java new file mode 100644 index 0000000..86680f8 --- /dev/null +++ b/core/src/main/java/mc/core/embedded/ConfigFromSpring.java @@ -0,0 +1,37 @@ +/* + * DmitriyMX + * 2018-04-08 + */ +package mc.core.embedded; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.Config; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.Base64; + +@Slf4j +@Getter +public class ConfigFromSpring implements Config { + @Setter + private String descriptionServer; + private byte[] faviconBase64; + @Setter + private int maxPlayers; + + public void setFaviconBase64(File faviconImageFile) { + log.debug("faviconImageFile: {}", faviconImageFile.getAbsolutePath()); + try { + faviconBase64 = Base64.getEncoder().encode( + FileUtils.readFileToByteArray(faviconImageFile) + ); + } catch (IOException e) { + log.warn("Can't load favicon", e); + faviconBase64 = null; + } + } +} diff --git a/core/src/main/java/mc/core/embedded/FakePlayerManager.java b/core/src/main/java/mc/core/embedded/FakePlayerManager.java new file mode 100644 index 0000000..a167573 --- /dev/null +++ b/core/src/main/java/mc/core/embedded/FakePlayerManager.java @@ -0,0 +1,83 @@ +package mc.core.embedded; + +import mc.core.EntityLocation; +import mc.core.chat.MessageType; +import mc.core.network.NetChannel; +import mc.core.network.SCPacket; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.text.Text; +import mc.core.text.Title; +import mc.core.world.World; + +import java.util.Collections; +import java.util.List; + +public class FakePlayerManager implements PlayerManager { + public static class FakeNetChannet implements NetChannel { + + @Override + public void sendTimeUpdate(long time, long age) { + } + + @Override + public void sendChatMessage(Text text, MessageType type) { + } + + @Override + public void sendTitle(Title title) { + } + + @Override + public void writeAndFlush(SCPacket pkt) { + } + + @Override + public void write(SCPacket pkt) { + } + + @Override + public void flush() { + } + } + + private static final NetChannel FAKE_NET_CHANNEL = new FakeNetChannet(); + + @Override + public Player createPlayer(String name, EntityLocation location, World world) { + return null; + } + + @Override + public void joinServer(Player player) { + } + + @Override + public void leftServer(Player player) { + } + + @Override + public Player getPlayer(String name) { + return null; + } + + @Override + public List getPlayers() { + return Collections.emptyList(); + } + + @Override + public int getCountPlayers() { + return 0; + } + + @Override + public NetChannel getBroadcastChannel() { + return FAKE_NET_CHANNEL; + } + + @Override + public Player getOfflinePlayer(String name) { + return null; + } +} diff --git a/core/src/main/java/mc/core/embedded/FakeServer.java b/core/src/main/java/mc/core/embedded/FakeServer.java new file mode 100644 index 0000000..99e296f --- /dev/null +++ b/core/src/main/java/mc/core/embedded/FakeServer.java @@ -0,0 +1,18 @@ +/* + * DmitriyMX + * 2018-06-29 + */ +package mc.core.embedded; + +import mc.core.network.Server; +import mc.core.network.StartServerException; + +public class FakeServer implements Server { + @Override + public void start() throws StartServerException { + } + + @Override + public void stop() { + } +} diff --git a/core/src/main/java/mc/core/eventbus/Event.java b/core/src/main/java/mc/core/eventbus/Event.java new file mode 100644 index 0000000..c31a008 --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/Event.java @@ -0,0 +1,4 @@ +package mc.core.eventbus; + +public interface Event { +} diff --git a/core/src/main/java/mc/core/eventbus/EventBus.java b/core/src/main/java/mc/core/eventbus/EventBus.java new file mode 100644 index 0000000..1a14331 --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/EventBus.java @@ -0,0 +1,89 @@ +package mc.core.eventbus; + +import javafx.util.Pair; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.helpers.MessageFormatter; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Stream; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EventBus { + @Getter + private static final EventBus instance = new EventBus(); + + private Queue eventQueue = new ConcurrentLinkedQueue<>(); + private Map, List>> subscribes = new HashMap<>(); + + private Stream getMethods(Object subscriberObject) { + return Stream.of(subscriberObject.getClass().getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(Subscriber.class)) + .filter(method -> method.getReturnType().equals(Void.TYPE)) + .filter(method -> method.getParameterCount() == 1) + .filter(method -> Event.class.isAssignableFrom(method.getParameterTypes()[0])); + } + + @SuppressWarnings("unchecked") + public void registerSubscribes(Object subscriberObject) { + getMethods(subscriberObject) + .forEach(method -> { + final Class type = (Class) method.getParameterTypes()[0]; + final List> pairs; + if (subscribes.containsKey(type)) { + pairs = subscribes.get(type); + } else { + pairs = new ArrayList<>(); + subscribes.put(type, pairs); + } + pairs.add(new Pair<>(subscriberObject, method)); + }); + } + + @SuppressWarnings("unchecked") + public void unregisterSubscribes(Object subscriberObject) { + getMethods(subscriberObject) + .forEach(method -> { + final Class type = (Class) method.getParameterTypes()[0]; + if (subscribes.containsKey(type)) { + final List> pairs = subscribes.get(type); + pairs.removeIf(pair -> pair.getKey() == subscriberObject); + + if (pairs.isEmpty()) { + subscribes.remove(type); + } + } + }); + } + + public void post(Event event) { + eventQueue.add(event); + } + + public void process() { + Event event; + while ((event = eventQueue.poll()) != null) { + final Class type = event.getClass(); + if (subscribes.containsKey(type)) { + final List> pairs = subscribes.get(type); + for (Pair pair : pairs) { + try { + pair.getValue().invoke(pair.getKey(), event); + } catch (IllegalAccessException | InvocationTargetException e) { + log.error(MessageFormatter.format("Invoke method '{}#{}'", + pair.getKey().getClass().getSimpleName(), + pair.getValue().getName()).getMessage(), + e + ); + } + } + } + } + } +} diff --git a/core/src/main/java/mc/core/eventbus/Subscriber.java b/core/src/main/java/mc/core/eventbus/Subscriber.java new file mode 100644 index 0000000..9a8aaee --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/Subscriber.java @@ -0,0 +1,11 @@ +package mc.core.eventbus; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(value= ElementType.METHOD) +@Retention(value= RetentionPolicy.RUNTIME) +public @interface Subscriber { +} diff --git a/core/src/main/java/mc/core/eventbus/events/CS_PlayerMoveEvent.java b/core/src/main/java/mc/core/eventbus/events/CS_PlayerMoveEvent.java new file mode 100644 index 0000000..1d9b6ae --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/CS_PlayerMoveEvent.java @@ -0,0 +1,23 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.Setter; +import mc.core.EntityLocation; +import mc.core.ImmutableEntityLocation; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +@Getter +public class CS_PlayerMoveEvent implements Event { + private final Player player; + private final ImmutableEntityLocation oldLocation; + @Setter + private EntityLocation newLocation; + @Setter + private boolean recalcChunk = false; + + public CS_PlayerMoveEvent(Player player, EntityLocation oldLocation) { + this.player = player; + this.oldLocation = new ImmutableEntityLocation(oldLocation); + } +} diff --git a/core/src/main/java/mc/core/eventbus/events/SC_ChunkLoadEvent.java b/core/src/main/java/mc/core/eventbus/events/SC_ChunkLoadEvent.java new file mode 100644 index 0000000..15148ad --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/SC_ChunkLoadEvent.java @@ -0,0 +1,17 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class SC_ChunkLoadEvent implements Event { + @Getter + private final Player player; + @Getter + private List needLoadChunks = new ArrayList<>(); +} diff --git a/core/src/main/java/mc/core/eventbus/events/SC_ChunkUnloadEvent.java b/core/src/main/java/mc/core/eventbus/events/SC_ChunkUnloadEvent.java new file mode 100644 index 0000000..d1a9517 --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/SC_ChunkUnloadEvent.java @@ -0,0 +1,17 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class SC_ChunkUnloadEvent implements Event { + @Getter + private final Player player; + @Getter + private List needUnloadChunks = new ArrayList<>(); +} diff --git a/core/src/main/java/mc/core/eventbus/events/SC_PlayerMoveEvent.java b/core/src/main/java/mc/core/eventbus/events/SC_PlayerMoveEvent.java new file mode 100644 index 0000000..7f479ee --- /dev/null +++ b/core/src/main/java/mc/core/eventbus/events/SC_PlayerMoveEvent.java @@ -0,0 +1,16 @@ +package mc.core.eventbus.events; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import mc.core.EntityLocation; +import mc.core.eventbus.Event; +import mc.core.player.Player; + +@RequiredArgsConstructor +@Getter +public class SC_PlayerMoveEvent implements Event { + private final Player player; + @Setter + private EntityLocation newLocation; +} diff --git a/core/src/main/java/mc/core/exception/ResourceUnloadedException.java b/core/src/main/java/mc/core/exception/ResourceUnloadedException.java new file mode 100644 index 0000000..eede649 --- /dev/null +++ b/core/src/main/java/mc/core/exception/ResourceUnloadedException.java @@ -0,0 +1,7 @@ +package mc.core.exception; + +public class ResourceUnloadedException extends RuntimeException { + public ResourceUnloadedException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/mc/core/network/BroadcastNetChannel.java b/core/src/main/java/mc/core/network/BroadcastNetChannel.java new file mode 100644 index 0000000..ed15fab --- /dev/null +++ b/core/src/main/java/mc/core/network/BroadcastNetChannel.java @@ -0,0 +1,48 @@ +/* + * DmitriyMX + * 2018-04-21 + */ +package mc.core.network; + +import lombok.RequiredArgsConstructor; +import mc.core.chat.MessageType; +import mc.core.player.Player; +import mc.core.text.Text; +import mc.core.text.Title; + +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class BroadcastNetChannel implements NetChannel { + private final Stream playerStream; + + @Override + public void sendTimeUpdate(final long time, final long age) { + playerStream.forEach(player -> player.getChannel().sendTimeUpdate(time, age)); + } + + @Override + public void sendChatMessage(final Text text, final MessageType type) { + playerStream.forEach(player -> player.getChannel().sendChatMessage(text, type)); + } + + @Override + public void sendTitle(final Title title) { + playerStream.forEach(player -> player.getChannel().sendTitle(title)); + } + + @Override + public void writeAndFlush(final SCPacket pkt) { + playerStream.forEach(player -> player.getChannel().writeAndFlush(pkt)); + } + + @Override + public void write(SCPacket pkt) { + playerStream.forEach(player -> player.getChannel().write(pkt)); + } + + @Override + public void flush() { + playerStream.forEach(player -> player.getChannel().flush()); + } +} diff --git a/core/src/main/java/mc/core/network/CSPacket.java b/core/src/main/java/mc/core/network/CSPacket.java new file mode 100644 index 0000000..4e8bb79 --- /dev/null +++ b/core/src/main/java/mc/core/network/CSPacket.java @@ -0,0 +1,12 @@ +/* + * DmitriyMX + * 2018-04-08 + */ +package mc.core.network; + +/** + * Пакеты Client->Server + */ +public interface CSPacket { + void readSelf(NetInputStream netStream); +} diff --git a/core/src/main/java/mc/core/network/NetChannel.java b/core/src/main/java/mc/core/network/NetChannel.java new file mode 100644 index 0000000..bfd6899 --- /dev/null +++ b/core/src/main/java/mc/core/network/NetChannel.java @@ -0,0 +1,22 @@ +/* + * DmitriyMX + * 2018-04-13 + */ +package mc.core.network; + +import mc.core.chat.MessageType; +import mc.core.text.Text; +import mc.core.text.Title; + +public interface NetChannel { + void sendTimeUpdate(long time, long age); + default void sendChatMessage(Text text) { + sendChatMessage(text, MessageType.CHAT_MESSAGE); + } + void sendChatMessage(Text text, MessageType type); + void sendTitle(Title title); + + void writeAndFlush(SCPacket pkt); + void write(SCPacket pkt); + void flush(); +} diff --git a/core/src/main/java/mc/core/network/NetInputStream.java b/core/src/main/java/mc/core/network/NetInputStream.java new file mode 100644 index 0000000..9262610 --- /dev/null +++ b/core/src/main/java/mc/core/network/NetInputStream.java @@ -0,0 +1,58 @@ +package mc.core.network; + +import com.flowpowered.nbt.Tag; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.io.InputStream; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class NetInputStream extends InputStream { + @Getter + @Setter + private int dataSize; + + public abstract boolean readBoolean(); + public abstract byte readByte(); + public int readBytes(byte[] buffer) { + return readBytes(buffer, 0, buffer.length); + } + public abstract int readBytes(byte[] buffer, int offset, int length); + public abstract int readUnsignedByte(); + public abstract int readUnsignedShort(); + public abstract short readShort(); + public abstract int readInt(); + public abstract int readVarInt(); + public abstract int readVarInt(AtomicInteger countReadBytes); + public abstract long readLong(); + public abstract float readFloat(); + public abstract double readDouble(); + public abstract String readString(); + public abstract UUID readUUID(); + public abstract Tag readNBT(); + + public abstract void skipBytes(int count); + + @Override + public int read() { + return readByte(); + } + + @Override + public int read(@NotNull byte[] b) { + return readBytes(b); + } + + @Override + public int read(@NotNull byte[] b, int off, int len) { + return readBytes(b, off, len); + } + + @Override + public long skip(long n) { + skipBytes((int) n); + return n; + } +} diff --git a/core/src/main/java/mc/core/network/NetOutputStream.java b/core/src/main/java/mc/core/network/NetOutputStream.java new file mode 100644 index 0000000..2f29ca5 --- /dev/null +++ b/core/src/main/java/mc/core/network/NetOutputStream.java @@ -0,0 +1,41 @@ +package mc.core.network; + +import com.flowpowered.nbt.Tag; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.UUID; + +public abstract class NetOutputStream extends OutputStream { + public abstract void writeBoolean(boolean value); + public abstract void writeByte(int value); + public abstract void writeUnsignedByte(int value); + public void writeBytes(byte[] buffer) { + writeBytes(buffer, 0, buffer.length); + } + public abstract void writeBytes(byte[] buffer, int offset, int lengtn); + public abstract void writeShort(int value); + public abstract void writeInt(int value); + public abstract void writeVarInt(int value); + public abstract void writeLong(long value); + public abstract void writeFloat(float value); + public abstract void writeDouble(double value); + public abstract void writeString(String value); + public abstract void writeUUID(UUID uuid); + public abstract void writeNBT(Tag tag); + + @Override + public void write(int b) throws IOException { + writeByte(b); + } + + @Override + public void write(byte[] b) throws IOException { + writeBytes(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + writeBytes(b, off, len); + } +} diff --git a/core/src/main/java/mc/core/network/SCPacket.java b/core/src/main/java/mc/core/network/SCPacket.java new file mode 100644 index 0000000..2265331 --- /dev/null +++ b/core/src/main/java/mc/core/network/SCPacket.java @@ -0,0 +1,12 @@ +/* + * DmitriyMX + * 2018-04-08 + */ +package mc.core.network; + +/** + * Пакеты Server->Client + */ +public interface SCPacket { + void writeSelf(NetOutputStream netStream); +} diff --git a/core/src/main/java/mc/core/network/Server.java b/core/src/main/java/mc/core/network/Server.java new file mode 100644 index 0000000..c419e65 --- /dev/null +++ b/core/src/main/java/mc/core/network/Server.java @@ -0,0 +1,10 @@ +/* + * DmitriyMX + * 2018-03-25 + */ +package mc.core.network; + +public interface Server { + void start() throws StartServerException; + void stop(); +} diff --git a/core/src/main/java/mc/core/network/StartServerException.java b/core/src/main/java/mc/core/network/StartServerException.java new file mode 100644 index 0000000..5657ee6 --- /dev/null +++ b/core/src/main/java/mc/core/network/StartServerException.java @@ -0,0 +1,11 @@ +/* + * DmitriyMX + * 2018-03-25 + */ +package mc.core.network; + +public class StartServerException extends Exception { + public StartServerException(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/mc/core/player/Player.java b/core/src/main/java/mc/core/player/Player.java new file mode 100644 index 0000000..eefe499 --- /dev/null +++ b/core/src/main/java/mc/core/player/Player.java @@ -0,0 +1,35 @@ +/* + * DmitriyMX + * 2018-04-13 + */ +package mc.core.player; + +import mc.core.EntityLocation; +import mc.core.network.NetChannel; +import mc.core.world.World; + +import java.util.List; +import java.util.UUID; + +public interface Player { + int getId(); + UUID getUuid(); + String getName(); + boolean isOnline(); + + /** Compacted list of Chunk coords (x,z) */ + List getLoadedChunks(); + + NetChannel getChannel(); + void setChannel(NetChannel channel); + + EntityLocation getLocation(); + World getWorld(); + void setWorld(World world); + + boolean isFlying(); + void setFlying(boolean value); + + PlayerSettings getSettings(); + void setSettings(PlayerSettings settings); +} diff --git a/core/src/main/java/mc/core/player/PlayerManager.java b/core/src/main/java/mc/core/player/PlayerManager.java new file mode 100644 index 0000000..4fe5485 --- /dev/null +++ b/core/src/main/java/mc/core/player/PlayerManager.java @@ -0,0 +1,20 @@ +package mc.core.player; + +import mc.core.EntityLocation; +import mc.core.network.NetChannel; +import mc.core.world.World; + +import java.util.List; + +public interface PlayerManager { + Player createPlayer(String name, EntityLocation location, World world); + void joinServer(Player player); + void leftServer(Player player); + + Player getPlayer(String name); + List getPlayers(); + int getCountPlayers(); + NetChannel getBroadcastChannel(); + + Player getOfflinePlayer(String name); +} diff --git a/core/src/main/java/mc/core/player/PlayerMode.java b/core/src/main/java/mc/core/player/PlayerMode.java new file mode 100644 index 0000000..cd6d1ed --- /dev/null +++ b/core/src/main/java/mc/core/player/PlayerMode.java @@ -0,0 +1,19 @@ +/* + * DmitriyMX + * 2018-05-06 + */ +package mc.core.player; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum PlayerMode { + SURVIVAL(0), + CREATIVE(1), + ADVENTURE(2), + SPECTATOR(3); + + private final int id; +} diff --git a/core/src/main/java/mc/core/player/PlayerSettings.java b/core/src/main/java/mc/core/player/PlayerSettings.java new file mode 100644 index 0000000..8a7c03c --- /dev/null +++ b/core/src/main/java/mc/core/player/PlayerSettings.java @@ -0,0 +1,56 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.player; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +public class PlayerSettings { + @RequiredArgsConstructor + public enum ChatMode { + ENABLED(0), + COMMANDS_ONLY(1), + HIDDEN(2); + + public static ChatMode getById(int id) { + if (id == 0) return ENABLED; + else if (id == 1) return COMMANDS_ONLY; + else return HIDDEN; + } + + @Getter + private final int id; + } + + @RequiredArgsConstructor + public enum Hand { + LEFT(0), + RIGHT(1); + + public static Hand getById(int id) { + if (id == 0) return LEFT; + else return RIGHT; + } + + @Getter + private final int id; + } + + private String locate = "en_US"; + private int viewDistance = 8; + private ChatMode chatMode = ChatMode.ENABLED; + private boolean chatColors = true; + private boolean capeEnabled = true, + jacketEnabled = true, + leftSleeveEnabled = true, + rightSleeveEnabled = true, + leftPantsLegEnabled = true, + rightPantsLegEnabled = true, + hatEnabled = true; + private Hand mainHand = Hand.RIGHT; +} diff --git a/core/src/main/java/mc/core/text/Text.java b/core/src/main/java/mc/core/text/Text.java new file mode 100644 index 0000000..4d470fe --- /dev/null +++ b/core/src/main/java/mc/core/text/Text.java @@ -0,0 +1,237 @@ +package mc.core.text; + +import com.google.common.collect.ImmutableList; +import lombok.Getter; + +import java.util.*; + +@Getter +public class Text { + private static final Text EMPTY = new Text(); + private static final Text NEW_LINE = new Text("\n", null, null, null); + + private final String content; + private final TextColor color; + private final TextStyle style; + private final ImmutableList children; + + private Text() { + content = ""; + color = null; + style = null; + children = null; + } + + private Text(String content, TextColor color, TextStyle style, ImmutableList children) { + this.content = content; + this.color = color; + this.style = style; + this.children = children; + } + + public boolean isEmpty() { + boolean result = (content == null || content.isEmpty()); + + if (children != null && !children.isEmpty()) { + for (Text child : children) { + result = result && child.isEmpty(); + } + } + + return result; + } + + public String toPlain() { + if (children != null && !children.isEmpty()) { + final StringJoiner sj = new StringJoiner(""); + children.forEach(child -> sj.add(child.toPlain())); + return sj.toString(); + } else { + return content; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Text text = (Text) o; + return Objects.equals(toPlain(), text.toPlain()); + } + + @Override + public int hashCode() { + return Objects.hash(toPlain()); + } + + public static class Builder { + @Getter + private String content; + @Getter + private TextColor color; + @Getter + private TextStyle style; + private List children; + + public Builder() { + this(""); + } + + public Builder(String content) { + this.content = content; + this.color = null; + this.style = null; + this.children = new ArrayList<>(); + } + + public Builder(Object... objects) { + this.children = new ArrayList<>(); + + for(Object obj : objects) { + if (obj instanceof String) { + if (this.content == null) { + this.content = (String) obj; + } else { + this.content = this.content.concat((String) obj); + } + } else if (obj instanceof TextStyle) { + if (this.style == null) { + this.style = TextStyle.none(); + } else { + this.style.merge((TextStyle) obj); + } + } else if (obj instanceof TextColor) { + this.color = (TextColor) obj; + } else if (obj instanceof Text) { + children.add((Text) obj); + } + } + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public Builder color(TextColor color) { + this.color = color; + return this; + } + + public Builder style(TextStyle style) { + if (this.style == null) { + this.style = TextStyle.none(); + } else { + this.style.merge(style); + } + + return this; + } + + public Builder style(TextStyle... styles) { + if (this.style == null) { + this.style = TextStyle.none(); + } + + for(TextStyle style : styles) { + this.style.merge(style); + } + + return this; + } + + public Builder append(String string) { + return append(Text.of(string)); + } + + public Builder append(Text child) { + if (child != null) { + this.children.add(child); + } + return this; + } + + public Builder append(Text... children) { + Collections.addAll(this.children, children); + return this; + } + + public Text build() { + if (children.isEmpty() && (content == null || content.isEmpty())) { + return Text.EMPTY; + } + + if (children.size() == 1 && children.get(0) != null) { + return children.get(0); + } else { + return new Text( + content, + color, + style, + ImmutableList.copyOf(children) + ); + } + } + } + + public static Builder builder() { + return new Builder(); + } + + public static Builder builder(String content) { + return new Builder(content); + } + + public static Builder builder(Object... objects) { + return new Builder(objects); + } + + public static Text of() { + return EMPTY; + } + + public static Text of(String string) { + if (string == null || string.isEmpty()) { + return EMPTY; + } else if (string.equals("\n")) { + return NEW_LINE; + } else { + return new Text(string, null, null, null); + } + } + + public static Text of(Object... objects) { + TextColor color = null; + TextStyle style = null; + String content = null; + + for(Object obj : objects) { + if (obj instanceof String) { + if (content == null) { + content = (String) obj; + } else { + content = content.concat((String) obj); + } + } else if (obj instanceof TextStyle) { + if (style == null) { + style = (TextStyle) obj; + } else { + style.merge((TextStyle) obj); + } + } else if (obj instanceof TextColor) { + color = (TextColor) obj; + } else if (obj != null){ + if (content == null) { + content = obj.toString(); + } else { + content = content.concat(obj.toString()); + } + } + } + + if (content == null || content.isEmpty()) { + return EMPTY; + } else { + return new Text(content, color, style, null); + } + } +} diff --git a/core/src/main/java/mc/core/text/TextColor.java b/core/src/main/java/mc/core/text/TextColor.java new file mode 100644 index 0000000..98c3214 --- /dev/null +++ b/core/src/main/java/mc/core/text/TextColor.java @@ -0,0 +1,32 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +package mc.core.text; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum TextColor { + BLACK ("black", '0'), + DARK_BLUE ("dark_blue", '1'), + DARK_GREEN ("dark_green", '2'), + DARK_AQUA ("dark_aqua", '3'), + DARK_RED ("dark_red", '4'), + DARK_PUEPLE("dark_purple", '5'), + GOLD ("gold", '6'), + GRAY ("gray", '7'), + DARK_GRAY ("dark_gray", '8'), + BLUE ("blue", '9'), + GREEN ("green", 'a'), + AQUA ("aqua", 'b'), + RED ("red", 'c'), + PUEPLE ("light_purple",'d'), + YELLOW ("yellow", 'e'), + WHITE ("white", 'f'); + + private final String name; + private final char code; +} diff --git a/core/src/main/java/mc/core/text/TextStyle.java b/core/src/main/java/mc/core/text/TextStyle.java new file mode 100644 index 0000000..f0afe98 --- /dev/null +++ b/core/src/main/java/mc/core/text/TextStyle.java @@ -0,0 +1,69 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +package mc.core.text; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; +import java.util.Optional; + +@Getter +@Setter +public class TextStyle { + public static final TextStyle BOLD = new TextStyle(true, null, null, null, null); + public static final TextStyle ITALIC = new TextStyle(null, true, null, null, null); + public static final TextStyle UNDERLINE = new TextStyle(null, null, true, null, null); + public static final TextStyle STRIKETHOUGH = new TextStyle(null, null, null, true, null); + public static final TextStyle OBFUSCATED = new TextStyle(null, null, null, null, true); + public static final TextStyle RESET = new TextStyle(false, false, false, false, false); + + private static class OptionalBoolean { + private static final Optional TRUE = Optional.of(true); + private static final Optional FALSE = Optional.of(false); + private static final Optional NONE = Optional.empty(); + + static Optional of(boolean bool) { + return bool ? TRUE : FALSE; + } + + static Optional of(@Nullable Boolean bool) { + if (bool != null) { + return of(bool.booleanValue()); + } + return NONE; + } + } + + private Optional bold; + private Optional italic; + private Optional underline; + private Optional strikethrough; + private Optional obfuscated; + + public TextStyle(@Nullable Boolean bold, + @Nullable Boolean italic, + @Nullable Boolean underline, + @Nullable Boolean strikethrough, + @Nullable Boolean obfuscated) { + this.bold = OptionalBoolean.of(bold); + this.italic = OptionalBoolean.of(italic); + this.underline = OptionalBoolean.of(underline); + this.strikethrough = OptionalBoolean.of(strikethrough); + this.obfuscated = OptionalBoolean.of(obfuscated); + } + + public void merge(TextStyle style) { + if (style.bold.isPresent()) this.bold = style.bold; + if (style.italic.isPresent()) this.italic = style.italic; + if (style.underline.isPresent()) this.underline = style.underline; + if (style.strikethrough.isPresent()) this.strikethrough = style.strikethrough; + if (style.obfuscated.isPresent()) this.obfuscated = style.obfuscated; + } + + public static TextStyle none() { + return new TextStyle(null,null,null,null, null); + } +} diff --git a/core/src/main/java/mc/core/text/TextTemplate.java b/core/src/main/java/mc/core/text/TextTemplate.java new file mode 100644 index 0000000..1e9600f --- /dev/null +++ b/core/src/main/java/mc/core/text/TextTemplate.java @@ -0,0 +1,150 @@ +/* + * DmitriyMX + * 2018-06-13 + */ +package mc.core.text; + +import com.google.common.collect.ImmutableList; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.*; + +public class TextTemplate { + private final ImmutableList elements; + + private TextTemplate(ImmutableList elements) { + this.elements = elements; + } + + public Text apply(Object... objects) { + Map variableMap = new HashMap<>((objects.length % 2) == 1 ? (objects.length / 2) + 1 : (objects.length / 2)); + + boolean skipValue = false; + String key = null; + for (Object obj : objects) { + if (skipValue) { + skipValue = false; + continue; + } + + if (key == null) { + if (obj == null || obj.toString().trim().isEmpty()) { + skipValue = true; + continue; + } + + key = obj.toString().trim(); + } else { + variableMap.put(key, obj); + key = null; + } + } + + if (key != null) { + variableMap.put(key, ""); + } + + return apply(variableMap); + } + + public Text apply(Map variables) { + Text.Builder textBuilder = Text.builder(); + + for(Object obj : elements) { + if (obj instanceof Text) { + textBuilder.append((Text) obj); + } else if (obj instanceof Arg) { + Arg arg = (Arg) obj; + if (variables.containsKey(arg.getKey())) { + Object valueObj = variables.get(arg.getKey()); + + if (valueObj instanceof Text) { + textBuilder.append((Text) valueObj); + } else { + textBuilder.append(Text.of(valueObj, arg.getColor(), arg.getStyle())); + } + } else { + textBuilder.append(Text.of(arg.getDefaultValue(), arg.getColor(), arg.getStyle())); + } + } + } + + return textBuilder.build(); + } + + @RequiredArgsConstructor + @Getter + public static class Arg { + private final String key; + private final String defaultValue; + @Setter + private TextColor color; + @Setter + private TextStyle style; + } + + public static class Builder { + private List elements = new ArrayList<>(); + + public Builder append(Text element) { + this.elements.add(element); + return this; + } + + public Builder append(Text... elements) { + Collections.addAll(this.elements, elements); + return this; + } + + public Builder arg(String name) { + this.elements.add(new Arg(name, null)); + return this; + } + + public Builder arg(String name, String defaultValue) { + this.elements.add(new Arg(name, defaultValue)); + return this; + } + + public Builder arg(Object... objects) { + String key = null, + defaultValue = null; + TextColor color = null; + TextStyle style = null; + + for(Object obj : objects) { + if (obj instanceof String) { + if (key == null) { + key = (String) obj; + } else { + defaultValue = (String) obj; + } + } else if (obj instanceof TextColor) { + color = (TextColor) obj; + } else if (obj instanceof TextStyle) { + if (style == null) { + style = TextStyle.none(); + } + style.merge((TextStyle) obj); + } + } + + Arg arg = new Arg(key, defaultValue); + arg.setColor(color); + arg.setStyle(style); + this.elements.add(arg); + + return this; + } + + public TextTemplate build() { + return new TextTemplate(ImmutableList.copyOf(elements)); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/core/src/main/java/mc/core/text/Title.java b/core/src/main/java/mc/core/text/Title.java new file mode 100644 index 0000000..7715b4c --- /dev/null +++ b/core/src/main/java/mc/core/text/Title.java @@ -0,0 +1,34 @@ +/* + * DmitriyMX + * 2018-06-24 + */ +package mc.core.text; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class Title { + private Text title = null; + private Text subtitle = null; + private Text textActionBar = null; + private Integer fadeInTime = null; + private Integer stayTime = null; + private Integer fadeOutTime = null; + private Boolean hide = null; + private Boolean reset = null; + + public void clear() { + this.title = null; + this.subtitle = null; + this.textActionBar = null; + this.fadeInTime = null; + this.stayTime = null; + this.fadeOutTime = null; + this.hide = null; + this.reset = null; + } +} diff --git a/core/src/main/java/mc/core/time/AbstractTimeProcessor.java b/core/src/main/java/mc/core/time/AbstractTimeProcessor.java new file mode 100644 index 0000000..cff6079 --- /dev/null +++ b/core/src/main/java/mc/core/time/AbstractTimeProcessor.java @@ -0,0 +1,14 @@ +/* + * DmitriyMX + * 2018-06-24 + */ +package mc.core.time; + +public abstract class AbstractTimeProcessor implements TimeProcessor { + private long worldAge = 0; + + @Override + public long getWorldAge() { + return worldAge++; + } +} diff --git a/core/src/main/java/mc/core/time/IdleTime.java b/core/src/main/java/mc/core/time/IdleTime.java new file mode 100644 index 0000000..b4be15d --- /dev/null +++ b/core/src/main/java/mc/core/time/IdleTime.java @@ -0,0 +1,14 @@ +/* + * DmitriyMX + * 2018-05-01 + */ +package mc.core.time; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +public class IdleTime extends AbstractTimeProcessor { + @Getter + private final long gameTime; +} diff --git a/core/src/main/java/mc/core/time/RealTime.java b/core/src/main/java/mc/core/time/RealTime.java new file mode 100644 index 0000000..5b5158f --- /dev/null +++ b/core/src/main/java/mc/core/time/RealTime.java @@ -0,0 +1,39 @@ +/* + * DmitriyMX + * 2018-05-01 + */ +package mc.core.time; + +import java.util.Calendar; + +public class RealTime extends AbstractTimeProcessor { + private static final long DIFF = 21600L; + private static final long HOUR24 = 86400L; + private final Calendar calendar = Calendar.getInstance(); + private long lastUpdate = 0; + private long gameTime; + + private void calcRealTime() { + lastUpdate = System.currentTimeMillis(); + + calendar.setTimeInMillis(lastUpdate); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + long time = (lastUpdate - calendar.getTimeInMillis())/1000; + if (time < DIFF) time += HOUR24; + + gameTime = (long) ((time - DIFF) / 3.6); + } + + @Override + public long getGameTime() { + if ((System.currentTimeMillis() - lastUpdate) > 1000) { + calcRealTime(); + } + + return gameTime; + } +} diff --git a/core/src/main/java/mc/core/time/TimePerTick.java b/core/src/main/java/mc/core/time/TimePerTick.java new file mode 100644 index 0000000..02dc756 --- /dev/null +++ b/core/src/main/java/mc/core/time/TimePerTick.java @@ -0,0 +1,21 @@ +/* + * DmitriyMX + * 2018-05-01 + */ +package mc.core.time; + +public class TimePerTick extends AbstractTimeProcessor { + private long gameTime; + + public void setStartGameTime(long value) { + gameTime = value; + } + + @Override + public long getGameTime() { + gameTime++; + if (gameTime > 24000) gameTime = 0; + + return gameTime; + } +} diff --git a/core/src/main/java/mc/core/time/TimeProcessor.java b/core/src/main/java/mc/core/time/TimeProcessor.java new file mode 100644 index 0000000..93f5547 --- /dev/null +++ b/core/src/main/java/mc/core/time/TimeProcessor.java @@ -0,0 +1,10 @@ +/* + * DmitriyMX + * 2018-05-01 + */ +package mc.core.time; + +public interface TimeProcessor { + long getGameTime(); + long getWorldAge(); +} diff --git a/core/src/main/java/mc/core/utils/CompactedCoords.java b/core/src/main/java/mc/core/utils/CompactedCoords.java new file mode 100644 index 0000000..c05884c --- /dev/null +++ b/core/src/main/java/mc/core/utils/CompactedCoords.java @@ -0,0 +1,22 @@ +package mc.core.utils; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CompactedCoords { + public static int compressXZ(int x, int z) { + if (x < Short.MIN_VALUE || x > Short.MAX_VALUE || + z < Short.MIN_VALUE || z > Short.MAX_VALUE) { + log.warn("Coord over range: [{},{}]", x, z); + } + + return ((x & 0xFFFF) << 16) | (z & 0xFFFF); + } + + public static int[] uncompressXZ(int compactValue) { + return new int[]{ + compactValue >> 16, + (compactValue & 0x8000) > 0 ? compactValue | 0xFFFF0000 : compactValue & 0xFFFF + }; + } +} diff --git a/core/src/main/java/mc/core/utils/NibbleArray.java b/core/src/main/java/mc/core/utils/NibbleArray.java new file mode 100644 index 0000000..e8514f7 --- /dev/null +++ b/core/src/main/java/mc/core/utils/NibbleArray.java @@ -0,0 +1,65 @@ +package mc.core.utils; + +import lombok.RequiredArgsConstructor; +import mc.core.world.block.BlockLocation; + +/** + * Сжатый массив значений 0-15 + */ +@RequiredArgsConstructor +public class NibbleArray { + private final byte[] data; + + public NibbleArray(int capacity) { + this.data = new byte[capacity]; + } + + public NibbleArray() { + this.data = new byte[2048]; + } + + private int coordsToIndex(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + private int nibbleIndex(int index) { + return index >> 1; + } + + private boolean isLowerNibble(int index) { + return (index & 1) == 0; + } + + public int get(BlockLocation location) { + return get(location.getX(), location.getY(), location.getZ()); + } + + public int get(int x, int y, int z) { + final int idx = coordsToIndex(x, y, z); + + final int ni = nibbleIndex(idx); + 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; + + final int idx = coordsToIndex(x, y, z); + final int ni = nibbleIndex(idx); + + if (isLowerNibble(idx)) { + this.data[ni] = (byte)(value); + } else { + this.data[ni] = (byte)(this.data[ni] | value << 4); + } + } + + public byte[] getRawData() { + return data; + } +} diff --git a/core/src/main/java/mc/core/world/Biome.java b/core/src/main/java/mc/core/world/Biome.java new file mode 100644 index 0000000..3f4d555 --- /dev/null +++ b/core/src/main/java/mc/core/world/Biome.java @@ -0,0 +1,80 @@ +package mc.core.world; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; + +@RequiredArgsConstructor +public enum Biome { + OCEAN(0), + PLAINS(1), + DESERT(2), + EXTREME_HILLS(3), + FOREST(4), + TAIGA(5), + SWAMPLAND(6), + RIVER(7), + HELL(8), + SKY(9), + FROZEN_OCEAN(10), + FROZEN_RIVER(11), + ICE_PLAINS(12), + ICE_MOUNTAINS(13), + MUSHROOM_ISLAND(14), + MUSHROOM_ISLAND_SHORE(15), + BEACH(16), + DESERT_HILLS(17), + FOREST_HILLS(18), + TAIGA_HILLS(19), + EXTREME_HILLS_EDGE(20), + JUNGLE(21), + JUNGLE_HILLS(22), + JUNGLE_EDGE(23), + DEEP_OCEAN(24), + STONE_BEACH(25), + COLD_BEACH(26), + BIRCH_FOREST(27), + BIRCH_FOREST_HILLS(28), + ROOFED_FOREST(29), + TAIGA_COLD(30), + TAIGA_COLD_HILLS(31), + REDWOOD_TAIGA(32), + REDWOOD_TAIGA_HILLS(33), + EXTREME_HILLS_WITH_TREES(34), + SAVANNA(35), + SAVANNA_ROCK(36), + MESA(37), + MESA_ROCK(38), + MESA_CLEAR_ROCK(39), + VOID(127), + MUTATED_PLAINS(129), + MUTATED_DESERT(130), + MUTATED_EXTREME_HILLS(131), + MUTATED_FOREST(132), + MUTATED_TAIGA(133), + MUTATED_SWAMPLAND(134), + MUTATED_ICE_FLATS(140), + MUTATED_JUNGLE(149), + MUTATED_JUNGLE_EDGE(151), + MUTATED_BIRCH_FOREST(155), + MUTATED_BIRCH_FOREST_HILLS(156), + MUTATED_ROOFED_FOREST(157), + MUTATED_TAIGA_COLD(158), + MUTATED_REDWOOD_TAIGA(160), + MUTATED_REDWOOD_TAIGA_HILLS(161), + MUTATED_EXTREME_HILLS_WITH_TREES(162), + MUTATED_SAVANNA(163), + MUTATED_SAVANNA_ROCK(164), + MUTATED_MESA(165), + MUTATED_MESA_ROCK(166), + MUTATED_MESA_CLEAR_ROCK(167); + + public static Biome getById(final int id) { + return Arrays.stream(Biome.values()).filter(biome -> biome.id == id).findFirst().orElse(Biome.PLAINS); + } + + @Getter + private final int id; +} diff --git a/core/src/main/java/mc/core/world/World.java b/core/src/main/java/mc/core/world/World.java new file mode 100644 index 0000000..dc76256 --- /dev/null +++ b/core/src/main/java/mc/core/world/World.java @@ -0,0 +1,63 @@ +package mc.core.world; + +import mc.core.EntityLocation; +import mc.core.world.block.Block; +import mc.core.world.block.BlockLocation; +import mc.core.world.chunk.Chunk; + +public interface World { + String getName(); + WorldType getType(); + + EntityLocation getSpawn(); + + void setSpawn(EntityLocation location); + + default void setSpawn(double x, double y, double z, float yaw, float pitch) { + setSpawn(new EntityLocation(x, y, z, yaw, pitch)); + } + + default void setSpawn(double x, double y, double z) { + setSpawn(x, y, z, 0f, 0f); + } + + /** + * Получить чанк по его координатам + * @param x chunk X + * @param z chunk Z + * @return {@link mc.core.world.chunk.Chunk} + */ + Chunk getChunk(int x, int z); + + /** + * Получить чанк по глобальным координатам блока + * @param location {@link BlockLocation} + * @return {@link Chunk} + */ + default Chunk getChunk(BlockLocation location) { + return getChunk(location.getX() >> 4, location.getZ() >> 4); + } + + /** + * Установить чанк по координатам + * @param x глобальный X + * @param z глобальный Z + * @param chunk {@link mc.core.world.chunk.Chunk} + */ + void setChunk(int x, int z, Chunk chunk); + + /** + * Получить блок по его координатам + * @param x X + * @param y Y + * @param z Z + * @return {@link Block} + */ + Block getBlock(int x, int y, int z); + + default Block getBlock(BlockLocation location) { + return getBlock(location.getX(), location.getY(), location.getZ()); + } + + void setBlock(Block block); +} diff --git a/core/src/main/java/mc/core/world/WorldType.java b/core/src/main/java/mc/core/world/WorldType.java new file mode 100644 index 0000000..86f2680 --- /dev/null +++ b/core/src/main/java/mc/core/world/WorldType.java @@ -0,0 +1,13 @@ +package mc.core.world; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum WorldType { + DEFAULT("default"), + FLAT("flat"); + + private final String name; +} diff --git a/core/src/main/java/mc/core/world/block/AbstractBlock.java b/core/src/main/java/mc/core/world/block/AbstractBlock.java new file mode 100644 index 0000000..ab57849 --- /dev/null +++ b/core/src/main/java/mc/core/world/block/AbstractBlock.java @@ -0,0 +1,23 @@ +package mc.core.world.block; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public abstract class AbstractBlock implements Block { + @Setter + private BlockLocation location; + private int light = 0; + private final BlockType type; + + protected AbstractBlock(BlockType type) { + this.type = type; + } + + @Override + public void setLight(int light) { + if (light > 15) this.light = 15; + else if (light < 0) this.light = 0; + else this.light = light; + } +} diff --git a/core/src/main/java/mc/core/world/block/Block.java b/core/src/main/java/mc/core/world/block/Block.java new file mode 100644 index 0000000..1774a61 --- /dev/null +++ b/core/src/main/java/mc/core/world/block/Block.java @@ -0,0 +1,17 @@ +package mc.core.world.block; + +import com.flowpowered.nbt.CompoundTag; + +public interface Block { + int getLight(); + void setLight(int light); + BlockType getType(); + BlockLocation getLocation(); + + default CompoundTag getNBTData() { + return null; + } + + default void setNBTData(CompoundTag nbtData) { + } +} diff --git a/core/src/main/java/mc/core/world/block/BlockFactory.java b/core/src/main/java/mc/core/world/block/BlockFactory.java new file mode 100644 index 0000000..2f2410a --- /dev/null +++ b/core/src/main/java/mc/core/world/block/BlockFactory.java @@ -0,0 +1,18 @@ +package mc.core.world.block; + +//TODO избавится от этого "аппендикса" +@Deprecated +public class BlockFactory { + + public Block create(BlockType blockType, int x, int y, int z) { + return new EmbeddedBlock(blockType, x, y, z); + } + + /** For first-time generation */ + private class EmbeddedBlock extends AbstractBlock { + EmbeddedBlock(BlockType type, int x, int y, int z) { + super(type); + setLocation(new BlockLocation(x, y, z)); + } + } +} diff --git a/core/src/main/java/mc/core/world/block/BlockLocation.java b/core/src/main/java/mc/core/world/block/BlockLocation.java new file mode 100644 index 0000000..ff6fd5f --- /dev/null +++ b/core/src/main/java/mc/core/world/block/BlockLocation.java @@ -0,0 +1,32 @@ +package mc.core.world.block; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class BlockLocation implements Cloneable { + private int x, y, z; + + public static BlockLocation ZERO() { + return new BlockLocation(0,0,0); + } + + public void setXYZ(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public BlockLocation clone() { + try { + return (BlockLocation) super.clone(); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + return ZERO(); + } + } +} diff --git a/core/src/main/java/mc/core/world/block/BlockType.java b/core/src/main/java/mc/core/world/block/BlockType.java new file mode 100644 index 0000000..13eb1e6 --- /dev/null +++ b/core/src/main/java/mc/core/world/block/BlockType.java @@ -0,0 +1,515 @@ +package mc.core.world.block; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.stream.Stream; + +@Slf4j +@RequiredArgsConstructor +public enum BlockType { + + AIR(0, 0), + + STONE (1, 0), + STONE_MOSS(48, 0), + + GRANITE (1, 1), + POLISHED_GRANITE(1, 2), + + DIORITE(1, 3), + ANDESITE(1, 5), + + GRASS(2, 0), + PATH(208, 0), + + DIRT(3, 0), + + /** Farmland, Dry, Moisture 0 */ + FARMLAND (60, 0), + /** Farmland, Dry, Moisture 1 */ + FARMLAND_1(60, 1), + /** Farmland, Dry, Moisture 2 */ + FARMLAND_2(60, 2), + /** Farmland, Dry, Moisture 3 */ + FARMLAND_3(60, 3), + /** Farmland, Dry, Moisture 4 */ + FARMLAND_4(60, 4), + /** Farmland, Dry, Moisture 5 */ + FARMLAND_5(60, 5), + /** Farmland, Dry, Moisture 6 */ + FARMLAND_6(60, 6), + /** Farmland, Dry, Moisture 7 */ + FARMLAND_7(60, 7), + + COBBLESTONE(4, 0), + BEDROCK(7, 0), + + /** Water, flowing, Level 7 (Source) */ + WATER_FLOWING (8, 0), + /** Water, flowing, Level 6 */ + WATER_FLOWING_1 (8, 1), + /** Water, flowing, Level 5 */ + WATER_FLOWING_2 (8, 2), + /** Water, flowing, Level 4 */ + WATER_FLOWING_3 (8, 3), + /** Water, flowing, Level 3 */ + WATER_FLOWING_4 (8, 4), + /** Water, flowing, Level 2 */ + WATER_FLOWING_5 (8, 5), + /** Water, flowing, Level 1 */ + WATER_FLOWING_6 (8, 6), + /** Water, flowing, Level 0 */ + WATER_FLOWING_7 (8, 7), + /** Water, flowing, Level 15 */ + WATER_FLOWING_8 (8, 8), + /** Water, flowing, Level 14 */ + WATER_FLOWING_9 (8, 9), + /** Water, flowing, Level 13 */ + WATER_FLOWING_10(8, 10), + /** Water, flowing, Level 12 */ + WATER_FLOWING_11(8, 11), + /** Water, flowing, Level 11 */ + WATER_FLOWING_12(8, 12), + /** Water, flowing, Level 10 */ + WATER_FLOWING_13(8, 13), + /** Water, flowing, Level 9 */ + WATER_FLOWING_14(8, 14), + /** Water, flowing, Level 8 */ + WATER_FLOWING_15(8, 15), + + /** Water, still, Level 7 (Source) */ + WATER_STILL (9, 0), + /** Water, still, Level 6 */ + WATER_STILL_1 (9, 1), + /** Water, still, Level 5 */ + WATER_STILL_2 (9, 2), + /** Water, still, Level 4 */ + WATER_STILL_3 (9, 3), + /** Water, still, Level 3 */ + WATER_STILL_4 (9, 4), + /** Water, still, Level 2 */ + WATER_STILL_5 (9, 5), + /** Water, still, Level 1 */ + WATER_STILL_6 (9, 6), + /** Water, still, Level 0 */ + WATER_STILL_7 (9, 7), + /** Water, still, Level 15 */ + WATER_STILL_8 (9, 8), + /** Water, still, Level 14 */ + WATER_STILL_9 (9, 9), + /** Water, still, Level 13 */ + WATER_STILL_10(9, 10), + /** Water, still, Level 12 */ + WATER_STILL_11(9, 11), + /** Water, still, Level 11 */ + WATER_STILL_12(9, 12), + /** Water, still, Level 10 */ + WATER_STILL_13(9, 13), + /** Water, still, Level 9 */ + WATER_STILL_14(9, 14), + /** Water, still, Level 8 */ + WATER_STILL_15(9, 15), + + /** Lava, flowing, Level 7 (Source) */ + LAVA_FLOWING (10, 0), + /** Lava, flowing, Level 6 */ + LAVA_FLOWING_1 (10, 1), + /** Lava, flowing, Level 5 */ + LAVA_FLOWING_2 (10, 2), + /** Lava, flowing, Level 4 */ + LAVA_FLOWING_3 (10, 3), + /** Lava, flowing, Level 3 */ + LAVA_FLOWING_4 (10, 4), + /** Lava, flowing, Level 2 */ + LAVA_FLOWING_5 (10, 5), + /** Lava, flowing, Level 1 */ + LAVA_FLOWING_6 (10, 6), + /** Lava, flowing, Level 0 */ + LAVA_FLOWING_7 (10, 7), + /** Lava, flowing, Level 15 */ + LAVA_FLOWING_8 (10, 8), + /** Lava, flowing, Level 14 */ + LAVA_FLOWING_9 (10, 9), + /** Lava, flowing, Level 13 */ + LAVA_FLOWING_10(10, 10), + /** Lava, flowing, Level 12 */ + LAVA_FLOWING_11(10, 11), + /** Lava, flowing, Level 11 */ + LAVA_FLOWING_12(10, 12), + /** Lava, flowing, Level 10 */ + LAVA_FLOWING_13(10, 13), + /** Lava, flowing, Level 9 */ + LAVA_FLOWING_14(10, 14), + /** Lava, flowing, Level 8 */ + LAVA_FLOWING_15(10, 15), + + /** Lava, still, Level 7 (Source) */ + LAVA_STILL (11, 0), + /** Lava, still, Level 6 */ + LAVA_STILL_1 (11, 1), + /** Lava, still, Level 5 */ + LAVA_STILL_2 (11, 2), + /** Lava, still, Level 4 */ + LAVA_STILL_3 (11, 3), + /** Lava, still, Level 3 */ + LAVA_STILL_4 (11, 4), + /** Lava, still, Level 2 */ + LAVA_STILL_5 (11, 5), + /** Lava, still, Level 1 */ + LAVA_STILL_6 (11, 6), + /** Lava, still, Level 0 */ + LAVA_STILL_7 (11, 7), + /** Lava, still, Level 15 */ + LAVA_STILL_8 (11, 8), + /** Lava, still, Level 14 */ + LAVA_STILL_9 (11, 9), + /** Lava, still, Level 13 */ + LAVA_STILL_10(11, 10), + /** Lava, still, Level 12 */ + LAVA_STILL_11(11, 11), + /** Lava, still, Level 11 */ + LAVA_STILL_12(11, 12), + /** Lava, still, Level 10 */ + LAVA_STILL_13(11, 13), + /** Lava, still, Level 9 */ + LAVA_STILL_14(11, 14), + /** Lava, still, Level 8 */ + LAVA_STILL_15(11, 15), + + SAND (12, 0), + SANDSTONE(24, 0), + + GRAVEL(13, 0), + + ORE_GOLD (14, 0), + ORE_IRON (15, 0), + ORE_COAL (16, 0), + ORE_LAPIS (21, 0), + ORE_DIAMOND (56, 0), + ORE_REDSTONE (73, 0), + ORE_GLOWING_REDSTONE(74, 0), + ORE_EMERALD (129, 0), + + // Upright + WOOD_OAK (17, 0), + WOOD_SPRUCE (17, 1), + WOOD_BIRCH (17, 2), + WOOD_JUNGLE (17, 3), + WOOD_ACACIA (162, 0), + WOOD_OAK_DARK(162, 1), + + // East/West + WOOD_OAK_EW (17, 4), + WOOD_SPRUCE_EW (17, 5), + WOOD_BIRCH_EW (17, 6), + WOOD_JUNGLE_EW (17, 7), + WOOD_ACACIA_EW (162, 4), + WOOD_OAK_DARK_EW(162, 5), + + // North/South + WOOD_OAK_NS (17, 8), + WOOD_SPRUCE_NS (17, 9), + WOOD_BIRCH_NS (17, 10), + WOOD_JUNGLE_NS (17, 11), + WOOD_ACACIA_NS (162, 8), + WOOD_OAK_DARK_NS(162, 9), + + PLANK_WOOD_OAK (5, 0), + PLANK_WOOD_SPRUCE (5, 1), + PLANK_WOOD_BIRCH (5, 2), + PLANK_WOOD_JUNGLE (5, 3), + PLANK_WOOD_ACACIA (5, 4), + PLANK_WOOD_OAK_DARK(5, 5), + + DOOR_LOW_OAK_EAST(64, 0), + DOOR_LOW_OAK_SOUTH(64, 1), + DOOR_LOW_OAK_WEST(64, 2), + DOOR_LOW_OAK_NORTH(64, 3), + DOOR_LOW_OAK_EAST_OPENED(64, 4), + DOOR_LOW_OAK_SOUTH_OPENED(64, 5), + DOOR_LOW_OAK_WEST_OPENED(64, 6), + DOOR_LOW_OAK_NORTH_OPENED(64, 7), + + DOOR_UP_OAK_LEFT(64, 8), + DOOR_UP_OAK_RIGHT(64, 9), + DOOR_UP_OAK_LEFT_POWERED(64, 10), + DOOR_UP_OAK_RIGHT_POWERED(64, 11), + DOOR_UP_OAK_12(64, 12), + DOOR_UP_OAK_13(64, 13), + DOOR_UP_OAK_14(64, 14), + DOOR_UP_OAK_15(64, 15), + + FENCE_OAK(85, 0), + + // Decay after Tree Update + LEAVES_OAK (18, 0), + LEAVES_SPRUCE (18, 1), + LEAVES_BIRCH (18, 2), + LEAVES_JUNGLE (18, 3), + LEAVES_ACACIA (161, 0), + LEAVES_OAK_DARK(161, 1), + + // No Decay + LEAVES_OAK2 (18, 4), + LEAVES_SPRUCE2 (18, 5), + LEAVES_BIRCH2 (18, 6), + LEAVES_JUNGLE2 (18, 7), + LEAVES_ACACIA2 (161, 4), + LEAVES_OAK_DARK2(161, 5), + + // Decay + LEAVES_OAK3 (18, 8), + LEAVES_SPRUCE3 (18, 9), + LEAVES_BIRCH3 (18, 10), + LEAVES_JUNGLE3 (18, 11), + LEAVES_ACACIA3 (161, 8), + LEAVES_OAK_DARK3(161, 9), + + // No decay, unused + @Deprecated + LEAVES_OAK4 (18, 12), + @Deprecated + LEAVES_SPRUCE4 (18, 13), + @Deprecated + LEAVES_BIRCH4 (18, 14), + @Deprecated + LEAVES_JUNGLE4 (18, 15), + @Deprecated + LEAVES_ACACIA4 (161, 12), + @Deprecated + LEAVES_OAK_DARK4(161, 13), + + COBWEB(30, 0), + TALLGRASS(31, 1), + DANDELION(37, 0), + + FLOWER_POPPY (38, 0), + FLOWER_BLUE_ORCHID (38, 1), + FLOWER_ALLIUM (38, 2), + FLOWER_AZURE_BLUET (38, 3), + FLOWER_TULIP_RED (38, 4), + FLOWER_TULIP_ORANGE(38, 5), + FLOWER_TULIP_WHITE (38, 6), + FLOWER_TULIP_PINK (38, 7), + FLOWER_OXEYE_DAISY (38, 8), + + MUSHROOM_BROWN(39, 0), + MUSHROOM_RED (40, 0), + + MUSHROOM_BLOCK_BROWN_ALL_INSIDE(99, 0), + MUSHROOM_BLOCK_BROWN_NW (99, 1), + MUSHROOM_BLOCK_BROWN_NORT (99, 2), + MUSHROOM_BLOCK_BROWN_NE (99, 3), + MUSHROOM_BLOCK_BROWN_WEST (99, 4), + MUSHROOM_BLOCK_BROWN_CENTER (99, 5), + MUSHROOM_BLOCK_BROWN_EAST (99, 6), + MUSHROOM_BLOCK_BROWN_SW (99, 7), + MUSHROOM_BLOCK_BROWN_SOUTH (99, 8), + MUSHROOM_BLOCK_BROWN_SE (99, 9), + MUSHROOM_BLOCK_BROWN_STEM (99, 10), + MUSHROOM_BLOCK_BROWN_ALL_OUSIDE(99, 14), + MUSHROOM_BLOCK_BROWN_ALL_STEM (99, 15), + + MUSHROOM_BLOCK_RED_ALL_INSIDE(100, 0), + MUSHROOM_BLOCK_RED_NW (100, 1), + MUSHROOM_BLOCK_RED_NORT (100, 2), + MUSHROOM_BLOCK_RED_NE (100, 3), + MUSHROOM_BLOCK_RED_WEST (100, 4), + MUSHROOM_BLOCK_RED_CENTER (100, 5), + MUSHROOM_BLOCK_RED_EAST (100, 6), + MUSHROOM_BLOCK_RED_SW (100, 7), + MUSHROOM_BLOCK_RED_SOUTH (100, 8), + MUSHROOM_BLOCK_RED_SE (100, 9), + MUSHROOM_BLOCK_RED_STEM (100, 10), + MUSHROOM_BLOCK_RED_ALL_OUSIDE(100, 14), + MUSHROOM_BLOCK_RED_ALL_STEM (100, 15), + + OBSIDIAN(49, 0), + + TORCH_EAST (50, 1), + TORCH_WEST (50, 2), + TORCH_SOUTH(50, 3), + TORCH_NORTH(50, 4), + TORCH_UP (50, 5), + + MONSTER_SPAWNER(52, 0), + + CHEST_NORTH(54, 2, "minecraft:chest"), + CHEST_SOUTH(54, 3, "minecraft:chest"), + CHEST_WEST (54, 4, "minecraft:chest"), + CHEST_EAST (54, 5, "minecraft:chest"), + + RAIL_NS (66, 0), + RAIL_EW (66, 1), + RAIL_ASCENDING_EAST (66, 2), + RAIL_ASCENDING_WEST (66, 3), + RAIL_ASCENDING_NORTH(66, 4), + RAIL_ASCENDING_SOUTH(66, 5), + RAIL_CURVED_SE (66, 6), + RAIL_CURVED_SW (66, 7), + RAIL_CURVED_NW (66, 8), + RAIL_CURVED_NE (66, 9), + + SNOW(78, 0), + + CLAY(82, 0), + CLAY_HARDENED(172, 0), + + /** Sugar canes (Age 0) */ + SUGAR_CANES(83, 0), + /** Sugar canes (Age 1) */ + SUGAR_CANES_1(83, 1), + /** Sugar canes (Age 2) */ + SUGAR_CANES_2(83, 2), + /** Sugar canes (Age 3) */ + SUGAR_CANES_3(83, 3), + /** Sugar canes (Age 4) */ + SUGAR_CANES_4(83, 4), + /** Sugar canes (Age 5) */ + SUGAR_CANES_5(83, 5), + /** Sugar canes (Age 6) */ + SUGAR_CANES_6(83, 6), + /** Sugar canes (Age 7) */ + SUGAR_CANES_7(83, 7), + /** Sugar canes (Age 8) */ + SUGAR_CANES_8(83, 8), + /** Sugar canes (Age 9) */ + SUGAR_CANES_9(83, 9), + /** Sugar canes (Age 10) */ + SUGAR_CANES_10(83, 10), + /** Sugar canes (Age 11) */ + SUGAR_CANES_11(83, 11), + /** Sugar canes (Age 12) */ + SUGAR_CANES_12(83, 12), + /** Sugar canes (Age 13) */ + SUGAR_CANES_13(83, 13), + /** Sugar canes (Age 14) */ + SUGAR_CANES_14(83, 14), + /** Sugar canes (Age 15) */ + SUGAR_CANES_15(83, 15), + + PUMPKIN_SOUTH(86, 0), + PUMPKIN_WEST (86, 1), + PUMPKIN_NORTH(86, 2), + PUMPKIN_EAST (86, 3), + + STONE_MONSTER_EGG(97, 0), + + GLASS_PANE(102, 0), + + VINE (106, 0), + VINE_SOUTH(106, 1), + VINE_WEST (106, 2), + VINE_SW (106, 3), + VINE_NORTH(106, 4), + VINE_NS (106, 5), + VINE_NW (106, 6), + VINE_NSW (106, 7), // North, South, West + VINE_EAST (106, 8), + VINE_ES (106, 9), + VINE_EW (106, 10), + VINE_ESW (106, 11), + VINE_EN (106, 12), + VINE_ENS (106, 13), + VINE_ENW (106, 14), + VINE_ENSW (106, 14), + + WATERLILY(111, 0), + + LILAC(175, 1), + DOUBLE_TALLGRASS(175, 2), + ROSE_BUSH(175, 4), + PEONY(175, 5), + ROSE_BUSH_10(175, 10), + + /** Wheat (Age 0) */ + WHEAT (59, 0), + /** Wheat (Age 1) */ + WHEAT_1(59, 1), + /** Wheat (Age 2) */ + WHEAT_2(59, 2), + /** Wheat (Age 3) */ + WHEAT_3(59, 3), + /** Wheat (Age 4) */ + WHEAT_4(59, 4), + /** Wheat (Age 5) */ + WHEAT_5(59, 5), + /** Wheat (Age 6) */ + WHEAT_6(59, 6), + /** Wheat (Age 7) */ + WHEAT_7(59, 7), + + /** Carrots (Age 0) */ + CARROTS(141, 0), + /** Carrots (Age 1) */ + CARROTS_1(141, 1), + /** Carrots (Age 2) */ + CARROTS_2(141, 2), + /** Carrots (Age 3) */ + CARROTS_3(141, 3), + /** Carrots (Age 4) */ + CARROTS_4(141, 4), + /** Carrots (Age 5) */ + CARROTS_5(141, 5), + /** Carrots (Age 6) */ + CARROTS_6(141, 6), + /** Carrots (Age 7) */ + CARROTS_7(141, 7), + + /** Potatoes (Age 0) */ + POTATOES (142, 0), + /** Potatoes (Age 1) */ + POTATOES_1(142, 1), + /** Potatoes (Age 2) */ + POTATOES_2(142, 2), + /** Potatoes (Age 3) */ + POTATOES_3(142, 3), + /** Potatoes (Age 4) */ + POTATOES_4(142, 4), + /** Potatoes (Age 5) */ + POTATOES_5(142, 5), + /** Potatoes (Age 6) */ + POTATOES_6(142, 6), + /** Potatoes (Age 7) */ + POTATOES_7(142, 7), + + /** Beetroot (Age 0) */ + BEETROOT (207, 0), + /** Beetroot (Age 1) */ + BEETROOT_1(207, 1), + /** Beetroot (Age 2) */ + BEETROOT_2(207, 2), + /** Beetroot (Age 3) */ + BEETROOT_3(207, 3); + + BlockType(int id, int meta) { + this.id = id; + this.meta = meta; + 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; +} diff --git a/core/src/main/java/mc/core/world/chunk/Chunk.java b/core/src/main/java/mc/core/world/chunk/Chunk.java new file mode 100644 index 0000000..4266487 --- /dev/null +++ b/core/src/main/java/mc/core/world/chunk/Chunk.java @@ -0,0 +1,65 @@ +package mc.core.world.chunk; + +import mc.core.world.Biome; +import mc.core.world.block.Block; + +/* 16x256x16 */ +public interface Chunk { + /** + * Глобальная координата X + * @return + */ + int getX(); + + /** + * Глобальная координата Z + * @return + */ + int getZ(); + + /** + * Получить секцию чанка + * @param height высота (0-15) + * @return {@link mc.core.world.chunk.ChunkSection} + */ + ChunkSection getChunkSection(int height); + + /** + * Установить секцию чанка + * @param height высота (0-15) + * @param chunkSection {@link mc.core.world.chunk.ChunkSection} + */ + void setChunkSection(int height, ChunkSection chunkSection); + + /** + * Получить блок по глобальным координатам секции чанка + * @param x global X + * @param y global Y + * @param z global Z + * @return {@link Block} + */ + Block getBlock(int x, int y, int z); + void setBlock(Block block); + + int getSkyLight(int x, int y, int z); + void setSkyLight(int x, int y, int z, int lightLevel); + + int getAddition(int x, int y, int z); + void setAddition(int x, int y, int z, int value); + + /** + * Получить тип биома по глобальным координатам + * @param x global X + * @param z global Z + * @return {@link mc.core.world.Biome} + */ + Biome getBiome(int x, int z); + + /** + * Указать данные по биому + * @param x global X + * @param z global Z + * @param biome {@link mc.core.world.Biome} + */ + void setBiome(int x, int z, Biome biome); +} diff --git a/core/src/main/java/mc/core/world/chunk/ChunkProvider.java b/core/src/main/java/mc/core/world/chunk/ChunkProvider.java new file mode 100644 index 0000000..d363fdb --- /dev/null +++ b/core/src/main/java/mc/core/world/chunk/ChunkProvider.java @@ -0,0 +1,14 @@ +package mc.core.world.chunk; + +public interface ChunkProvider { + /** + * Получить чанк по координатам + * @param x глобальный X + * @param z глобальный Z + * @return {@link mc.core.world.chunk.Chunk} + */ + Chunk getChunk(int x , int z); + + void saveChunk(Chunk chunk); + void saveChunk(Chunk... chunks); +} diff --git a/core/src/main/java/mc/core/world/chunk/ChunkSection.java b/core/src/main/java/mc/core/world/chunk/ChunkSection.java new file mode 100644 index 0000000..ebe5cd8 --- /dev/null +++ b/core/src/main/java/mc/core/world/chunk/ChunkSection.java @@ -0,0 +1,53 @@ +package mc.core.world.chunk; + +import mc.core.world.block.Block; + +/** + * Секция чанка размером 16x16x16 блоков + */ +public interface ChunkSection { + Chunk getParent(); + void setParent(Chunk chunk); + + /** + * Высота + * @return + */ + int getY(); + + /** + * Получить блок по локальным координатам секции чанка + * @param localX local X (0-15) + * @param localY local Y (0-15) + * @param localZ local Z (0-15) + * @return {@link Block} + */ + Block getBlock(int localX, int localY, int localZ); + + /** + * Установить блок + * @param block {@link mc.core.world.block.Block} + */ + void setBlock(Block block); + + /** + * Получить данные о естественной подсветке + * @param localX локальный X (0-15) + * @param localY локальный Y (0-15) + * @param localZ локальный Z (0-15) + * @return integer значение 0-15, где 0 - это света нет, а 15 - получает прямой солнечный свет + */ + int getSkyLight(int localX, int localY, int localZ); + + /** + * Указать данные о естественной подсветке + * @param localX локальный X (0-15) + * @param localY локальный Y (0-15) + * @param localZ локальный Z (0-15) + * @param lightLevel значение 0-15, где 0 - это света нет, а 15 - получает прямой солнечный свет + */ + void setSkyLight(int localX, int localY, int localZ, int lightLevel); + + int getAddition(int localX, int localY, int localZ); + void setAddition(int localX, int localY, int localZ, int value); +} diff --git a/core/src/main/resources/spring.xml b/core/src/main/resources/spring.xml new file mode 100644 index 0000000..bb81a1a --- /dev/null +++ b/core/src/main/resources/spring.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/test/java/mc/core/EntityLocationTest.java b/core/src/test/java/mc/core/EntityLocationTest.java new file mode 100644 index 0000000..0e31fdc --- /dev/null +++ b/core/src/test/java/mc/core/EntityLocationTest.java @@ -0,0 +1,91 @@ +package mc.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.*; + +class EntityLocationTest { + private static final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + private static final double minD = 0.0d, maxD = 10.0d; + private static final float minF = 0.0f, maxF = 359.9f; + private double x, y, z; + private float yaw, pitch; + + @BeforeEach + void before() { + x = rnd.nextDouble(minD, maxD); + y = rnd.nextDouble(minD, maxD); + z = rnd.nextDouble(minD, maxD); + yaw = rnd.nextFloat() * (maxF - minF) + minF; + pitch = rnd.nextFloat() * (maxF - minF) + minF; + } + + @Test + void equals_() { + EntityLocation loc1 = new EntityLocation(x, y, z, yaw, pitch); + EntityLocation loc2 = new EntityLocation(x, y, z, yaw, pitch); + assertEquals(loc1, loc2); + + loc2 = new EntityLocation(x+1, y+2, z-3, yaw, pitch); + assertNotEquals(loc1, loc2); + + loc2 = new EntityLocation(x, y, z, yaw-1, pitch+2); + assertNotEquals(loc1, loc2); + } + + @Test + void clone_() { + EntityLocation locOrig = new EntityLocation(x, y, z, yaw, pitch); + EntityLocation locClone = locOrig.clone(); + assertEquals(locOrig, locClone); + assertNotSame(locOrig, locClone); + } + + @Test + void getBlockXZ() { + EntityLocation location; + + location = new EntityLocation(0d, 0, 0d, 0f, 0f); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(0.1d, 0, 0.1d); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(0.5d, 0, 0.5d); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(0.9d, 0, 0.9d); + assertEquals(0, location.getBlockX()); + assertEquals(0, location.getBlockZ()); + + location.setXYZ(1d, 0, 1d); + assertEquals(1, location.getBlockX()); + assertEquals(1, location.getBlockZ()); + + location.setXYZ(-0.1d, 0, -0.1d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-0.5d, 0, -0.5d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-0.9d, 0, -0.9d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-1d, 0, -1d); + assertEquals(-1, location.getBlockX()); + assertEquals(-1, location.getBlockZ()); + + location.setXYZ(-1.1d, 0, -1.1d); + assertEquals(-2, location.getBlockX()); + assertEquals(-2, location.getBlockZ()); + } +} diff --git a/core/src/test/java/mc/core/ImmutableEntityLocationTest.java b/core/src/test/java/mc/core/ImmutableEntityLocationTest.java new file mode 100644 index 0000000..0335538 --- /dev/null +++ b/core/src/test/java/mc/core/ImmutableEntityLocationTest.java @@ -0,0 +1,33 @@ +package mc.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ImmutableEntityLocationTest { + + @Test + void setValue() { + EntityLocation location = new ImmutableEntityLocation(1d, 2d, 3d, 4f, 5f); + + assertThrows(UnsupportedOperationException.class, () -> { + location.setX(1); + location.setY(1); + location.setZ(1); + location.setYaw(1); + location.setPitch(1); + location.setXYZ(1, 2, 3); + location.setYawPitch(1, 2); + location.set(EntityLocation.ZERO()); + }); + } + + @Test + void clone_() { + EntityLocation locOrig = new ImmutableEntityLocation(1d, 2d, 3d, 4f, 5f); + EntityLocation locClone = locOrig.clone(); + + assertEquals(locOrig, locClone); + } +} \ No newline at end of file diff --git a/core/src/test/java/mc/core/TestEventBus.java b/core/src/test/java/mc/core/TestEventBus.java new file mode 100644 index 0000000..beb19c8 --- /dev/null +++ b/core/src/test/java/mc/core/TestEventBus.java @@ -0,0 +1,125 @@ +package mc.core; + +import javafx.util.Pair; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import mc.core.eventbus.Event; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.Subscriber; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.internal.util.reflection.Whitebox; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class TestEventBus { + private List resultList = new ArrayList<>(); + + @SuppressWarnings("unchecked") + private Map, List>> getEventBusFieldSubscribes() { + return (Map, List>>) + Whitebox.getInternalState(EventBus.getInstance(), "subscribes"); + } + + @BeforeEach + @SuppressWarnings("unchecked") + void before() { + getEventBusFieldSubscribes().clear(); + ((Queue) Whitebox.getInternalState(EventBus.getInstance(), "eventQueue")).clear(); + } + + + @Test + void testRegisterSubscribes() { + DumbEventHandler handler = new DumbEventHandler(); + EventBus.getInstance().registerSubscribes(handler); + + Map, List>> subscribes = getEventBusFieldSubscribes(); + assertEquals(1, subscribes.size()); + + List> pairs = subscribes.values().iterator().next(); + assertEquals(1, pairs.size()); + + Pair pair = pairs.get(0); + assertSame(handler, pair.getKey()); + assertEquals("corectSubscribe", pair.getValue().getName()); + } + + @Test + void testUnregisterSubscribes() { + DumbEventHandler handler = new DumbEventHandler(); + EventBus.getInstance().registerSubscribes(handler); + + EventBus.getInstance().unregisterSubscribes(handler); + + Map, List>> subscribes = getEventBusFieldSubscribes(); + assertEquals(0, subscribes.size()); + } + + @Test + @SuppressWarnings("unchecked") + void testPost() { + EventBus.getInstance().post(new DumbEvent()); + + Queue eventQueue = (Queue) Whitebox.getInternalState(EventBus.getInstance(), "eventQueue"); + assertEquals(1, eventQueue.size()); + } + + @Test + void testProcess() { + Stream.of(new DumbEventHandler("D1 "), new DumbEventHandler("D2 ")) + .forEach(handler -> EventBus.getInstance().registerSubscribes(handler)); + + Stream.of(new DumbEvent("message 1"), new DumbEvent("message 2")) + .forEach(event -> EventBus.getInstance().post(event)); + + EventBus.getInstance().process(); + + assertEquals(4, resultList.size()); + assertEquals("D1 message 1", resultList.get(0)); + assertEquals("D2 message 1", resultList.get(1)); + assertEquals("D1 message 2", resultList.get(2)); + assertEquals("D2 message 2", resultList.get(3)); + } + + @AllArgsConstructor + @NoArgsConstructor + private class DumbEvent implements Event { + String message; + } + + @AllArgsConstructor + @NoArgsConstructor + public class DumbEventHandler { + private String prefix = ""; + + @Subscriber + public void corectSubscribe(DumbEvent event) { + resultList.add(prefix + event.message); + } + + @Subscriber + public Object incorectSubscribeReturnType(DumbEvent event) { + return null; + } + + @Subscriber + public void incorrectSubscriberTypeParameter(Object object) { + } + + @Subscriber + public void incorrectSubscriberManyParameters(DumbEvent event, Object object) { + } + + public void someMethod() { + } + } +} diff --git a/core/src/test/java/mc/core/TestSpringConfig.java b/core/src/test/java/mc/core/TestSpringConfig.java new file mode 100644 index 0000000..34dc1de --- /dev/null +++ b/core/src/test/java/mc/core/TestSpringConfig.java @@ -0,0 +1,34 @@ +package mc.core; + +import mc.core.world.World; +import mc.core.world.chunk.Chunk; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Configuration +public class TestSpringConfig { + @Bean() + public World simpleMockWorld() { + return mock(World.class); + } + + @Bean + public World chunkedMockWorld() { + World world = mock(World.class); + when(world.getChunk(anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + + Chunk chunk = mock(Chunk.class); + when(chunk.getX()).thenReturn((int) args[0]); + when(chunk.getZ()).thenReturn((int) args[1]); + + return chunk; + }); + + return world; + } +} diff --git a/core/src/test/java/mc/core/text/TextTest.java b/core/src/test/java/mc/core/text/TextTest.java new file mode 100644 index 0000000..fbb417b --- /dev/null +++ b/core/src/test/java/mc/core/text/TextTest.java @@ -0,0 +1,76 @@ +package mc.core.text; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TextTest { + @Test + void toPlain() { + final String m1 = "mes"; + final String m2 = "sage"; + final String message = m1 + m2; + + assertEquals(message, Text.of(message).toPlain()); + assertEquals(message, Text.builder(message).build().toPlain()); + assertEquals(message, Text.builder(Text.of(message)).build().toPlain()); + assertEquals(message, Text.builder().append(message).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(message)).build().toPlain()); + + assertEquals(message, Text.builder(m1, m2).build().toPlain()); + assertEquals(message, Text.builder(Text.of(m1), Text.of(m2)).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(m1), Text.of(m2)).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(m1)).append(Text.of(m2)).build().toPlain()); + + + } + + @Test + void equals_() { + assertEquals(Text.of(), Text.of("")); + assertEquals(Text.of(), Text.builder().build()); + assertEquals(Text.of(), Text.builder("").build()); + assertEquals(Text.of(), Text.builder().append().build()); + assertEquals(Text.of(), Text.builder().append("").build()); + + assertNotEquals(Text.of(), Text.of("??")); + assertNotEquals(Text.of(), Text.builder("??").build()); + assertNotEquals(Text.of(), Text.builder().append("??").build()); + + assertEquals(Text.of("message"), Text.builder("message").build()); + assertEquals(Text.of("message"), Text.builder(Text.of("message")).build()); + assertEquals(Text.of("message"), Text.builder().append("message").build()); + assertEquals(Text.of("message"), Text.builder().append(Text.of("message")).build()); + } + + @Test + void isEmpty() { + assertTrue(Text.of().isEmpty()); + assertTrue(Text.of((String) null).isEmpty()); + assertTrue(Text.of((Text) null).isEmpty()); + assertTrue(Text.of("").isEmpty()); + assertTrue(Text.of("", "").isEmpty()); + + assertTrue(Text.builder().build().isEmpty()); + assertTrue(Text.builder((String) null).build().isEmpty()); + assertTrue(Text.builder((Text) null).build().isEmpty()); + assertTrue(Text.builder("").build().isEmpty()); + assertTrue(Text.builder("", "").build().isEmpty()); + assertTrue(Text.builder(Text.of()).build().isEmpty()); + assertTrue(Text.builder(Text.of(), Text.of()).build().isEmpty()); + + assertTrue(Text.builder().append().build().isEmpty()); + assertTrue(Text.builder().append((String) null).build().isEmpty()); + assertTrue(Text.builder().append((Text) null).build().isEmpty()); + assertTrue(Text.builder().append("").build().isEmpty()); + assertTrue(Text.builder().append(Text.of()).build().isEmpty()); + assertTrue(Text.builder().append(Text.of(), Text.of()).build().isEmpty()); + assertTrue(Text.builder().append(Text.of()).append(Text.of()).build().isEmpty()); + + assertFalse(Text.of("??").isEmpty()); + assertFalse(Text.builder("??").build().isEmpty()); + assertFalse(Text.builder(Text.of("??")).build().isEmpty()); + assertFalse(Text.builder().append("??").build().isEmpty()); + assertFalse(Text.builder().append(Text.of("??")).build().isEmpty()); + } +} diff --git a/core/src/test/java/mc/core/utils/CompactedCoordsTest.java b/core/src/test/java/mc/core/utils/CompactedCoordsTest.java new file mode 100644 index 0000000..ab75543 --- /dev/null +++ b/core/src/test/java/mc/core/utils/CompactedCoordsTest.java @@ -0,0 +1,37 @@ +package mc.core.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CompactedCoordsTest { + private static Stream streamTestParams() { + return Stream.of( + Arguments.of(Short.MIN_VALUE, Short.MIN_VALUE), + Arguments.of(Short.MIN_VALUE, Short.MAX_VALUE), + Arguments.of(Short.MAX_VALUE, Short.MAX_VALUE), + Arguments.of(Short.MAX_VALUE, Short.MIN_VALUE), + Arguments.of(0, 0), + Arguments.of(-1, -1), + Arguments.of(-1, 1), + Arguments.of(1, 1), + Arguments.of(1, -1) + ); + } + + @ParameterizedTest + @MethodSource("streamTestParams") + void testCompress(int x, int z) { + final int compressXZ = CompactedCoords.compressXZ(x, z); + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + + assertTrue(x == xz[0] && z == xz[1], + String.format("x = %d, vx = %d; z = %d, vz = %d", + x, xz[0], + z, xz[1])); + } +} diff --git a/core/src/test/java/mc/core/world/block/BlockLocationTest.java b/core/src/test/java/mc/core/world/block/BlockLocationTest.java new file mode 100644 index 0000000..d716995 --- /dev/null +++ b/core/src/test/java/mc/core/world/block/BlockLocationTest.java @@ -0,0 +1,39 @@ +package mc.core.world.block; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class BlockLocationTest { + private static final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + private static final int minI = 0, maxI = 10; + private int x, y, z; + + @BeforeEach + void before() { + x = rnd.nextInt(minI, maxI); + y = rnd.nextInt(minI, maxI); + z = rnd.nextInt(minI, maxI); + } + + @Test + void equals_() { + BlockLocation loc1 = new BlockLocation(x, y, z); + BlockLocation loc2 = new BlockLocation(x, y, z); + assertEquals(loc1, loc2); + + loc2 = new BlockLocation(x+1, y+2, z-3); + assertNotEquals(loc1, loc2); + } + + @Test + void clone_() { + BlockLocation locOrig = new BlockLocation(x, y, z); + BlockLocation locClone = locOrig.clone(); + assertEquals(locOrig, locClone); + } +} diff --git a/h2_playermanager/build.gradle b/h2_playermanager/build.gradle new file mode 100644 index 0000000..acdbf0c --- /dev/null +++ b/h2_playermanager/build.gradle @@ -0,0 +1,17 @@ +version '1.0-SNAPSHOT' + +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/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..7db2b42 --- /dev/null +++ b/h2_playermanager/src/main/java/mc/core/h2db/H2PlayerManager.java @@ -0,0 +1,98 @@ +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.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Component +public class H2PlayerManager implements PlayerManager { + @Setter + @Autowired + private H2PlayerService h2PlayerService; + private List playerList = Collections.synchronizedList(new ArrayList<>()); + + @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/build.gradle b/proto_1.12.2/build.gradle new file mode 100644 index 0000000..7c71711 --- /dev/null +++ b/proto_1.12.2/build.gradle @@ -0,0 +1,10 @@ +version '1.0-SNAPSHOT' + +dependencies { + /* Core */ + compile_excludeCopy project(':core') + + /* Components */ + compile (group: 'com.google.code.gson', name: 'gson', version: '2.8.5') + compile (group: 'net.sf.trove4j', name: 'trove4j', version: '3.0.3') +} 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..a548a3d --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/State.java @@ -0,0 +1,132 @@ +/* + * DmitriyMX + * 2018-06-08 + */ +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 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(0x01, TabCompletePacket.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(0x14, PlayerDiggingPacket.class) + .put(0x15, EntityActionPacket.class) + .put(0x1A, HeldItemChangePacket.class) + .put(0x1D, AnimationPacket.class) + .put(0x1F, PlayerBlockPlacementPacket.class) + .build(), + ImmutableMap., Integer>builder() + .put(BossBarPacket.class, 0x0C) + .put(ChatMessageServerPacket.class, 0x0F) + .put(PluginMessagePacket.class, 0x18) + .put(UnloadChunkPacket.class, 0x1D) + .put(ChangeGameState.class, 0x1E) + .put(KeepAlivePacket.class, 0x1F) + .put(ChunkDataPacket.class, 0x20) + .put(JoinGamePacket.class, 0x23) + .put(PlayerAbilitiesPacket.class, 0x2C) + .put(PlayerListItemPacket.class, 0x2E) + .put(PlayerPositionAndLookPacket.class, 0x2F) + .put(SpawnPositionPacket.class, 0x46) + .put(TimeUpdatePacket.class, 0x47) + .put(TitlePacket.class, 0x48) + .put(PlayerListHeaderAndFooterPacket.class, 0x4A) + .build() + ); + + public static State valueOf(int id) { + if (id == 0) return HANDSHAKE; + else if (id == 1) return STATUS; + else if (id == 2) return LOGIN; + else if (id == 3) return PLAY; + else { + log.warn("Unknown state: {}", id); + return UNKNOWN; + } + } + + @Getter + private final int id; + private final Map> clientSidePacketsMap; + private final Map, Integer> serverSidePacketsMap; + + public Class getClientSidePacket(int id) { + return clientSidePacketsMap.get(id); + } + + public Integer getServerSidePacket(Class clazz) { + return serverSidePacketsMap.get(clazz); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/TeleportManager.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/TeleportManager.java new file mode 100644 index 0000000..ba9cd07 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/TeleportManager.java @@ -0,0 +1,55 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +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/package-info.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/package-info.java new file mode 100644 index 0000000..d6207e6 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/package-info.java @@ -0,0 +1,32 @@ +/** + * Протокол Minecraft версии 1.12.2 (номер версии протокола - 340) + * + * + * Типы данных. + * + * (см. http://wiki.vg/Protocol#Data_types) + * + * + * Формат пакетов. + * + * Есть два варианта: без использования сжатия и с использованием. + * Регулируется это пакетом {@link mc.core.network.proto_1_12_2.packets.SetCompressionPacket} + * + * Формат без использования сжатия: + * + * +---------------+------------+--------------------+ + * | Название | Тип | Комментарий | + * +---------------+------------+--------------------+ + * | Размер пакета | VarInt | ID пакета + данные | + * +---------------+------------+--------------------+ + * | ID пакета | VarInt | | + * +---------------+------------+--------------------+ + * | Данные | Byte Array | | + * +---------------+------------+--------------------+ + * + * Формат с использованием сжатия: + * + * (см. http://wiki.vg/Protocol#With_compression) + */ + +package mc.core.network.proto_1_12_2; \ No newline at end of file diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/AnimationPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/AnimationPacket.java new file mode 100644 index 0000000..be82f07 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/AnimationPacket.java @@ -0,0 +1,17 @@ +/* + * DmitriyMX + * 2018-06-17 + */ +package mc.core.network.proto_1_12_2.packets; + +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +public class AnimationPacket implements CSPacket { + private int handAnimation; + + @Override + public void readSelf(NetInputStream netStream) { + this.handAnimation = netStream.readVarInt(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/BossBarPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/BossBarPacket.java new file mode 100644 index 0000000..7339c11 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/BossBarPacket.java @@ -0,0 +1,117 @@ +/* + * DmitriyMX + * 2018-07-12 + */ +package mc.core.network.proto_1_12_2.packets; + +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.TextMapper; +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(TextMapper.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/ChangeGameState.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChangeGameState.java new file mode 100644 index 0000000..e4d1169 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChangeGameState.java @@ -0,0 +1,36 @@ +/* + * DmitriyMX + * 2018-07-27 + */ +package mc.core.network.proto_1_12_2.packets; + +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@Setter +public class ChangeGameState implements SCPacket { + @RequiredArgsConstructor + public enum Reason { + INVALID_BED(0), // Would be used to switch between messages, but the only used message is 0 for invalid bed (wat?) + RAINING_END(1), + RAINING_BEGIN(2), + CHANGE_GAMEMODE(3), // 0: Survival, 1: Creative, 2: Adventure, 3: Spectator + ARROW_HITTING_PLAYER(6), // Appears to be played when an arrow strikes another player in Multiplayer + FADE_VALUE(7), // The current darkness value. 1 = Dark, 0 = Bright, Setting the value higher causes the game to change color and freeze + FADE_TIME(8), // Time in ticks for the sky to fade + GUARDIAN_APPEARANCE(10); // Play elder guardian mob appearance (effect and sound) + + private final int id; + } + + private Reason reason; + private float value; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeUnsignedByte(reason.id); + netStream.writeFloat(value); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChatMessageClientPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChatMessageClientPacket.java new file mode 100644 index 0000000..70aa84a --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChatMessageClientPacket.java @@ -0,0 +1,21 @@ +/* + * DmitriyMX + * 2018-06-17 + */ +package mc.core.network.proto_1_12_2.packets; + +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/ChatMessageServerPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChatMessageServerPacket.java new file mode 100644 index 0000000..903e61c --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChatMessageServerPacket.java @@ -0,0 +1,30 @@ +/* + * DmitriyMX + * 2018-06-24 + */ +package mc.core.network.proto_1_12_2.packets; + +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.TextMapper; +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(TextMapper.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/ChunkDataPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacket.java new file mode 100644 index 0000000..9be81ac --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacket.java @@ -0,0 +1,317 @@ +package mc.core.network.proto_1_12_2.packets; + +import com.flowpowered.nbt.CompoundTag; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +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.BlockLocation; +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 TIntList palette = new TIntArrayList(); + private byte[] blocks = new byte[4096]; + private NibbleArray blockLight = new NibbleArray(); + private NibbleArray skyLight = new NibbleArray(); + + private int coordsToIndex(BlockLocation location) { + return coordsToIndex(location.getX(), location.getY(), location.getZ()); + } + + 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) { + BlockLocation location = new BlockLocation( + block.getLocation().getX() - ((block.getLocation().getX() >> 4) << 4), + block.getLocation().getY() - ((block.getLocation().getY() >> 4) << 4), + block.getLocation().getZ() - ((block.getLocation().getZ() >> 4) << 4) + ); + blocks[coordsToIndex(location)] = addBlockType(block.getType()); + blockLight.set(location, block.getLight()); + this.skyLight.set(location, 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(value -> { netOutputStream.writeVarInt(value); return true; }); // 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/ClientSettingsPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ClientSettingsPacket.java new file mode 100644 index 0000000..6cb98bd --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/ClientSettingsPacket.java @@ -0,0 +1,48 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +package mc.core.network.proto_1_12_2.packets; + +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/DisconnectPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/DisconnectPacket.java new file mode 100644 index 0000000..96f83f7 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/DisconnectPacket.java @@ -0,0 +1,25 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +package mc.core.network.proto_1_12_2.packets; + +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.TextMapper; +import mc.core.text.Text; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +public class DisconnectPacket implements SCPacket { + private Text reason; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeString(TextMapper.getInstance().mapping(reason)); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/EncryptionRequestPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/EncryptionRequestPacket.java new file mode 100644 index 0000000..33a2f54 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/EncryptionRequestPacket.java @@ -0,0 +1,32 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +package mc.core.network.proto_1_12_2.packets; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +import java.security.PublicKey; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +public class EncryptionRequestPacket implements SCPacket { + private String serverId; + private PublicKey publicKey; + private byte[] verifyToken; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeString(serverId); + byte[] bytes = publicKey.getEncoded(); + netStream.writeVarInt(bytes.length); + netStream.writeBytes(bytes); + netStream.writeVarInt(verifyToken.length); + netStream.writeBytes(verifyToken); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/EntityActionPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/EntityActionPacket.java new file mode 100644 index 0000000..0e02a01 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/EntityActionPacket.java @@ -0,0 +1,45 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; + +import java.util.Arrays; + +@Getter +public class EntityActionPacket implements CSPacket { + @RequiredArgsConstructor + public enum Action { + START_SNEAKING(0), + STOP_SNEAKING(1), + LEAVE_BED(2), // Leave bed is only sent when the “Leave Bed” button is clicked on the sleep GUI, not when waking up due today time. + START_SPRINTING(3), + STOP_SPRINTING(4), + START_JUMP_WITH_HORSE(5), + STOP_JUMP_WITH_HORSE(6), + OPEN_HORSE_INVENTORY(7), // Open horse inventory is only sent when pressing the inventory key (default: E) while on a horse — all other methods of opening a horse's inventory (involving right-clicking or shift-right-clicking it) do not use this packet. + START_FLYING_WITH_ELYTRA(8); + + public static Action getById(final int id) { + return Arrays.stream(Action.values()) + .filter(action -> action.id == id) + .findFirst() + .orElse(null); + } + + @Getter + private final int id; + } + + private int entityId; + private Action action; + private int jumpBoost; // Only used by the “start jump with horse” action, in which case it ranges from 0 to 100. In all other cases it is 0. + + @Override + public void readSelf(NetInputStream netStream) { + entityId = netStream.readVarInt(); + action = Action.getById(netStream.readVarInt()); + jumpBoost = netStream.readVarInt(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/HandshakePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/HandshakePacket.java new file mode 100644 index 0000000..a0a2ab2 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/HandshakePacket.java @@ -0,0 +1,30 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +package mc.core.network.proto_1_12_2.packets; + +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/HeldItemChangePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/HeldItemChangePacket.java new file mode 100644 index 0000000..ca9f073 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/HeldItemChangePacket.java @@ -0,0 +1,17 @@ +/* + * DmitriyMX + * 2018-06-17 + */ +package mc.core.network.proto_1_12_2.packets; + +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/JoinGamePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/JoinGamePacket.java new file mode 100644 index 0000000..6000085 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/JoinGamePacket.java @@ -0,0 +1,34 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +package mc.core.network.proto_1_12_2.packets; + +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/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..bb4ed23 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/KeepAlivePacket.java @@ -0,0 +1,32 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +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/LoginStartPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/LoginStartPacket.java new file mode 100644 index 0000000..e3654c7 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/LoginStartPacket.java @@ -0,0 +1,21 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +package mc.core.network.proto_1_12_2.packets; + +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/LoginSuccessPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/LoginSuccessPacket.java new file mode 100644 index 0000000..376d7a1 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/LoginSuccessPacket.java @@ -0,0 +1,29 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +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.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/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..3f63723 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PingPacket.java @@ -0,0 +1,26 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +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..6258bc5 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacket.java @@ -0,0 +1,58 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +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/PlayerBlockPlacementPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerBlockPlacementPacket.java new file mode 100644 index 0000000..b9c2a2e --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerBlockPlacementPacket.java @@ -0,0 +1,30 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.Getter; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.proto_1_12_2.Direction; +import mc.core.network.proto_1_12_2.serializers.BlockLocationSerializer; +import mc.core.world.block.BlockLocation; + +@Getter +@ToString +public class PlayerBlockPlacementPacket implements CSPacket { + private BlockLocation location; + private Direction face; + /** true - main hand; false - off hand */ + private boolean hand; + private float cursorX, cursorY, cursorZ; + + @Override + public void readSelf(NetInputStream netStream) { + long compactedCoords = netStream.readLong(); + location = BlockLocationSerializer.fromLong(compactedCoords); + face = Direction.getById(netStream.readVarInt()); + hand = (netStream.readVarInt() == 1); + cursorX = netStream.readFloat(); + cursorY = netStream.readFloat(); + cursorZ = netStream.readFloat(); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerDiggingPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerDiggingPacket.java new file mode 100644 index 0000000..341ca67 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerDiggingPacket.java @@ -0,0 +1,55 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.proto_1_12_2.Direction; +import mc.core.network.proto_1_12_2.serializers.BlockLocationSerializer; +import mc.core.world.block.BlockLocation; + +import java.util.Arrays; + +@Getter +@ToString +public class PlayerDiggingPacket implements CSPacket { + @RequiredArgsConstructor + public enum Status { + STARTED_DIGGING(0), + CANCELLED_DIGGING(1), + FINISHED_DIGGING(2), + DROP_ITEM_STACK(3), + DROP_ITEM(4), + /* Indicates that the currently held item should have its + * state updated such as eating food, pulling back bows, + * using buckets, etc. Location is always set to 0/0/0, + * Face is always set to -Y. + */ + SHOOT_ARROW(5), + FINISH_EATING(5), + SWAP_ITEM_IN_HAND(6); + + public static Status getById(final int id) { + return Arrays.stream(Status.values()) + .filter(status -> status.id == id) + .findFirst() + .orElse(null); + } + + @Getter + private final int id; + } + + private Status status; + private BlockLocation location; + private Direction face; + + @Override + public void readSelf(NetInputStream netStream) { + status = Status.getById(netStream.readVarInt()); + long compactCoord = netStream.readLong(); + location = BlockLocationSerializer.fromLong(compactCoord); + face = Direction.getById(netStream.readByte()); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerListHeaderAndFooterPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerListHeaderAndFooterPacket.java new file mode 100644 index 0000000..20872c5 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerListHeaderAndFooterPacket.java @@ -0,0 +1,35 @@ +/* + * DmitriyMX + * 2018-07-11 + */ +package mc.core.network.proto_1_12_2.packets; + +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.TextMapper; +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(TextMapper.getInstance().mapping(header)); + } + + if (footer == null) { + netStream.writeString("{\"translate\":\"\"}"); + } else { + netStream.writeString(TextMapper.getInstance().mapping(footer)); + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerListItemPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerListItemPacket.java new file mode 100644 index 0000000..9eb85d3 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerListItemPacket.java @@ -0,0 +1,84 @@ +/* + * DmitriyMX + * 2018-07-11 + */ +package mc.core.network.proto_1_12_2.packets; + +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.TextMapper; +import mc.core.player.PlayerMode; +import mc.core.text.Text; + +import java.util.*; + +@Slf4j +@Getter +@Setter +@ToString +public class PlayerListItemPacket implements SCPacket { + @RequiredArgsConstructor + public enum Action { + ADD_PLAYER(0), + UPDATE_GAMEMODE(1), + UPDATE_LATENCY(2), + UPDATE_DISPLAY_NAME(3), + REMOVE_PLAYER(4); + + @Getter + private final int id; + } + + @Data + @ToString + public static class PlayerData { + private UUID uuid; + private String name; + private Properties properties = new Properties(); + private PlayerMode gameMode; + private int ping; + private boolean hasDisplayName = false; + private Text displayName; + } + + private Action action; + private List listPlayers = new ArrayList<>(); + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeVarInt(action.id); + netStream.writeVarInt(listPlayers.size()); + + for (PlayerData playerData : listPlayers) { + netStream.writeUUID(playerData.uuid); + + if (action == Action.ADD_PLAYER) { + netStream.writeString(playerData.name); + netStream.writeVarInt(playerData.properties.size()); + + for (Map.Entry entry : playerData.properties.entrySet()) { + netStream.writeString(entry.getKey().toString()); + netStream.writeString(entry.getValue().toString()); + netStream.writeBoolean(false); // Is Signed + } + } + + if (action == Action.ADD_PLAYER || action == Action.UPDATE_GAMEMODE) { + netStream.writeVarInt(playerData.gameMode.getId()); + } + + if (action == Action.ADD_PLAYER || action == Action.UPDATE_LATENCY) { + netStream.writeVarInt(playerData.ping); + } + + if (action == Action.ADD_PLAYER || action == Action.UPDATE_DISPLAY_NAME) { + netStream.writeBoolean(playerData.hasDisplayName); + if (playerData.hasDisplayName) { + netStream.writeString(TextMapper.getInstance().mapping(playerData.displayName)); + } + } + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerLookPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerLookPacket.java new file mode 100644 index 0000000..b21e14b --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerLookPacket.java @@ -0,0 +1,18 @@ +package mc.core.network.proto_1_12_2.packets; + +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/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..3c783a7 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerPositionAndLookPacket.java @@ -0,0 +1,56 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +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/PlayerPositionPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerPositionPacket.java new file mode 100644 index 0000000..986af29 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PlayerPositionPacket.java @@ -0,0 +1,19 @@ +package mc.core.network.proto_1_12_2.packets; + +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/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..cef386f --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/PluginMessagePacket.java @@ -0,0 +1,34 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +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/SpawnPositionPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/SpawnPositionPacket.java new file mode 100644 index 0000000..2b3eb6e --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/SpawnPositionPacket.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.EntityLocation; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@ToString +public class SpawnPositionPacket implements SCPacket { + private EntityLocation location; + + private long location2long(EntityLocation entityLocation) { + return (((long) entityLocation.getBlockX() & 0x3FFFFFF) << 38) + | (((long) entityLocation.getBlockY() & 0x0000FFF) << 26) + | ((long) entityLocation.getBlockZ() & 0x3FFFFFF); + } + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeLong(location2long(location)); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/StatusRequestPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/StatusRequestPacket.java new file mode 100644 index 0000000..b57c56a --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/StatusRequestPacket.java @@ -0,0 +1,16 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +package mc.core.network.proto_1_12_2.packets; + +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/StatusResponsePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/StatusResponsePacket.java new file mode 100644 index 0000000..3505581 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/StatusResponsePacket.java @@ -0,0 +1,53 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +package mc.core.network.proto_1_12_2.packets; + +import com.google.gson.JsonObject; +import lombok.Setter; +import lombok.ToString; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +@Setter +@ToString +public class StatusResponsePacket implements SCPacket { + public static final String NAME = "1.12.2"; + public static final int PROTOCOL = 340; + private static final JsonObject versionObj; + + static { + versionObj = new JsonObject(); + versionObj.addProperty("name", NAME); + versionObj.addProperty("protocol", PROTOCOL); + } + + private int maxOnline; + private int online; + private String description; + private byte[] faviconBase64; + + @Override + public void writeSelf(NetOutputStream netStream) { + JsonObject playersObj = new JsonObject(); + playersObj.addProperty("max", maxOnline); + playersObj.addProperty("online", online); + + JsonObject descriptionObj = new JsonObject(); + descriptionObj.addProperty("text", description); + + JsonObject rootObj = new JsonObject(); + rootObj.add("version", versionObj); + rootObj.add("players", playersObj); + rootObj.add("description", descriptionObj); + + if (faviconBase64 != null && faviconBase64.length > 0) { + rootObj.addProperty("favicon", + "data:image/png;base64," + new String(faviconBase64) + ); + } + + netStream.writeString(rootObj.toString()); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TabCompletePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TabCompletePacket.java new file mode 100644 index 0000000..4e1082e --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TabCompletePacket.java @@ -0,0 +1,33 @@ +/* + * DmitriyMX + * 2018-06-17 + */ +package mc.core.network.proto_1_12_2.packets; + +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.world.block.BlockLocation; + +public class TabCompletePacket implements CSPacket { + private String text; + private boolean assumeCommand; + private boolean hasPosition; + private BlockLocation location; + + @Override + public void readSelf(NetInputStream netStream) { + this.text = netStream.readString(); + this.assumeCommand = netStream.readBoolean(); + this.hasPosition = netStream.readBoolean(); + + if (this.hasPosition) { + long compactValue = netStream.readLong(); + + double x = compactValue >> 38; + double y = (compactValue >> 26) & 0xFFF; + double z = compactValue << 38 >> 38; // is normal? + + this.location = new BlockLocation((int)x, (int)y, (int)z); //FIXME + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TeleportConfirmPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TeleportConfirmPacket.java new file mode 100644 index 0000000..356b6a3 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TeleportConfirmPacket.java @@ -0,0 +1,21 @@ +/* + * DmitriyMX + * 2018-06-12 + */ +package mc.core.network.proto_1_12_2.packets; + +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/TimeUpdatePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TimeUpdatePacket.java new file mode 100644 index 0000000..e17160e --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TimeUpdatePacket.java @@ -0,0 +1,27 @@ +/* + * DmitriyMX + * 2018-06-24 + */ +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.NetOutputStream; +import mc.core.network.SCPacket; + +@AllArgsConstructor +@NoArgsConstructor +@Setter +@ToString +public class TimeUpdatePacket implements SCPacket { + private long time; + private long worldage; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeLong(worldage); + netStream.writeLong(time); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TitlePacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TitlePacket.java new file mode 100644 index 0000000..9b357a8 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/TitlePacket.java @@ -0,0 +1,117 @@ +/* + * DmitriyMX + * 2018-06-24 + */ +package mc.core.network.proto_1_12_2.packets; + +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.TextMapper; +import mc.core.text.Text; + +@RequiredArgsConstructor +@Setter +@ToString +public class TitlePacket implements SCPacket { + private static final int TICKS_PER_SEC = 20, + MIN_MS = (1000 / TICKS_PER_SEC); + + @RequiredArgsConstructor + public enum Action { + SET_TITLE(0), + SET_SUBTITLE(1), + SET_ACTION_BAR(2), + SET_DISPLAY_TIME(3), + HIDE(4), + RESET(5); + + @Getter + private final int id; + } + + private final Action action; + private Text text = null; + private Integer fadeInTime = null; + private Integer stayTime = null; + private Integer fadeOutTime = null; + + public TitlePacket(Action action, Object... values) { + if (values.length == 0 && (action != Action.HIDE && action != Action.RESET)) { + this.action = Action.HIDE; + return; + } + + this.action = action; + + switch (this.action) { + case SET_TITLE: + case SET_SUBTITLE: + case SET_ACTION_BAR: + if (values[0] == null) { + this.text = Text.of(); + } else if (values[0] instanceof Text) { + this.text = (Text) values[0]; + } else { + this.text = Text.of(values[0].toString()); + } + break; + case SET_DISPLAY_TIME: + if (values.length < 3) { + this.fadeInTime = 0; + this.stayTime = 0; + this.fadeOutTime = 0; + } else { + if (values[0] instanceof Integer) { + if (((Integer) values[0]) < MIN_MS) { + this.fadeInTime = 1; + } else { + this.fadeInTime = ((Integer) values[0]) / MIN_MS; + } + } else { + this.fadeInTime = 0; + } + + if (values[1] instanceof Integer) { + if (((Integer) values[1]) < MIN_MS) { + this.stayTime = 1; + } else { + this.stayTime = ((Integer) values[1]) / MIN_MS; + } + } else { + this.stayTime = 0; + } + + if (values[2] instanceof Integer) { + if (((Integer) values[2]) < MIN_MS) { + this.fadeOutTime = 1; + } else { + this.fadeOutTime = ((Integer) values[2]) / MIN_MS; + } + } else { + this.fadeOutTime = 0; + } + } + } + } + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeVarInt(action.id); + + switch (action) { + case SET_TITLE: + case SET_SUBTITLE: + case SET_ACTION_BAR: + netStream.writeString(TextMapper.getInstance().mapping(this.text)); + break; + case SET_DISPLAY_TIME: + netStream.writeInt(this.fadeInTime); + netStream.writeInt(this.stayTime); + netStream.writeInt(this.fadeOutTime); + } + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/UnloadChunkPacket.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/UnloadChunkPacket.java new file mode 100644 index 0000000..5bcfe15 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/packets/UnloadChunkPacket.java @@ -0,0 +1,16 @@ +package mc.core.network.proto_1_12_2.packets; + +import lombok.Setter; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; + +public class UnloadChunkPacket implements SCPacket { + @Setter + private int x, z; + + @Override + public void writeSelf(NetOutputStream netStream) { + netStream.writeInt(x); + netStream.writeInt(z); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializer.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializer.java new file mode 100644 index 0000000..9aea2f5 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializer.java @@ -0,0 +1,63 @@ +package mc.core.network.proto_1_12_2.serializers; + +import mc.core.world.block.BlockLocation; + +import static com.google.common.math.IntMath.isPowerOfTwo; + +public class BlockLocationSerializer { + private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[] {0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; + private static final int NUM_X_BITS = 1 + log2(smallestEncompassingPowerOfTwo(30000000)); + private static final int NUM_Z_BITS = NUM_X_BITS; + private static final int NUM_Y_BITS = 64 - NUM_X_BITS - NUM_Z_BITS; + private static final int Y_SHIFT = NUM_Z_BITS; + private static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS; + private static final long X_MASK = (1L << NUM_X_BITS) - 1L; + private static final long Y_MASK = (1L << NUM_Y_BITS) - 1L; + private static final long Z_MASK = (1L << NUM_Z_BITS) - 1L; + + /* + * net.minecraft.util.math.MathHelper#log2(int) + */ + private static int log2(int value) { + return log2DeBruijn(value) - (isPowerOfTwo(value) ? 0 : 1); + } + + /* + * net.minecraft.util.math.MathHelper#log2DeBruijn(int) + */ + private static int log2DeBruijn(int value) { + value = isPowerOfTwo(value) ? value : smallestEncompassingPowerOfTwo(value); + return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int)((long)value * 125613361L >> 27) & 31]; + } + + /* + * net.minecraft.util.math.MathHelper#smallestEncompassingPowerOfTwo(int) + */ + private static int smallestEncompassingPowerOfTwo(int value) { + int i = value - 1; + i = i | i >> 1; + i = i | i >> 2; + i = i | i >> 4; + i = i | i >> 8; + i = i | i >> 16; + return i + 1; + } + + public static long toLong(BlockLocation location) { + return ((long)location.getX() & X_MASK) << X_SHIFT | + ((long)location.getY() & Y_MASK) << Y_SHIFT | + ((long)location.getZ() & Z_MASK); + } + + public static BlockLocation fromLong(long value) { + BlockLocation location = BlockLocation.ZERO(); + fromLong(value, location); + return location; + } + + public static void fromLong(long value, BlockLocation location) { + location.setX((int)(value << 64 - X_SHIFT - NUM_X_BITS >> 64 - NUM_X_BITS)); + location.setY((int)(value << 64 - Y_SHIFT - NUM_Y_BITS >> 64 - NUM_Y_BITS)); + location.setZ((int)(value << 64 - NUM_Z_BITS >> 64 - NUM_Z_BITS)); + } +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/Mapper.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/Mapper.java new file mode 100644 index 0000000..167dc43 --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/Mapper.java @@ -0,0 +1,5 @@ +package mc.core.network.proto_1_12_2.serializers; + +public interface Mapper { + T mapping(F fromObject); +} diff --git a/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/TextMapper.java b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/TextMapper.java new file mode 100644 index 0000000..2590d2f --- /dev/null +++ b/proto_1.12.2/src/main/java/mc/core/network/proto_1_12_2/serializers/TextMapper.java @@ -0,0 +1,55 @@ +/* + * DmitriyMX + * 2018-06-11 + */ +package mc.core.network.proto_1_12_2.serializers; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Getter; +import mc.core.text.Text; + +public class TextMapper implements Mapper { + @Getter + private static TextMapper instance = new TextMapper(); + + private JsonObject serialize(Text text) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("text", text.getContent()); + + if (text.getColor() != null) { + jsonObject.addProperty("color", text.getColor().getName()); + } + + if (text.getStyle() != null) { + if (text.getStyle().getBold().isPresent()) { + jsonObject.addProperty("bold", text.getStyle().getBold().get()); + } + if (text.getStyle().getItalic().isPresent()) { + jsonObject.addProperty("italic", text.getStyle().getItalic().get()); + } + if (text.getStyle().getObfuscated().isPresent()) { + jsonObject.addProperty("obfuscated", text.getStyle().getObfuscated().get()); + } + if (text.getStyle().getStrikethrough().isPresent()) { + jsonObject.addProperty("strikethrough", text.getStyle().getStrikethrough().get()); + } + if (text.getStyle().getUnderline().isPresent()) { + jsonObject.addProperty("underlined", text.getStyle().getUnderline().get()); + } + } + + if (text.getChildren() != null) { + JsonArray extra = new JsonArray(); + text.getChildren().forEach(child -> extra.add(serialize(child))); + jsonObject.add("extra", extra); + } + + return jsonObject; + } + + @Override + public String mapping(Text fromObject) { + return serialize(fromObject).toString(); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStream.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStream.java new file mode 100644 index 0000000..4b0c90a --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStream.java @@ -0,0 +1,84 @@ +package mc.core.network.proto_1_12_2; + +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +@Slf4j +public class ByteArrayInputNetStream extends NetInputStream_p340 { + private ByteArrayInputStream bais; + + public ByteArrayInputNetStream(byte[] buff) { + bais = new ByteArrayInputStream(buff); + } + + @Override + public boolean readBoolean() { + return readByte() != 0; + } + + @Override + public byte readByte() { + return (byte) bais.read(); + } + + @Override + public int readBytes(byte[] buffer, int offset, int length) { + try { + int read = bais.read(buffer, offset, length); + if (read < length) { + throw new IOException("not enough data"); + } + return read; + } catch (IOException e) { + log.error("", e); + return -1; + } + } + + @Override + public int readUnsignedByte() { + return bais.read() & 0xFF; + } + + @Override + public int readUnsignedShort() { + throw new UnsupportedOperationException(); + } + + @Override + public short readShort() { + throw new UnsupportedOperationException(); + } + + @Override + public int readInt() { + int ch1 = bais.read(); + int ch2 = bais.read(); + int ch3 = bais.read(); + int ch4 = bais.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) return 0; + return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4)); + } + + @Override + public long readLong() { + throw new UnsupportedOperationException(); + } + + @Override + public float readFloat() { + return Float.intBitsToFloat(readInt()); + } + + @Override + public double readDouble() { + throw new UnsupportedOperationException(); + } + + @Override + public void skipBytes(int count) { + throw new UnsupportedOperationException(); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStreamTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStreamTest.java new file mode 100644 index 0000000..7c3c32a --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayInputNetStreamTest.java @@ -0,0 +1,135 @@ +package mc.core.network.proto_1_12_2; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ByteArrayInputNetStreamTest { + private Random random; + + @BeforeEach + void before() { + random = new Random(System.currentTimeMillis()); + } + + @Test + void testReadBoolean() { + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(true); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertTrue(byteArrayInputNetStream.readBoolean()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(false); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertFalse(byteArrayInputNetStream.readBoolean()); + } + + @Test + void testReadByte() throws IOException { + final byte[] bytes = new byte[1]; + random.nextBytes(bytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeByte(bytes[0]); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(bytes[0], byteArrayInputNetStream.readByte()); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(bytes[0], byteArrayInputNetStream.read()); + } + + @Test + void testReadBytes() throws IOException { + final byte[] expectedBytes = new byte[10]; + random.nextBytes(expectedBytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBytes(expectedBytes); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + byte[] actualBytes = new byte[10]; + byteArrayInputNetStream.readBytes(actualBytes); + + assertArrayEquals(expectedBytes, actualBytes); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + actualBytes = new byte[10]; + int r = byteArrayInputNetStream.read(actualBytes); + + assertArrayEquals(expectedBytes, actualBytes); + assertEquals(expectedBytes.length, r); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + actualBytes = new byte[10]; + byteArrayInputNetStream.readBytes(actualBytes, 2, 5); + byte[] nibbleExpectedBytes = new byte[10]; + System.arraycopy(expectedBytes, 0, nibbleExpectedBytes, 2, 5); + + assertArrayEquals(nibbleExpectedBytes, actualBytes); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + actualBytes = new byte[10]; + r = byteArrayInputNetStream.read(actualBytes, 2, 5); + nibbleExpectedBytes = new byte[10]; + System.arraycopy(expectedBytes, 0, nibbleExpectedBytes, 2, 5); + + assertArrayEquals(nibbleExpectedBytes, actualBytes); + assertEquals(5, r); + } + + @Test + void testReadUnsignedByte() { + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(30); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(30, byteArrayInputNetStream.readUnsignedByte()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(130); + + byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(130, byteArrayInputNetStream.readUnsignedByte()); + } + + @Test + void testReadInt() { + final int integerDig = random.nextInt(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeInt(integerDig); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(integerDig, byteArrayInputNetStream.readInt()); + } + + @Test + void readFloat() { + final float floatDig = random.nextFloat(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeFloat(floatDig); + + ByteArrayInputNetStream byteArrayInputNetStream = new ByteArrayInputNetStream(byteArrayOutputNetStream.toByteArray()); + + assertEquals(floatDig, byteArrayInputNetStream.readFloat()); + } +} \ No newline at end of file diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStreamTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStreamTest.java new file mode 100644 index 0000000..3d34bb1 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/ByteArrayOutputNetStreamTest.java @@ -0,0 +1,259 @@ +package mc.core.network.proto_1_12_2; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class ByteArrayOutputNetStreamTest { + private Random random; + + @BeforeEach + void before() { + random = new Random(System.currentTimeMillis()); + } + + @Test + void testWriteBoolean() { + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(true); + + assertArrayEquals(new byte[]{0x01}, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBoolean(false); + + assertArrayEquals(new byte[]{0x00}, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteByte() throws IOException { + final byte[] bytes = new byte[1]; + random.nextBytes(bytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeByte(bytes[0]); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.write(bytes[0]); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteUnsignedByte() { + final byte[] bytes = new byte[1]; + random.nextBytes(bytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(bytes[0]); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUnsignedByte(0xFF); + + assertArrayEquals(new byte[]{(byte) 0xFF}, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteBytes() throws IOException { + final byte[] expectedBytes = new byte[10]; + random.nextBytes(expectedBytes); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBytes(expectedBytes); + + assertArrayEquals(expectedBytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.write(expectedBytes); + + assertArrayEquals(expectedBytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeBytes(expectedBytes, 2, 5); + byte[] nibbleExpectedBytes = new byte[5]; + System.arraycopy(expectedBytes, 2, nibbleExpectedBytes, 0, 5); + + assertArrayEquals(nibbleExpectedBytes, byteArrayOutputNetStream.toByteArray()); + + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.write(expectedBytes, 2, 5); + nibbleExpectedBytes = new byte[5]; + System.arraycopy(expectedBytes, 2, nibbleExpectedBytes, 0, 5); + + assertArrayEquals(nibbleExpectedBytes, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteShort() { + int smallInt; + do { + smallInt = random.nextInt(); + } while (smallInt > Short.MAX_VALUE || smallInt < Short.MIN_VALUE); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeShort(smallInt); + + assertArrayEquals(new byte[]{ (byte) (smallInt >>> 8), + (byte) smallInt }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteInt() { + final int integerDig = random.nextInt(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeInt(integerDig); + + assertArrayEquals(new byte[]{ (byte) ((integerDig >>> 24) & 0xFF), + (byte) ((integerDig >>> 16) & 0xFF), + (byte) ((integerDig >>> 8) & 0xFF), + (byte) (integerDig & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteLong() { + final long longDig = random.nextLong(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeLong(longDig); + + assertArrayEquals(new byte[]{ (byte) ((longDig >>> 56) & 0xFF), + (byte) ((longDig >>> 48) & 0xFF), + (byte) ((longDig >>> 40) & 0xFF), + (byte) ((longDig >>> 32) & 0xFF), + (byte) ((longDig >>> 24) & 0xFF), + (byte) ((longDig >>> 16) & 0xFF), + (byte) ((longDig >>> 8) & 0xFF), + (byte) (longDig & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteFloat() { + final float floatDig = random.nextFloat(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeFloat(floatDig); + final int floatBits = Float.floatToIntBits(floatDig); + + assertArrayEquals(new byte[]{ (byte) ((floatBits >>> 24) & 0xFF), + (byte) ((floatBits >>> 16) & 0xFF), + (byte) ((floatBits >>> 8) & 0xFF), + (byte) (floatBits & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteDouble() { + final double doubleDig = random.nextDouble(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeDouble(doubleDig); + final long doubleBits = Double.doubleToLongBits(doubleDig); + + assertArrayEquals(new byte[]{ (byte) ((doubleBits >>> 56) & 0xFF), + (byte) ((doubleBits >>> 48) & 0xFF), + (byte) ((doubleBits >>> 40) & 0xFF), + (byte) ((doubleBits >>> 32) & 0xFF), + (byte) ((doubleBits >>> 24) & 0xFF), + (byte) ((doubleBits >>> 16) & 0xFF), + (byte) ((doubleBits >>> 8) & 0xFF), + (byte) (doubleBits & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteVarInt() { + final int b1Int = 120; + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b1Int); + + assertArrayEquals(new byte[]{ 0x78 }, + byteArrayOutputNetStream.toByteArray()); + + final int b2Int = 12000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b2Int); + + assertArrayEquals(new byte[]{ (byte) 0xE0, 0x5D }, + byteArrayOutputNetStream.toByteArray()); + + final int b3Int = 120000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b3Int); + + assertArrayEquals(new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }, + byteArrayOutputNetStream.toByteArray()); + + final int b4Int = 120000000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b4Int); + + assertArrayEquals(new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }, + byteArrayOutputNetStream.toByteArray()); + + final int b5Int = 1200000000; + byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeVarInt(b5Int); + + assertArrayEquals(new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 }, + byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteString() { + final String string = "Hello? Есть тут кто?"; + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeString(string); + + final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8); + final byte[] bytes = new byte[strBytes.length + 1]; + bytes[0] = (byte) string.length(); // здесь считается, что размер поместится в один байт + System.arraycopy(strBytes, 0, bytes, 1, strBytes.length); + + assertArrayEquals(bytes, byteArrayOutputNetStream.toByteArray()); + } + + @Test + void testWriteUUID() { + final UUID uuid = UUID.randomUUID(); + + ByteArrayOutputNetStream byteArrayOutputNetStream = new ByteArrayOutputNetStream(); + byteArrayOutputNetStream.writeUUID(uuid); + + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + + assertArrayEquals(new byte[]{ (byte) ((mostSignificantBits >>> 56) & 0xFF), + (byte) ((mostSignificantBits >>> 48) & 0xFF), + (byte) ((mostSignificantBits >>> 40) & 0xFF), + (byte) ((mostSignificantBits >>> 32) & 0xFF), + (byte) ((mostSignificantBits >>> 24) & 0xFF), + (byte) ((mostSignificantBits >>> 16) & 0xFF), + (byte) ((mostSignificantBits >>> 8) & 0xFF), + (byte) (mostSignificantBits & 0xFF), + + (byte) ((leastSignificantBits >>> 56) & 0xFF), + (byte) ((leastSignificantBits >>> 48) & 0xFF), + (byte) ((leastSignificantBits >>> 40) & 0xFF), + (byte) ((leastSignificantBits >>> 32) & 0xFF), + (byte) ((leastSignificantBits >>> 24) & 0xFF), + (byte) ((leastSignificantBits >>> 16) & 0xFF), + (byte) ((leastSignificantBits >>> 8) & 0xFF), + (byte) (leastSignificantBits & 0xFF) }, + byteArrayOutputNetStream.toByteArray()); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacketTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacketTest.java new file mode 100644 index 0000000..5f4e772 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/ChunkDataPacketTest.java @@ -0,0 +1,312 @@ +package mc.core.network.proto_1_12_2.packets; + +import com.flowpowered.nbt.*; +import javafx.util.Pair; +import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream; +import mc.core.network.proto_1_12_2.packets.DumbChunkData.DumbChunkSection; +import mc.core.world.Biome; +import mc.core.world.block.*; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.*; + +class ChunkDataPacketTest { + private static List> listOfParams; + + private static DumbChunkData createExpectedData(String xz) throws IOException { + InputStream inputStream = ChunkDataPacketTest.class.getResourceAsStream(String.format("ChunkDataPacket%s.bin", xz)); + assertNotNull(inputStream); + return DumbChunkData.ReadFromNetInputStream(IOUtils.toByteArray(inputStream)); + } + + private static Block createChestBlock00(BlockType type, int x, int y, int z, int height) { + final BlockLocation location = new BlockLocation(x, y, z); + + final CompoundMap compoundMap = new CompoundMap(); + compoundMap.put(new IntTag("x", x)); + compoundMap.put(new IntTag("y", (height << 4) + y)); + compoundMap.put(new IntTag("z", z)); + compoundMap.put(new StringTag("id", type.getNamedId())); + final CompoundTag compoundTag = new CompoundTag("", compoundMap); + + return new AbstractBlock(type) { + @Override + public BlockLocation getLocation() { + return location; + } + + @Override + public CompoundTag getNBTData() { + return compoundTag; + } + }; + } + + private static ChunkSection createChunkSection00(int height) { + final ChunkSection chunkSection = mock(ChunkSection.class); + when(chunkSection.getSkyLight(anyInt(), anyInt(), anyInt())).thenReturn(0); + when(chunkSection.getY()).thenReturn(height); + + if (height == 0) { + when(chunkSection.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + final int x = (int) args[0]; + final int y = (int) args[1]; + final int z = (int) args[2]; + + BlockFactory blockFactory = new BlockFactory(); + + if (y == 0) { + // @formatter:off + if (x == 0 && z == 0) return blockFactory.create(BlockType.STONE, x, y, z); + else if (x == 15 && z == 0) return blockFactory.create(BlockType.GRANITE, x, y, z); + else if (x == 0 && z == 15) return blockFactory.create(BlockType.POLISHED_GRANITE, x, y, z); + else if (x == 15 && z == 15) return blockFactory.create(BlockType.DIORITE, x, y, z); + else return blockFactory.create(BlockType.BEDROCK, x, y, z); + // @formatter:on + } else { + return blockFactory.create(BlockType.STONE, x, y, z); + } + }); + } else { + when(chunkSection.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + final int x = (int) args[0]; + final int y = (int) args[1]; + final int z = (int) args[2]; + + BlockFactory blockFactory = new BlockFactory(); + + // @formatter:off + if (y == 0) return blockFactory.create(BlockType.DIRT, x, y, z); + else if (y == 1) return blockFactory.create(BlockType.GRASS, x, y, z); + else if (y == 2) { + if ((x == 2 || x == 4 || x == 5) && z == 1) + return createChestBlock00(BlockType.CHEST_NORTH, x, y, z, height); + else if ((x == 2 || x == 3 || x == 5) && z == 6) + return createChestBlock00(BlockType.CHEST_SOUTH, x, y, z, height); + else if (x == 1 && (z == 2 || z == 3 || z == 5)) + return createChestBlock00(BlockType.CHEST_WEST, x, y, z, height); + else if (x == 6 && (z == 2 || z == 4 || z == 5)) + return createChestBlock00(BlockType.CHEST_EAST, x, y, z, height); + else + return blockFactory.create(BlockType.AIR, x, y, z); + } + else return blockFactory.create(BlockType.AIR, x, y, z); + // @formatter:on + }); + } + + return chunkSection; + } + + private static Chunk createMockChunk00() { + final ChunkSection chunkSection0 = createChunkSection00(0); + final ChunkSection chunkSection1 = createChunkSection00(1); + + final Chunk chunk = mock(Chunk.class); + when(chunk.getX()).thenReturn(0); + when(chunk.getZ()).thenReturn(0); + when(chunk.getBiome(anyInt(), anyInt())).thenReturn(Biome.PLAINS); + when(chunk.getChunkSection(0)).thenReturn(chunkSection0); + when(chunk.getChunkSection(1)).thenReturn(chunkSection1); + + return chunk; + } + + private static ChunkSection createChunkSection01() { + final ChunkSection chunkSection = mock(ChunkSection.class); + when(chunkSection.getSkyLight(anyInt(), anyInt(), anyInt())).thenReturn(0); + when(chunkSection.getY()).thenReturn(0); + + final List types = Arrays.asList( + BlockType.CLAY, + BlockType.ORE_REDSTONE, + BlockType.ORE_DIAMOND, + BlockType.OBSIDIAN, + BlockType.STONE_MOSS, + BlockType.SANDSTONE, + BlockType.ORE_LAPIS, + BlockType.WOOD_JUNGLE, + BlockType.WOOD_BIRCH, + BlockType.WOOD_SPRUCE, + BlockType.WOOD_OAK, + BlockType.ORE_COAL, + BlockType.ORE_IRON, + BlockType.ORE_GOLD, + BlockType.GRAVEL, + BlockType.SAND + ); + + when(chunkSection.getBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + final int x = (int) args[0]; + final int y = (int) args[1]; + final int z = (int) args[2]; + + BlockFactory blockFactory = new BlockFactory(); + + if (y == 0) { + // @formatter:off + if (x == 0 && z == 0) return blockFactory.create(BlockType.STONE, x, y, z); + else if (x == 15 && z == 0) return blockFactory.create(BlockType.GRANITE, x, y, z); + else if (x == 0 && z == 15) return blockFactory.create(BlockType.POLISHED_GRANITE, x, y, z); + else if (x == 15 && z == 15) return blockFactory.create(BlockType.DIORITE, x, y, z); + else return blockFactory.create(BlockType.BEDROCK, x, y, z); + // @formatter:on + } else if (y == 1) { + return blockFactory.create(types.get(x), x, y, z); + } else { + return blockFactory.create(BlockType.AIR, x, y, z); + } + }); + + return chunkSection; + } + + private static Chunk createMockChunk01() { + final ChunkSection chunkSection0 = createChunkSection01(); + + final Chunk chunk = mock(Chunk.class); + when(chunk.getX()).thenReturn(0); + when(chunk.getZ()).thenReturn(1); + when(chunk.getBiome(anyInt(), anyInt())).thenReturn(Biome.PLAINS); + when(chunk.getChunkSection(0)).thenReturn(chunkSection0); + + return chunk; + } + + private static void verifyMock(Chunk chunk) { + verify(chunk, atLeast(1)).getX(); + verify(chunk, atLeast(1)).getZ(); + verify(chunk, times(256)).getBiome(anyInt(), anyInt()); + verify(chunk, atLeast(2)).getChunkSection(anyInt()); + } + + private static DumbChunkData createActualData(Chunk chunk) { + ChunkDataPacket packet = new ChunkDataPacket(); + packet.setX(chunk.getX()); + packet.setZ(chunk.getZ()); + packet.setChunk(chunk); + packet.setInitChunk(true); + + ByteArrayOutputNetStream netStream = new ByteArrayOutputNetStream(); + packet.writeSelf(netStream); + + verifyMock(chunk); + + return DumbChunkData.ReadFromNetInputStream(netStream.toByteArray()); + } + + @BeforeAll + static void beforeClassTest() throws IOException { + listOfParams = Arrays.asList( + new Pair<>(createExpectedData("00"), createActualData(createMockChunk00())), + new Pair<>(createExpectedData("01"), createActualData(createMockChunk01())) + ); + } + + private static Stream streamArguments() { + return listOfParams.stream().map(pair -> Arguments.of(pair.getKey(), pair.getValue())); + } + + @DisplayName("testGeneral") + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("streamArguments") + void testGeneral(DumbChunkData expected, DumbChunkData actual) { + assertEquals(expected.getX(), actual.getX(), "X coord not equals"); + assertEquals(expected.getZ(), actual.getZ(), "Z coord not equals"); + assertEquals(expected.isInitChunk(), actual.isInitChunk(), "Flag init chunk not equals"); + assertEquals(expected.getBitMask(), actual.getBitMask(), "BitMask not equals"); + assertArrayEquals(expected.getBiomes(), actual.getBiomes(), "Biomes not equals"); + } + + @DisplayName("testNBT") + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("streamArguments") + void testNBT(DumbChunkData expected, DumbChunkData actual) { + assertEquals(expected.getNumberNBT(), actual.getNumberNBT()); + assertEquals(expected.getNbt().size(), actual.getNbt().size()); + + for (Tag tag : actual.getNbt()) { + assertTrue(expected.getNbt().contains(tag)); + } + } + + @DisplayName("testData (disabled light test)") + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("streamArguments") + void testData(DumbChunkData expected, DumbChunkData actual) { + assertEquals(expected.getData().length, actual.getData().length); + + for (int numberSection = 0; numberSection < expected.getData().length; numberSection++) { + final DumbChunkSection expectedSection = expected.getData()[numberSection]; + final DumbChunkSection actualSection = actual.getData()[numberSection]; + + // Palette + testPalette(expectedSection, actualSection, numberSection); + + // Data + testDataBlock(expectedSection, actualSection, numberSection); + + // Block and Sky light + // DISABLE // + //testLight(expectedSection, actualSection, numberSection); + } + } + + private void testPalette(DumbChunkSection expected, DumbChunkSection actual, int numberSection) { + assertEquals(expected.getBitsPerBlock(), actual.getBitsPerBlock()); + + if (expected.getPalette().size() > actual.getPalette().size()) { + for (int j = 0; j < actual.getPalette().size(); j++) { + assertTrue(expected.getPalette().contains( + actual.getPalette().get(j) + ), String.format("[%d] Palette not contains %s", numberSection, actual.getPalette().get(j))); + } + } else { + for (int j = 0; j < expected.getPalette().size(); j++) { + assertTrue(actual.getPalette().contains( + expected.getPalette().get(j) + ), String.format("[%d] Palette not contains %s", numberSection, actual.getPalette().get(j))); + } + } + } + + private void testDataBlock(DumbChunkSection expected, DumbChunkSection actual, int numberSection) { + assertEquals(expected.getData().size(), actual.getData().size()); + + for (int j = 0; j < expected.getData().size(); j++) { + assertEquals( + expected.getData().get(j), + actual.getData().get(j), + String.format("[%d] Data (blocks)", numberSection) + ); + } + } + + private void testLight(DumbChunkSection expected, DumbChunkSection actual, int numberSection) { + // Block light + assertArrayEquals(expected.getBlockLight(), actual.getBlockLight(), + String.format("[%d] Block light", numberSection)); + + // Sky light + assertArrayEquals(expected.getSkyLight(), actual.getSkyLight(), + String.format("[%d] Sky light", numberSection)); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/DumbChunkData.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/DumbChunkData.java new file mode 100644 index 0000000..bd472c8 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/DumbChunkData.java @@ -0,0 +1,133 @@ +package mc.core.network.proto_1_12_2.packets; + +import com.flowpowered.nbt.Tag; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mc.core.network.proto_1_12_2.ByteArrayInputNetStream; +import mc.core.world.block.BlockType; + +import java.nio.ByteBuffer; +import java.nio.LongBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +class DumbChunkData { + private int x; + private int z; + private boolean initChunk; + private int bitMask; + + private int sizeOfData; + private DumbChunkSection[] data; + private byte[] biomes; + + private int numberNBT; + private List> nbt; + + private static BlockType deserializeBlockState(int blockState) { + return BlockType.getByIdMeta(blockState >> 4, blockState & 0x0F); + } + + static DumbChunkData ReadFromNetInputStream(byte[] bytes) { + ByteArrayInputNetStream netStream = new ByteArrayInputNetStream(bytes); + + DumbChunkData dumbChunkData = new DumbChunkData(); + + dumbChunkData.x = netStream.readInt(); + dumbChunkData.z = netStream.readInt(); + dumbChunkData.initChunk = netStream.readBoolean(); + + dumbChunkData.bitMask = netStream.readVarInt(); + int countOfSections = 0; + for (int shift = 0; shift < 8; shift++) { + countOfSections += ((dumbChunkData.bitMask >> shift) & 0x01) > 0 ? 1 : 0; + } + + dumbChunkData.sizeOfData = netStream.readVarInt(); + + dumbChunkData.data = new DumbChunkSection[countOfSections]; + for (int c = 0; c < countOfSections; c++) { + DumbChunkSection dumbChunkSection = new DumbChunkSection(); + + dumbChunkSection.bitsPerBlock = netStream.readUnsignedByte(); + int sizePalette = netStream.readVarInt(); + dumbChunkSection.palette = new ArrayList<>(sizePalette); + for (int i = 0; i < sizePalette; i++) { + dumbChunkSection.palette.add(deserializeBlockState(netStream.readVarInt())); + } + + final byte[] rawData = new byte[netStream.readVarInt() * 8]; + netStream.readBytes(rawData); + LongBuffer data = ByteBuffer.wrap(rawData).asLongBuffer(); + + final int bitMask = (1 << dumbChunkSection.bitsPerBlock) - 1; + dumbChunkSection.data = new ArrayList<>(4096); + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + final int blockNumber = (((y << 4) + z) << 4) + x; + final int startLong = ( blockNumber * dumbChunkSection.bitsPerBlock ) / 64; + final int startOffset = ( blockNumber * dumbChunkSection.bitsPerBlock ) % 64; + final int endLong = ((blockNumber + 1) * dumbChunkSection.bitsPerBlock - 1) / 64; + + int idxBlock; + if (startLong == endLong) { + idxBlock = (int)(data.get(startLong) >> startOffset); + } else { + int endOffset = 64 - startOffset; + long mask = (1 << endOffset) - 1; + idxBlock = (int)(((data.get(startLong) >> startOffset) & mask) | data.get(endLong) << endOffset); + } + + dumbChunkSection.data.add(dumbChunkSection.palette.get(idxBlock & bitMask)); + } + } + } + + dumbChunkSection.blockLight = new byte[2048]; + netStream.readBytes(dumbChunkSection.blockLight); + dumbChunkSection.skyLight = new byte[2048]; + netStream.readBytes(dumbChunkSection.skyLight); + + dumbChunkData.data[c] = dumbChunkSection; + } + + dumbChunkData.biomes = new byte[256]; + netStream.readBytes(dumbChunkData.biomes); + + dumbChunkData.numberNBT = netStream.readVarInt(); + if (dumbChunkData.numberNBT > 0) { + dumbChunkData.nbt = new ArrayList<>(dumbChunkData.numberNBT); + for (int i = 0; i < dumbChunkData.numberNBT; i++) { + dumbChunkData.nbt.add(netStream.readNBT()); + } + } else { + dumbChunkData.nbt = Collections.emptyList(); + } + + return dumbChunkData; + } + + @Override + public String toString() { + return "DumbChunkData{" + + "x=" + x + + ", z=" + z + + '}'; + } + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @Getter + static class DumbChunkSection { + private int bitsPerBlock; + private List palette; + + private List data; + private byte[] blockLight; + private byte[] skyLight; + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacketTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacketTest.java new file mode 100644 index 0000000..62d70c7 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/packets/PlayerAbilitiesPacketTest.java @@ -0,0 +1,41 @@ +package mc.core.network.proto_1_12_2.packets; + +import mc.core.network.proto_1_12_2.ByteArrayInputNetStream; +import mc.core.network.proto_1_12_2.ByteArrayOutputNetStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PlayerAbilitiesPacketTest { + private Random rnd = new Random(); + private PlayerAbilitiesPacket packet; + + @BeforeEach + void before() { + packet = new PlayerAbilitiesPacket(); + packet.setGodMode(rnd.nextBoolean()); + packet.setFlying(rnd.nextBoolean()); + packet.setCanFly(rnd.nextBoolean()); + packet.setInstantDestroyBlocks(rnd.nextBoolean()); + packet.setFlyingSpeed(rnd.nextFloat()); + } + + @Test + void writePacket() { + ByteArrayOutputNetStream netOutputStream = new ByteArrayOutputNetStream(); + packet.writeSelf(netOutputStream); + + ByteArrayInputNetStream netInputStream = new ByteArrayInputNetStream(netOutputStream.toByteArray()); + PlayerAbilitiesPacket outPkt = new PlayerAbilitiesPacket(); + outPkt.readSelf(netInputStream); + + assertEquals(packet.isGodMode(), outPkt.isGodMode(), "god mode"); + assertEquals(packet.isFlying(), outPkt.isFlying(), "flying"); + assertEquals(packet.isCanFly(), outPkt.isCanFly(), "can fly"); + assertEquals(packet.isInstantDestroyBlocks(), outPkt.isInstantDestroyBlocks(), "instant destroy block"); + assertEquals(packet.getFlyingSpeed(), outPkt.getFlyingSpeed(), 0.00001f, "flying speed"); + } +} diff --git a/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializerTest.java b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializerTest.java new file mode 100644 index 0000000..dda7500 --- /dev/null +++ b/proto_1.12.2/src/test/java/mc/core/network/proto_1_12_2/serializers/BlockLocationSerializerTest.java @@ -0,0 +1,32 @@ +package mc.core.network.proto_1_12_2.serializers; + +import mc.core.world.block.BlockLocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BlockLocationSerializerTest { + private static final ThreadLocalRandom rnd = ThreadLocalRandom.current(); + private static final int minI = 0, maxI = 10; + private int x, y, z; + + @BeforeEach + void before() { + x = rnd.nextInt(minI, maxI); + y = rnd.nextInt(minI, maxI); + z = rnd.nextInt(minI, maxI); + } + + @Test + void serialize() { + BlockLocation location = new BlockLocation(x, y, z); + final long serializedCoords = BlockLocationSerializer.toLong(location); + + BlockLocation deserLoc = BlockLocationSerializer.fromLong(serializedCoords); + + assertEquals(location, deserLoc); + } +} \ No newline at end of file diff --git a/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket00.bin b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket00.bin new file mode 100644 index 0000000..6125544 Binary files /dev/null and b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket00.bin differ diff --git a/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket01.bin b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket01.bin new file mode 100644 index 0000000..6c12d37 Binary files /dev/null and b/proto_1.12.2/src/test/resources/mc/core/network/proto_1_12_2/packets/ChunkDataPacket01.bin differ diff --git a/proto_1.12.2_netty/build.gradle b/proto_1.12.2_netty/build.gradle new file mode 100644 index 0000000..0e9cbf0 --- /dev/null +++ b/proto_1.12.2_netty/build.gradle @@ -0,0 +1,13 @@ +version '1.0-SNAPSHOT' + +ext { + netty_version = '4.1.22.Final' +} + +dependencies { + /* Protocol 1.12.2 */ + compile_excludeCopy project(':proto_1.12.2') + + /* Netty */ + compile (group: 'io.netty', name: 'netty-all', version: netty_version) +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/KeepAliveThread.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/KeepAliveThread.java new file mode 100644 index 0000000..97fd7c4 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/KeepAliveThread.java @@ -0,0 +1,48 @@ +package mc.core.network.proto_1_12_2.netty; + +import lombok.Setter; +import mc.core.network.proto_1_12_2.packets.KeepAlivePacket; +import mc.core.player.PlayerManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class KeepAliveThread extends Thread { + private final Object lock = new Object(); + @Autowired + private PlayerManager playerManager; + @Setter + private int interval = 10; + + public void notifyLock() { + synchronized (lock) { + lock.notify(); + } + } + + @Override + public void run() { + final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(); + + while (!Thread.currentThread().isInterrupted()) { + while(playerManager.getCountPlayers() == 0) { + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { + return; + } + } + } + + keepAlivePacket.setPayload(System.currentTimeMillis()); + playerManager.getBroadcastChannel().writeAndFlush(keepAlivePacket); + + try { + Thread.sleep(interval); + } catch (InterruptedException e) { + return; + } + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/NettyServer.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/NettyServer.java new file mode 100644 index 0000000..d17cba5 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/NettyServer.java @@ -0,0 +1,92 @@ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.util.AttributeKey; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.EventBus; +import mc.core.network.Server; +import mc.core.network.StartServerException; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.packets.StatusResponsePacket; +import mc.core.player.Player; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +public class NettyServer implements Server { + public static final AttributeKey ATTR_STATE = AttributeKey.newInstance("ATTR_STATE"); + public static final AttributeKey ATTR_PLAYER = AttributeKey.newInstance("ATTR_PLAYER"); + + @Autowired + private ApplicationContext context; + @Autowired + private KeepAliveThread keepAliveThread; + @Setter + private String host; + @Setter + private int port; + @Setter + private int workerGroupCount = 0; + private EventLoopGroup bossGroup, workerGroup; + + private ChannelInitializer buildChannelInitializer() { + return new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) { + Map beans = context.getBeansOfType(ChannelHandler.class); + beans.forEach(socketChannel.pipeline()::addLast); + } + }; + } + + private ServerBootstrap buildServerBootstrap() { + ServerBootstrap bootstrap = new ServerBootstrap(); + + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(buildChannelInitializer()); + + return bootstrap; + } + + @Override + public void start() throws StartServerException { + log.info("Use protocol {}", StatusResponsePacket.NAME); + + EventBus.getInstance().registerSubscribes(new PlayerEventListener()); + + bossGroup = new NioEventLoopGroup(1); + workerGroup = new NioEventLoopGroup(workerGroupCount); + + ServerBootstrap serverBootstrap = buildServerBootstrap(); + + log.info("Start server: {}:{}", host, port); + try { + ChannelFuture channelFuture = serverBootstrap.bind(host, port).sync(); + keepAliveThread.start(); + channelFuture.channel().closeFuture().sync(); + } catch (InterruptedException e) { + throw new StartServerException(e); + } + } + + @Override + public void stop() { + log.info("Server shutdown"); + keepAliveThread.interrupt(); + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketDecoder.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketDecoder.java new file mode 100644 index 0000000..585a88d --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketDecoder.java @@ -0,0 +1,66 @@ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ReplayingDecoder; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.CSPacket; +import mc.core.network.NetInputStream; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetInputStream; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +public class PacketDecoder extends ReplayingDecoder { + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.channel().attr(ATTR_STATE).set(State.HANDSHAKE); + ctx.fireChannelActive(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.channel().attr(ATTR_STATE).set(null); + ctx.fireChannelInactive(); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + State state = ctx.channel().attr(ATTR_STATE).get(); + NetInputStream netStream = new WrapperNetInputStream(in); + + int packetSize = netStream.readVarInt(); + log.debug("Packet size: {}", packetSize); + int leftDataPacket = packetSize; + + final AtomicInteger countReadBytes = new AtomicInteger(0); + int packetId = netStream.readVarInt(countReadBytes); + String hexPacketId = Integer.toHexString(packetId).toUpperCase(); + if (hexPacketId.length() == 1) hexPacketId = "0" + hexPacketId; + log.debug("Packet id: 0x{}", hexPacketId); + leftDataPacket = leftDataPacket - countReadBytes.get(); + + Class packetClass = state.getClientSidePacket(packetId); + if (packetClass == null) { + log.warn("Unknown packet: {}:0x{}", state.name(), hexPacketId); + in.skipBytes(leftDataPacket); + } else { + netStream.setDataSize(leftDataPacket); + CSPacket packet = packetClass.newInstance(); + try { + packet.readSelf(netStream); + log.debug("Known packet: {}:{}", state.name(), packet.toString()); + out.add(packet); + } catch (Exception e) { + log.warn("Known packet: {}:{}. But throw exception. See debug log.", state.name(), packet.getClass().getSimpleName()); + log.debug("Read packet", e); + in.skipBytes(leftDataPacket); + } + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketEncoder.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketEncoder.java new file mode 100644 index 0000000..9dbc4a9 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketEncoder.java @@ -0,0 +1,41 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.NetOutputStream; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetOutputStream; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; +import static org.slf4j.helpers.MessageFormatter.format; + +@Slf4j +public class PacketEncoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, SCPacket packet, ByteBuf out) { + State state = ctx.channel().attr(ATTR_STATE).get(); + Integer id = state.getServerSidePacket(packet.getClass()); + if (id == null) { + log.error("Not defined ID packet: {}:{}", state.name(), packet.getClass()); + return; + } + + log.debug("Send {}:{}", state, packet); + + try { + NetOutputStream netStream = new WrapperNetOutputStream(out); + netStream.writeVarInt(id); + packet.writeSelf(netStream); + } catch (Throwable t) { + log.error(format("Error encoding packet {}:{}", state, packet).getMessage(), t); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketHandler.java new file mode 100644 index 0000000..01b44ca --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketHandler.java @@ -0,0 +1,76 @@ +/* + * DmitriyMX + * 2018-06-10 + */ +package mc.core.network.proto_1_12_2.netty; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import lombok.extern.slf4j.Slf4j; +import mc.core.chat.ChatProcessor; +import mc.core.network.CSPacket; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.netty.handlers.*; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +import javax.annotation.PostConstruct; +import java.util.Collection; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_PLAYER; +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +@ComponentScan("mc.core.network.proto_1_12_2.netty.handlers") +public class PacketHandler extends SimpleChannelInboundHandler { + @Autowired + private ApplicationContext applicationContext; + @Autowired + private PlayerManager playerManager; + @Autowired + private ChatProcessor chatProcessor; + private ImmutableMap> handlersMap = ImmutableMap.of(); + + @PostConstruct + public void init() { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + Collection beans1 = applicationContext.getBeansOfType(HandshakeStateHandler.class).values(); + builder.put(State.HANDSHAKE, ImmutableList.copyOf(beans1)); + + Collection beans2 = applicationContext.getBeansOfType(StatusStateHandler.class).values(); + builder.put(State.STATUS, ImmutableList.copyOf(beans2)); + + Collection beans3 = applicationContext.getBeansOfType(LoginStateHandler.class).values(); + builder.put(State.LOGIN, ImmutableList.copyOf(beans3)); + + Collection beans4 = applicationContext.getBeansOfType(PlayStateHandler.class).values(); + builder.put(State.PLAY, ImmutableList.copyOf(beans4)); + + this.handlersMap = builder.build(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + Player player = ctx.channel().attr(ATTR_PLAYER).get(); + if (player != null) { + playerManager.leftServer(player); + player.setChannel(null); + } + ctx.channel().attr(ATTR_PLAYER).set(null); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, CSPacket packet) throws Exception { + ImmutableList stateHandlers = this.handlersMap.get(ctx.channel().attr(ATTR_STATE).get()); + for (StateHandler handler : stateHandlers) { + handler.handle(ctx.channel(), packet); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketPostEncoder.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketPostEncoder.java new file mode 100644 index 0000000..7e7b848 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PacketPostEncoder.java @@ -0,0 +1,38 @@ +package mc.core.network.proto_1_12_2.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetOutputStream; + +/** + * Подсчет размера отправляемого пакета + */ +@Slf4j +public class PacketPostEncoder extends MessageToByteEncoder { + + private static int getVarIntSize(int input) { + for (int i = 1; i < 5; ++i) { + if ((input & -1 << i * 7) == 0) { + return i; + } + } + + return 5; + } + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) { + final int pktSize = msg.readableBytes(); + final int sizeOfPktSize = getVarIntSize(pktSize); + + if (sizeOfPktSize > 3) { + log.warn("unable to fit {} into {}", pktSize, 3); + } + + out.ensureWritable(sizeOfPktSize + pktSize); + (new WrapperNetOutputStream(out)).writeVarInt(pktSize); + out.writeBytes(msg); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PlayerEventListener.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PlayerEventListener.java new file mode 100644 index 0000000..5602221 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/PlayerEventListener.java @@ -0,0 +1,70 @@ +package mc.core.network.proto_1_12_2.netty; + +import lombok.extern.slf4j.Slf4j; +import mc.core.eventbus.Subscriber; +import mc.core.eventbus.events.SC_ChunkLoadEvent; +import mc.core.eventbus.events.SC_ChunkUnloadEvent; +import mc.core.eventbus.events.SC_PlayerMoveEvent; +import mc.core.network.NetChannel; +import mc.core.network.proto_1_12_2.TeleportManager; +import mc.core.network.proto_1_12_2.packets.ChunkDataPacket; +import mc.core.network.proto_1_12_2.packets.PlayerPositionAndLookPacket; +import mc.core.network.proto_1_12_2.packets.UnloadChunkPacket; +import mc.core.utils.CompactedCoords; +import mc.core.world.chunk.Chunk; + +@Slf4j +public class PlayerEventListener { + @Subscriber + public void playerMoveEventHandler(SC_PlayerMoveEvent event) { + log.debug("(SC) playerMoveEventHandler()"); + PlayerPositionAndLookPacket packet = new PlayerPositionAndLookPacket(); + packet.setLocation(event.getNewLocation()); + int tpId = TeleportManager.getInstance().append(event.getPlayer(), event.getNewLocation()); + packet.setTeleportId(tpId); + + event.getPlayer().getChannel().writeAndFlush(packet); + } + + @Subscriber + public void playerChunkLoadHandler(SC_ChunkLoadEvent event) { + if (event.getNeedLoadChunks().size() == 0) return; + + final NetChannel channel = event.getPlayer().getChannel(); + + for(Integer compressXZ : event.getNeedLoadChunks()) { + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + Chunk chunk = event.getPlayer().getWorld().getChunk(xz[0], xz[1]); + if (chunk == null) continue; + + ChunkDataPacket packet = new ChunkDataPacket(); + packet.setX(xz[0]); + packet.setZ(xz[1]); + packet.setInitChunk(true); + packet.setChunk(chunk); + + channel.write(packet); + } + + channel.flush(); + } + + @Subscriber + public void playerChunkUnloadHandler(SC_ChunkUnloadEvent event) { + if (event.getNeedUnloadChunks().size() == 0) return; + + final NetChannel channel = event.getPlayer().getChannel(); + + for(Integer compressXZ : event.getNeedUnloadChunks()) { + int[] xz = CompactedCoords.uncompressXZ(compressXZ); + + UnloadChunkPacket packet = new UnloadChunkPacket(); + packet.setX(xz[0]); + packet.setZ(xz[1]); + + channel.write(packet); + } + + channel.flush(); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/AbstractStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/AbstractStateHandler.java new file mode 100644 index 0000000..5cb51d4 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/AbstractStateHandler.java @@ -0,0 +1,54 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +import com.google.common.collect.ImmutableMap; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.CSPacket; + +import javax.annotation.PostConstruct; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.function.Function; +import java.util.stream.Stream; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +public abstract class AbstractStateHandler implements StateHandler { + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + protected @interface Handler { + } + + private ImmutableMap, Method> methods = ImmutableMap.of(); + + @PostConstruct + protected void init() { + this.methods = Stream.of(this.getClass().getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(Handler.class) + && method.getParameterCount() == 2 + && method.getParameterTypes()[0].isAssignableFrom(Channel.class) + && CSPacket.class.isAssignableFrom(method.getParameterTypes()[1])) + .collect(ImmutableMap.toImmutableMap( + method -> method.getParameterTypes()[1], + Function.identity())); + } + + @Override + public void handle(Channel channel, CSPacket packet) throws Exception { + if (this.methods.containsKey(packet.getClass())) { + this.methods.get(packet.getClass()).invoke(this, channel, packet); + } else { + log.trace("No def listener of {}:{}", + channel.attr(ATTR_STATE).get(), + packet.getClass().getSimpleName()); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeHandler.java new file mode 100644 index 0000000..3ff9af1 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeHandler.java @@ -0,0 +1,25 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.packets.HandshakePacket; +import org.springframework.stereotype.Component; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Slf4j +@Component +public class HandshakeHandler extends AbstractStateHandler implements HandshakeStateHandler { + @Handler + public void onHandshake(Channel channel, HandshakePacket packet) { + if (packet.getNextState().equals(State.UNKNOWN)) return; + //FIXME обрати внимание: хацкер может намеренно передать state:03 и тогда может начатся пиздец + log.debug("New state: {}", packet.getNextState()); + channel.attr(ATTR_STATE).set(packet.getNextState()); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeStateHandler.java new file mode 100644 index 0000000..1bf92e7 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/HandshakeStateHandler.java @@ -0,0 +1,11 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface HandshakeStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java new file mode 100644 index 0000000..c0ced7f --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginHandler.java @@ -0,0 +1,127 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.events.CS_PlayerMoveEvent; +import mc.core.network.proto_1_12_2.State; +import mc.core.network.proto_1_12_2.TeleportManager; +import mc.core.network.proto_1_12_2.netty.KeepAliveThread; +import mc.core.network.proto_1_12_2.netty.wrappers.WrapperNetChannel; +import mc.core.network.proto_1_12_2.packets.*; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.player.PlayerMode; +import mc.core.text.Text; +import mc.core.text.TextColor; +import mc.core.text.TextStyle; +import mc.core.world.World; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_PLAYER; +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_STATE; + +@Component +public class LoginHandler extends AbstractStateHandler implements LoginStateHandler { + @Autowired + private PlayerManager playerManager; + @Autowired + private KeepAliveThread keepAliveThread; + @Autowired + private World world; + + @Handler + public void onLoginStart(Channel channel, LoginStartPacket packet) { + Player player = playerManager.getPlayer(packet.getPlayerName()); + if (player != null) { + channel.writeAndFlush(new DisconnectPacket( + Text.builder("Player \"") + .append(Text.of(packet.getPlayerName(), TextColor.YELLOW)) + .append(Text.of("\" is online", TextColor.WHITE)) + .build())) + .addListener(ChannelFutureListener.CLOSE); + } else { + player = playerManager.getOfflinePlayer(packet.getPlayerName()); + + if (player == null) { + player = playerManager.createPlayer( + packet.getPlayerName(), + world.getSpawn(), + world + ); + + if (player == null) { + channel.writeAndFlush(new DisconnectPacket( + Text.of("Internal server error: can't create new player")) + ).addListener(ChannelFutureListener.CLOSE); + return; + } + } + + channel.writeAndFlush(new LoginSuccessPacket( + player.getUuid(), + packet.getPlayerName())); + channel.attr(ATTR_PLAYER).set(player); + channel.attr(ATTR_STATE).set(State.PLAY); + + // Join Game + JoinGamePacket pkt1 = new JoinGamePacket(); + pkt1.setEntityId(player.getId()); //TODO отделить системный ID от EntityID + pkt1.setMode(PlayerMode.CREATIVE); //TODO перенести в Config + pkt1.setDimension(0/*Overworld*/); //TODO перенести в World + pkt1.setDifficulty(0/*Peaceful*/); //TODO перенести в Config + pkt1.setLevelType(world.getType().getName()); + channel.write(pkt1); + + // Spawn Position + SpawnPositionPacket pkt2 = new SpawnPositionPacket(); + pkt2.setLocation(world.getSpawn()); + channel.write(pkt2); + + // Player Abilities + PlayerAbilitiesPacket pkt3 = new PlayerAbilitiesPacket(); //TODO перенести в Player + pkt3.setCanFly(true); + pkt3.setFlying(true); + pkt3.setGodMode(true); + pkt3.setInstantDestroyBlocks(true); + channel.write(pkt3); + + channel.flush(); + + // Player Position And Look + PlayerPositionAndLookPacket pkt4 = new PlayerPositionAndLookPacket(); + pkt4.setLocation(player.getLocation()); + pkt4.setTeleportId(TeleportManager.getInstance().append(player, player.getLocation())); + channel.writeAndFlush(pkt4); + + player.setChannel(new WrapperNetChannel(channel)); + + // Send items + //TODO обновление должно приходить всем игрокам на сервере + PlayerListItemPacket pkt5 = new PlayerListItemPacket(); + pkt5.setAction(PlayerListItemPacket.Action.ADD_PLAYER); + PlayerListItemPacket.PlayerData playerData = new PlayerListItemPacket.PlayerData(); + playerData.setUuid(player.getUuid()); + playerData.setName(player.getName()); + playerData.setGameMode(PlayerMode.CREATIVE); + playerData.setPing(0); + playerData.setHasDisplayName(true); + playerData.setDisplayName(Text.builder() + .append(Text.of(TextColor.RED, TextStyle.BOLD, player.getName().substring(0,1))) + .append(Text.of(TextColor.WHITE, player.getName().substring(1))) + .build() + ); + pkt5.getListPlayers().add(playerData); + channel.writeAndFlush(pkt5); + + playerManager.joinServer(player); + keepAliveThread.notifyLock(); + + CS_PlayerMoveEvent event = new CS_PlayerMoveEvent(player, player.getLocation()); + event.setNewLocation(player.getLocation()); + event.setRecalcChunk(true); + EventBus.getInstance().post(event); + } + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginStateHandler.java new file mode 100644 index 0000000..a51a626 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/LoginStateHandler.java @@ -0,0 +1,11 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface LoginStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayHandler.java new file mode 100644 index 0000000..c1675ba --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayHandler.java @@ -0,0 +1,90 @@ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import mc.core.EntityLocation; +import mc.core.chat.ChatProcessor; +import mc.core.eventbus.EventBus; +import mc.core.eventbus.events.CS_PlayerMoveEvent; +import mc.core.network.proto_1_12_2.TeleportManager; +import mc.core.network.proto_1_12_2.packets.*; +import mc.core.player.Player; +import mc.core.player.PlayerSettings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static mc.core.network.proto_1_12_2.netty.NettyServer.ATTR_PLAYER; + +@Slf4j +@Component +public class PlayHandler extends AbstractStateHandler implements PlayStateHandler { + @Autowired + private ChatProcessor chatProcessor; + + private TeleportManager teleport = TeleportManager.getInstance(); + + @Handler + public void onClientSettings(Channel channel, ClientSettingsPacket packet) { + Player player = channel.attr(ATTR_PLAYER).get(); + + player.getSettings().setLocate(packet.getLocale()); + player.getSettings().setViewDistance(packet.getViewDistance()); + player.getSettings().setChatMode(PlayerSettings.ChatMode.getById(packet.getChatMode())); + player.getSettings().setChatColors(packet.isChatColors()); + + player.getSettings().setCapeEnabled(packet.isCapeEnabled()); + player.getSettings().setJacketEnabled(packet.isJacketEnabled()); + player.getSettings().setLeftSleeveEnabled(packet.isLeftSleeveEnabled()); + player.getSettings().setRightSleeveEnabled(packet.isRightSleeveEnabled()); + player.getSettings().setLeftPantsLegEnabled(packet.isLeftPantsLegEnabled()); + player.getSettings().setRightPantsLegEnabled(packet.isRightPantsLegEnabled()); + player.getSettings().setHatEnabled(packet.isHatEnabled()); + + player.getSettings().setMainHand(PlayerSettings.Hand.getById(packet.getMainHand())); + } + + @Handler + public void onTeleportConfirm(Channel channel, TeleportConfirmPacket packet) { + this.teleport.apply(packet.getTeleportId()); + } + + @Handler + public void onPositionAndLook(Channel channel, PlayerPositionAndLookPacket packet) { + Player player = channel.attr(ATTR_PLAYER).get(); + player.getLocation().set(packet.getLocation()); + } + + @Handler + public void onChat(Channel channel, ChatMessageClientPacket packet) { + chatProcessor.process( + channel.attr(ATTR_PLAYER).get(), + packet.getMessage() + ); + } + + @Handler + public void onPlayerMove(Channel channel, PlayerPositionPacket packet) { + log.debug("(Netty) onPlayerMove()"); + Player player = channel.attr(ATTR_PLAYER).get(); + + if (player.getLocation().getX() == packet.getX() && + player.getLocation().getY() == packet.getY() && + player.getLocation().getZ() == packet.getZ()) { + return; + } + + CS_PlayerMoveEvent event = new CS_PlayerMoveEvent(player, player.getLocation().clone()); + event.setNewLocation(new EntityLocation( + packet.getX(), packet.getY(), packet.getZ(), + player.getLocation().getYaw(), + player.getLocation().getPitch() + )); + EventBus.getInstance().post(event); + } + + @Handler + public void onPlayerLook(Channel channel, PlayerLookPacket packet) { + Player player = channel.attr(ATTR_PLAYER).get(); + player.getLocation().setYawPitch(packet.getYaw(), packet.getPitch()); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayStateHandler.java new file mode 100644 index 0000000..8f05c0f --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/PlayStateHandler.java @@ -0,0 +1,11 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface PlayStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StateHandler.java new file mode 100644 index 0000000..c853d62 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StateHandler.java @@ -0,0 +1,12 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import mc.core.network.CSPacket; + +public interface StateHandler { + void handle(Channel channel, CSPacket packet) throws Exception; +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusHandler.java new file mode 100644 index 0000000..10c48fc --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusHandler.java @@ -0,0 +1,38 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +import io.netty.channel.Channel; +import mc.core.Config; +import mc.core.network.proto_1_12_2.packets.PingPacket; +import mc.core.network.proto_1_12_2.packets.StatusRequestPacket; +import mc.core.network.proto_1_12_2.packets.StatusResponsePacket; +import mc.core.player.PlayerManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class StatusHandler extends AbstractStateHandler implements StatusStateHandler { + @Autowired + private Config config; + @Autowired + private PlayerManager playerManager; + + @Handler + public void onStatusRequest(Channel channel, StatusRequestPacket packet) { + StatusResponsePacket responsePacket = new StatusResponsePacket(); + responsePacket.setMaxOnline(config.getMaxPlayers()); + responsePacket.setDescription(config.getDescriptionServer()); + responsePacket.setFaviconBase64(config.getFaviconBase64()); + responsePacket.setOnline(playerManager.getCountPlayers()); + + channel.writeAndFlush(responsePacket); + } + + @Handler + public void onPing(Channel channel, PingPacket packet) { + channel.writeAndFlush(packet); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusStateHandler.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusStateHandler.java new file mode 100644 index 0000000..38d77f2 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/handlers/StatusStateHandler.java @@ -0,0 +1,11 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.handlers; + +/** + * Marker interface + */ +public interface StatusStateHandler extends StateHandler { +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetChannel.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetChannel.java new file mode 100644 index 0000000..960297a --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetChannel.java @@ -0,0 +1,73 @@ +/* + * DmitriyMX + * 2018-06-23 + */ +package mc.core.network.proto_1_12_2.netty.wrappers; + +import io.netty.channel.Channel; +import lombok.RequiredArgsConstructor; +import mc.core.chat.MessageType; +import mc.core.network.NetChannel; +import mc.core.network.SCPacket; +import mc.core.network.proto_1_12_2.packets.ChatMessageServerPacket; +import mc.core.network.proto_1_12_2.packets.TimeUpdatePacket; +import mc.core.network.proto_1_12_2.packets.TitlePacket; +import mc.core.text.Text; +import mc.core.text.Title; + +@RequiredArgsConstructor +public class WrapperNetChannel implements NetChannel { + private final Channel channel; + + @Override + public void sendTimeUpdate(long time, long age) { + writeAndFlush(new TimeUpdatePacket(time, age)); + } + + @Override + public void sendChatMessage(Text text, MessageType type) { + writeAndFlush(new ChatMessageServerPacket(text, type)); + } + + @Override + public void sendTitle(Title title) { + Text text = title.getTitle(); + if (text != null) write(new TitlePacket(TitlePacket.Action.SET_TITLE, text)); + + text = title.getSubtitle(); + if (text != null) write(new TitlePacket(TitlePacket.Action.SET_SUBTITLE, text)); + + text = title.getTextActionBar(); + if (text != null) write(new TitlePacket(TitlePacket.Action.SET_ACTION_BAR, text)); + + Integer fadeIn = title.getFadeInTime(); + Integer stay = title.getStayTime(); + Integer fadeOut = title.getFadeOutTime(); + if (fadeIn != null && stay != null && fadeOut != null) { + write(new TitlePacket(TitlePacket.Action.SET_DISPLAY_TIME, fadeIn, stay, fadeOut)); + } + + Boolean bool = title.getHide(); + if (bool != null && bool) write(new TitlePacket(TitlePacket.Action.HIDE)); + + bool = title.getReset(); + if (bool != null && bool) write(new TitlePacket(TitlePacket.Action.RESET)); + + flush(); + } + + @Override + public void writeAndFlush(SCPacket pkt) { + channel.writeAndFlush(pkt, channel.voidPromise()); + } + + @Override + public void write(SCPacket pkt) { + channel.write(pkt); + } + + @Override + public void flush() { + channel.flush(); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetInputStream.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetInputStream.java new file mode 100644 index 0000000..4658c5b --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetInputStream.java @@ -0,0 +1,68 @@ +package mc.core.network.proto_1_12_2.netty.wrappers; + +import io.netty.buffer.ByteBuf; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.NetInputStream_p340; + +@Slf4j +@RequiredArgsConstructor +public class WrapperNetInputStream extends NetInputStream_p340 { + private final ByteBuf byteBuf; + + @Override + public boolean readBoolean() { + return byteBuf.readBoolean(); + } + + @Override + public byte readByte() { + return byteBuf.readByte(); + } + + @Override + public int readBytes(byte[] buffer, int offset, int length) { + byteBuf.readBytes(buffer, offset, length); + return length; + } + + @Override + public int readUnsignedByte() { + return byteBuf.readUnsignedByte(); + } + + @Override + public int readUnsignedShort() { + return byteBuf.readUnsignedShort(); + } + + @Override + public short readShort() { + return byteBuf.readShort(); + } + + @Override + public int readInt() { + return byteBuf.readInt(); + } + + @Override + public long readLong() { + return byteBuf.readLong(); + } + + @Override + public float readFloat() { + return byteBuf.readFloat(); + } + + @Override + public double readDouble() { + return byteBuf.readDouble(); + } + + @Override + public void skipBytes(int count) { + byteBuf.skipBytes(count); + } +} diff --git a/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetOutputStream.java b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetOutputStream.java new file mode 100644 index 0000000..b227994 --- /dev/null +++ b/proto_1.12.2_netty/src/main/java/mc/core/network/proto_1_12_2/netty/wrappers/WrapperNetOutputStream.java @@ -0,0 +1,57 @@ +package mc.core.network.proto_1_12_2.netty.wrappers; + +import io.netty.buffer.ByteBuf; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.core.network.proto_1_12_2.NetOutputStream_p340; + +@Slf4j +@RequiredArgsConstructor +public class WrapperNetOutputStream extends NetOutputStream_p340 { + private final ByteBuf byteBuf; + + @Override + public void writeBoolean(boolean value) { + byteBuf.writeBoolean(value); + } + + @Override + public void writeByte(int value) { + byteBuf.writeByte(value); + } + + @Override + public void writeUnsignedByte(int value) { + byteBuf.writeByte((byte)(value & 0xFF)); + } + + @Override + public void writeBytes(byte[] buffer, int offset, int lengtn) { + byteBuf.writeBytes(buffer, offset, lengtn); + } + + @Override + public void writeShort(int value) { + byteBuf.writeShort(value); + } + + @Override + public void writeInt(int value) { + byteBuf.writeInt(value); + } + + @Override + public void writeLong(long value) { + byteBuf.writeLong(value); + } + + @Override + public void writeFloat(float value) { + byteBuf.writeFloat(value); + } + + @Override + public void writeDouble(double value) { + byteBuf.writeDouble(value); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6e2132d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,9 @@ +rootProject.name = 'mc-server' + +include('core') // Core +include('simple_world') +include('h2_playermanager') +include('vanilla_commands') +include('proto_1.12.2') // Protocol 1.12.2 +include('proto_1.12.2_netty') // Protocol 1.12.2 (Netty impl.) +include('anvil-loader') // Vanilla world loader (aka Anvil) diff --git a/simple_world/README.MD b/simple_world/README.MD new file mode 100644 index 0000000..02871ed --- /dev/null +++ b/simple_world/README.MD @@ -0,0 +1,39 @@ +# Simple world + +Простая реализация мира + +## Spring bean + +```xml + + + + + + + + + + + + + + + + 1;BEDROCK + 2;DIRT + 1;GRASS + + + +``` + +`spawn` - точка спавна. + +При указании точки спавна, указывать шестой параметр `World` не имеет смысла, +т.к. `SimpleWorld` всё равно перезапишет этот параметр. + +`layersBlock` - слои блоков. + +В качестве значения указывается спиток строк, каждая из которых описывает слой блоков. +Формат строк такой: `кол-во_слоёв;тип_блока`. Порядок строк такой: сверху нижние слои, а снизу - верхние. diff --git a/simple_world/build.gradle b/simple_world/build.gradle new file mode 100644 index 0000000..6d1bcee --- /dev/null +++ b/simple_world/build.gradle @@ -0,0 +1,6 @@ +version '1.0-SNAPSHOT' + +dependencies { + /* Core */ + compile_excludeCopy project(':core') +} diff --git a/simple_world/src/main/java/mc/world/simple/FlatChunkProvider.java b/simple_world/src/main/java/mc/world/simple/FlatChunkProvider.java new file mode 100644 index 0000000..45a3cd0 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/FlatChunkProvider.java @@ -0,0 +1,55 @@ +package mc.world.simple; + +import mc.core.world.block.BlockType; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkProvider; +import mc.core.world.chunk.ChunkSection; + +import java.util.ArrayList; +import java.util.List; + +public class FlatChunkProvider implements ChunkProvider { + private ChunkSection chunkSection; + + public void setLayersBlockAsString(List listOfLayers) { + List layoutsBlock = new ArrayList<>(); + + for (String value : listOfLayers) { + String[] splitValue = value.split(";"); + + BlockType blockType; + try { + blockType = BlockType.valueOf(splitValue[1]); + } catch (IllegalArgumentException e) { + continue; + } + + for (int i = 0; i < Integer.parseInt(splitValue[0]); i++) { + layoutsBlock.add(blockType); + } + } + + setLayersBlock(layoutsBlock); + } + + public void setLayersBlock(List layoutsBlock) { + this.chunkSection = new SimpleChunkSection(layoutsBlock); + } + + @Override + public Chunk getChunk(int x, int z) { + Chunk chunk = new SimpleChunk(x, z); + chunk.setChunkSection(0, chunkSection); + return chunk; + } + + @Override + public void saveChunk(Chunk chunk) { + //FIXME nope... + } + + @Override + public void saveChunk(Chunk... chunks) { + //FIXME nope... + } +} diff --git a/simple_world/src/main/java/mc/world/simple/SimpleChunk.java b/simple_world/src/main/java/mc/world/simple/SimpleChunk.java new file mode 100644 index 0000000..d6a7427 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/SimpleChunk.java @@ -0,0 +1,79 @@ +package mc.world.simple; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.core.world.Biome; +import mc.core.world.block.Block; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +@Slf4j +@RequiredArgsConstructor +public class SimpleChunk implements Chunk { + @Getter + private final int x, z; + private ChunkSection chunkSection; + + @Override + public ChunkSection getChunkSection(int height) { + return chunkSection; + } + + @Override + public void setChunkSection(int height, ChunkSection chunkSection) { + this.chunkSection = chunkSection; + this.chunkSection.setParent(this); + } + + public Block getBlock(int x, int y, int z) { + return chunkSection.getBlock( + x - (x >> 4) << 4, + y - (y >> 4) << 4, + z - (z >> 4) << 4 + ); + } + + @Override + public void setBlock(Block block) { + // ignore + } + + @Override + public int getSkyLight(int x, int y, int z) { + return chunkSection.getSkyLight( + x - (x >> 4) << 4, + y - (y >> 4) << 4, + z - (z >> 4) << 4 + ); + } + + @Override + public void setSkyLight(int x, int y, int z, int lightLevel) { + // ignore + } + + @Override + public int getAddition(int x, int y, int z) { + return chunkSection.getAddition( + x - (x >> 4) << 4, + y - (y >> 4) << 4, + z - (z >> 4) << 4 + ); + } + + @Override + public void setAddition(int x, int y, int z, int value) { + // ignore + } + + @Override + public Biome getBiome(int x, int z) { + return Biome.PLAINS; + } + + @Override + public void setBiome(int x, int z, Biome biome) { + // ignore + } +} diff --git a/simple_world/src/main/java/mc/world/simple/SimpleChunkSection.java b/simple_world/src/main/java/mc/world/simple/SimpleChunkSection.java new file mode 100644 index 0000000..cde4005 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/SimpleChunkSection.java @@ -0,0 +1,70 @@ +package mc.world.simple; + +import lombok.Getter; +import lombok.Setter; +import mc.core.world.block.Block; +import mc.core.world.block.BlockFactory; +import mc.core.world.block.BlockType; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkSection; + +import java.util.List; + +public class SimpleChunkSection implements ChunkSection { + @Getter + @Setter + private Chunk parent; + private final BlockFactory blockFactory = new BlockFactory(); + private final List layersBlock; + + public SimpleChunkSection(List layersBlock) { + this.layersBlock = layersBlock; + } + + @Override + public int getSkyLight(int localX, int localY, int localZ) { + if (localY <= 3) return 0; + else return 15; + } + + @Override + public void setSkyLight(int localX, int localY, int localZ, int lightLevel) { + } + + @Override + public int getAddition(int localX, int localY, int localZ) { + return 0; + } + + @Override + public void setAddition(int localX, int localY, int localZ, int value) { + } + + @Override + public int getY() { + return 0; + } + + @Override + public void setBlock(Block block) { + } + + @Override + public Block getBlock(int localX, int localY, int localZ) { + if (localX < 0) localX = 0; + else if (localX > 15) localX = 15; + if (localY < 0) localY = 0; + else if (localY > 15) localY = 15; + if (localZ < 0) localZ = 0; + else if (localZ > 15) localZ = 15; + + if (localY >= layersBlock.size()) { + return blockFactory.create(BlockType.AIR, localX, localY, localZ); + } + + BlockType blockType = layersBlock.get(localY); + if (blockType == null) return blockFactory.create(BlockType.AIR, localX, localY, localZ); + + return blockFactory.create(blockType, localX, localY, localZ); + } +} diff --git a/simple_world/src/main/java/mc/world/simple/SimpleWorld.java b/simple_world/src/main/java/mc/world/simple/SimpleWorld.java new file mode 100644 index 0000000..4a6e345 --- /dev/null +++ b/simple_world/src/main/java/mc/world/simple/SimpleWorld.java @@ -0,0 +1,67 @@ +package mc.world.simple; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import mc.core.EntityLocation; +import mc.core.world.World; +import mc.core.world.WorldType; +import mc.core.world.block.Block; +import mc.core.world.chunk.Chunk; +import mc.core.world.chunk.ChunkProvider; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; + +@Slf4j +@Component +public class SimpleWorld implements World, BeanNameAware { + @Getter + private String name; + @Getter + private final WorldType type = WorldType.FLAT; + private EntityLocation spawn; + @Setter + private ChunkProvider chunkProvider; + + @Override + public EntityLocation getSpawn() { + if (this.spawn == null) { + log.warn("Spawn is not defined! Set spawn [0, 6, 0]"); + setSpawn(0d, 6d, 0d); + } + + return this.spawn; + } + + @Override + public void setSpawn(EntityLocation location) { + this.spawn = location; + } + + @Override + public Chunk getChunk(int x, int z) { + return chunkProvider.getChunk(x, z); + } + + @Override + public void setChunk(int x, int z, Chunk chunk) { + throw new UnsupportedOperationException(); + } + + @Override + public Block getBlock(int x, int y, int z) { + return chunkProvider.getChunk(x >> 4, z >> 4).getBlock(x, y, z); + } + + @Override + public void setBlock(Block block) { + // nope... + } + + @Override + public void setBeanName(@Nonnull String name) { + this.name = name; + } +} diff --git a/simple_world/src/test/java/mc/world/simple/SimpleChunkSectionTest.java b/simple_world/src/test/java/mc/world/simple/SimpleChunkSectionTest.java new file mode 100644 index 0000000..b0486b9 --- /dev/null +++ b/simple_world/src/test/java/mc/world/simple/SimpleChunkSectionTest.java @@ -0,0 +1,46 @@ +package mc.world.simple; + +import com.google.common.collect.Lists; +import mc.core.world.block.Block; +import mc.core.world.block.BlockType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SimpleChunkSectionTest { + private SimpleChunkSection chunkSection; + private List layersBlock; + + @BeforeEach + void before() { + layersBlock = Lists.newArrayList( + BlockType.BEDROCK, + BlockType.DIRT, + BlockType.DIRT, + BlockType.GRASS + ); + + chunkSection = new SimpleChunkSection(layersBlock); + } + + @Test + @Disabled + void getBlock() { + for (int y = 15; y >= 0; y--) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + Block block = chunkSection.getBlock(x, y, z); + if (y > layersBlock.size()-1) { + assertEquals(block.getType(), BlockType.AIR); + } else { + assertEquals(block.getType(), layersBlock.get(y)); + } + } + } + } + } +} \ No newline at end of file diff --git a/simple_world/src/test/java/mc/world/simple/SimpleWorldTest.java b/simple_world/src/test/java/mc/world/simple/SimpleWorldTest.java new file mode 100644 index 0000000..110a830 --- /dev/null +++ b/simple_world/src/test/java/mc/world/simple/SimpleWorldTest.java @@ -0,0 +1,37 @@ +package mc.world.simple; + +import mc.core.EntityLocation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {TestSpringConfig.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +class SimpleWorldTest { + @Autowired + private SimpleWorld world; + + @Test + void spawn() { + final EntityLocation location = new EntityLocation(1d, 2d, 3d,4f, 5f); + + world.setSpawn(location); + assertEquals(location, world.getSpawn()); + assertSame(location, world.getSpawn()); + + world.setSpawn(1d, 2d, 3d, 4f, 5f); + assertEquals(location, world.getSpawn()); + assertNotSame(location, world.getSpawn()); + + location.setYawPitch(0, 0); + world.setSpawn(1d, 2d, 3d); + assertEquals(location, world.getSpawn()); + assertNotSame(location, world.getSpawn()); + } +} \ No newline at end of file diff --git a/simple_world/src/test/java/mc/world/simple/TestSpringConfig.java b/simple_world/src/test/java/mc/world/simple/TestSpringConfig.java new file mode 100644 index 0000000..865fdd9 --- /dev/null +++ b/simple_world/src/test/java/mc/world/simple/TestSpringConfig.java @@ -0,0 +1,38 @@ +package mc.world.simple; + +import com.google.common.collect.Lists; +import mc.core.world.block.BlockType; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ComponentScan("mc.world.simple") +public class TestSpringConfig { + @Bean + public List layersBlock() { + return Lists.newArrayList( + BlockType.BEDROCK, + BlockType.DIRT, + BlockType.DIRT, + BlockType.GRASS + ); + } + + @Bean + public SimpleChunkSection chunkSection(List layersBlock) { + return new SimpleChunkSection(layersBlock); + } + + @Bean + public SimpleWorld simpleWorld(List layersBlock) { + FlatChunkProvider chunkProvider = new FlatChunkProvider(); + chunkProvider.setLayersBlock(layersBlock); + + SimpleWorld world = new SimpleWorld(); + world.setChunkProvider(chunkProvider); + return world; + } +} diff --git a/vanilla_commands/build.gradle b/vanilla_commands/build.gradle new file mode 100644 index 0000000..6d1bcee --- /dev/null +++ b/vanilla_commands/build.gradle @@ -0,0 +1,6 @@ +version '1.0-SNAPSHOT' + +dependencies { + /* Core */ + compile_excludeCopy project(':core') +} diff --git a/vanilla_commands/src/main/java/mc/commands/HelpCommand.java b/vanilla_commands/src/main/java/mc/commands/HelpCommand.java new file mode 100644 index 0000000..c3d466e --- /dev/null +++ b/vanilla_commands/src/main/java/mc/commands/HelpCommand.java @@ -0,0 +1,70 @@ +/* + * DmitriyMX + * 2018-05-23 + */ +package mc.commands; + +import lombok.extern.slf4j.Slf4j; +import mc.core.chat.CommandExecutor; +import mc.core.chat.CommanderChatProcessor; +import mc.core.chat.MessageType; +import mc.core.player.Player; +import mc.core.text.Text; +import mc.core.text.TextColor; +import mc.core.text.TextTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +import java.util.Optional; + +@Slf4j +public class HelpCommand implements CommandExecutor { + private static final TextTemplate messageFormat = TextTemplate.builder() + .arg("command", TextColor.RED) + .append(Text.of(TextColor.GRAY, " - ")) + .arg("description", TextColor.WHITE) + .build(); + + @Autowired + private ApplicationContext applicationContext; + private CommanderChatProcessor commanderChatProcessor; + + @Override + public String getName() { + return "help"; + } + + @Override + public Optional getAliases() { + return Optional.of(new String[]{"?"}); + } + + @Override + public Optional getUsage() { + return Optional.empty(); + } + + @Override + public String getDescription() { + return "shows this message"; + } + + @Override + public void execute(Player sender, String... args) { + if (commanderChatProcessor == null) { + commanderChatProcessor = applicationContext.getBean(CommanderChatProcessor.class); + if (commanderChatProcessor == null) { + log.error("Error get bean of type \"CommanderChatProcessor\". WTF?!"); + sender.getChannel().sendChatMessage(Text.of(TextColor.RED, "!!-Server error-!!")); + return; + } + } + + commanderChatProcessor.getAllCommands().forEach(commandExecutor -> { + Text message = messageFormat.apply( + "command", commandExecutor.getUsage().orElse(commandExecutor.getName()), + "description", commandExecutor.getDescription()); + sender.getChannel().sendChatMessage(message, MessageType.SYSTEM_MESSAGE); + }); + } +} diff --git a/vanilla_commands/src/main/java/mc/commands/ListCommand.java b/vanilla_commands/src/main/java/mc/commands/ListCommand.java new file mode 100644 index 0000000..82411a6 --- /dev/null +++ b/vanilla_commands/src/main/java/mc/commands/ListCommand.java @@ -0,0 +1,60 @@ +/* + * DmitriyMX + * 2018-05-23 + */ +package mc.commands; + +import mc.core.chat.CommandExecutor; +import mc.core.chat.MessageType; +import mc.core.player.Player; +import mc.core.player.PlayerManager; +import mc.core.text.Text; +import mc.core.text.TextColor; +import mc.core.text.TextTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Optional; +import java.util.StringJoiner; + +public class ListCommand implements CommandExecutor { + private static final TextTemplate messageFormat = TextTemplate.builder() + .append(Text.of(TextColor.GREEN, "Online(")) + .arg("count") + .append(Text.of(TextColor.GREEN, "): ")) + .arg("players", TextColor.DARK_GREEN) + .build(); + + @Autowired + private PlayerManager playerManager; + + @Override + public String getName() { + return "list"; + } + + @Override + public Optional getAliases() { + return Optional.empty(); + } + + @Override + public Optional getUsage() { + return Optional.empty(); + } + + @Override + public String getDescription() { + return "lists all currently connected players"; + } + + @Override + public void execute(Player sender, String... args) { + StringJoiner sj = new StringJoiner(", "); + playerManager.getPlayers().forEach(pl -> sj.add(pl.getName())); + + Text message = messageFormat.apply( + "count", playerManager.getCountPlayers(), + "players", sj.toString()); + sender.getChannel().sendChatMessage(message, MessageType.SYSTEM_MESSAGE); + } +}