diff --git a/conventions/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts b/conventions/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts index d322b29..07663a5 100644 --- a/conventions/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts +++ b/conventions/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts @@ -1,8 +1,8 @@ plugins { java `java-library` - jacoco id("io.freefair.lombok") + id("de.siphalor.tweed5.unit-tests") id("de.siphalor.tweed5.publishing") id("de.siphalor.tweed5.local-runtime-only") id("de.siphalor.tweed5.expanded-sources-jar") @@ -13,12 +13,6 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -val testAgent = configurations.dependencyScope("mockitoAgent") -val testAgentClasspath = configurations.resolvable("testAgentClasspath") { - isTransitive = false - extendsFrom(testAgent.get()) -} - lombok { version = libs.versions.lombok.get() } @@ -39,40 +33,9 @@ dependencies { testImplementation(libs.acl) testImplementation(libs.slf4j.rt) - testImplementation(platform(libs.junit.platform)) - testImplementation(libs.junit.core) - testRuntimeOnly(libs.junit.launcher) - testImplementation(libs.mockito) - testAgent(libs.mockito) - testImplementation(libs.assertj) testImplementation(project(":generic-test-utils")) } -tasks.compileTestJava { - sourceCompatibility = libs.versions.java.test.get() - targetCompatibility = libs.versions.java.test.get() -} - -tasks.test { - val testAgentFiles = testAgentClasspath.map { it.files } - doFirst { - jvmArgs(testAgentFiles.get().map { file -> "-javaagent:${file.absolutePath}" }) - } - dependsOn(testAgentClasspath) - finalizedBy(tasks.jacocoTestReport) - - useJUnitPlatform() - systemProperties( - "junit.jupiter.execution.timeout.mode" to "disabled_on_debug", - "junit.jupiter.execution.timeout.testable.method.default" to "10s", - "junit.jupiter.execution.timeout.thread.mode.default" to "SEPARATE_THREAD", - ) -} - -tasks.jacocoTestReport { - dependsOn(tasks.test) -} - publishing { publications { create("lib") { diff --git a/conventions/src/main/kotlin/de.siphalor.tweed5.unit-tests.gradle.kts b/conventions/src/main/kotlin/de.siphalor.tweed5.unit-tests.gradle.kts new file mode 100644 index 0000000..31b89e7 --- /dev/null +++ b/conventions/src/main/kotlin/de.siphalor.tweed5.unit-tests.gradle.kts @@ -0,0 +1,48 @@ +plugins { + java + jacoco +} + +val testAgent = configurations.dependencyScope("mockitoAgent") +val testAgentClasspath = configurations.resolvable("testAgentClasspath") { + isTransitive = false + extendsFrom(testAgent.get()) +} + +dependencies { + versionCatalogs.find("libs").ifPresent { libs -> + testImplementation(platform(libs.findLibrary("junit.platform").get())) + testImplementation(libs.findLibrary("junit.core").get()) + testRuntimeOnly(libs.findLibrary("junit.launcher").get()) + testImplementation(libs.findLibrary("mockito").get()) + testAgent(libs.findLibrary("mockito").get()) + testImplementation(libs.findLibrary("assertj").get()) + } +} + +tasks.compileTestJava { + versionCatalogs.find("libs").ifPresent { libs -> + sourceCompatibility = libs.findVersion("java.test").get().requiredVersion + targetCompatibility = libs.findVersion("java.test").get().requiredVersion + } +} + +tasks.test { + val testAgentFiles = testAgentClasspath.map { it.files } + doFirst { + jvmArgs(testAgentFiles.get().map { file -> "-javaagent:${file.absolutePath}" }) + } + dependsOn(testAgentClasspath) + finalizedBy(tasks.jacocoTestReport) + + useJUnitPlatform() + systemProperties( + "junit.jupiter.execution.timeout.mode" to "disabled_on_debug", + "junit.jupiter.execution.timeout.testable.method.default" to "10s", + "junit.jupiter.execution.timeout.thread.mode.default" to "SEPARATE_THREAD", + ) +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) +} diff --git a/tweed5-minecraft/buildSrc/src/main/kotlin/de.siphalor.tweed5.minecraft.mod.cross-version.gradle.kts b/tweed5-minecraft/buildSrc/src/main/kotlin/de.siphalor.tweed5.minecraft.mod.cross-version.gradle.kts index 8307ca0..6aac0ae 100644 --- a/tweed5-minecraft/buildSrc/src/main/kotlin/de.siphalor.tweed5.minecraft.mod.cross-version.gradle.kts +++ b/tweed5-minecraft/buildSrc/src/main/kotlin/de.siphalor.tweed5.minecraft.mod.cross-version.gradle.kts @@ -7,6 +7,7 @@ import java.util.Properties plugins { java id("fabric-loom") + id("de.siphalor.tweed5.unit-tests") id("de.siphalor.tweed5.publishing") id("de.siphalor.tweed5.expanded-sources-jar") id("de.siphalor.jcyo") diff --git a/tweed5-minecraft/gradle/mcCommonLibs.versions.toml b/tweed5-minecraft/gradle/mcCommonLibs.versions.toml index e5a63a9..ca7e12c 100644 --- a/tweed5-minecraft/gradle/mcCommonLibs.versions.toml +++ b/tweed5-minecraft/gradle/mcCommonLibs.versions.toml @@ -1,7 +1,7 @@ [versions] fabric-loader = "0.17.2" fabric-loom = "1.11-SNAPSHOT" -jcyo = "0.5.1" +jcyo = "0.6.1" [plugins] jcyo = { id = "de.siphalor.jcyo", version.ref = "jcyo" } diff --git a/tweed5-minecraft/networking/build.gradle.kts b/tweed5-minecraft/networking/build.gradle.kts new file mode 100644 index 0000000..3e8dd03 --- /dev/null +++ b/tweed5-minecraft/networking/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("de.siphalor.tweed5.local-runtime-only") + id("de.siphalor.tweed5.minecraft.mod.cross-version") +} + +dependencies { + modCompileOnly(fabricApi.module("fabric-networking-api-v1", mcLibs.versions.fabric.api.get())) + compileOnly("de.siphalor.tweed5:tweed5-core") + compileOnly("de.siphalor.tweed5:tweed5-serde-extension") + + testImplementation("de.siphalor.tweed5:tweed5-core") + testImplementation("de.siphalor.tweed5:tweed5-serde-extension") +} diff --git a/tweed5-minecraft/networking/gradle.properties b/tweed5-minecraft/networking/gradle.properties new file mode 100644 index 0000000..bf3c488 --- /dev/null +++ b/tweed5-minecraft/networking/gradle.properties @@ -0,0 +1,2 @@ +module.name = Tweed 5 Netwoking +module.description = Minecraft networking support for Tweed 5 diff --git a/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/ByteBufReader.java b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/ByteBufReader.java new file mode 100644 index 0000000..6498d28 --- /dev/null +++ b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/ByteBufReader.java @@ -0,0 +1,297 @@ +package de.siphalor.tweed5.minecraft.networking.api; + +import de.siphalor.tweed5.dataapi.api.TweedDataReadException; +import de.siphalor.tweed5.dataapi.api.TweedDataReader; +import de.siphalor.tweed5.dataapi.api.TweedDataToken; +import de.siphalor.tweed5.dataapi.api.TweedDataTokens; +import de.siphalor.tweed5.minecraft.networking.impl.ByteBufSerdeConstants; +import io.netty.buffer.ByteBuf; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Deque; + +@RequiredArgsConstructor +public class ByteBufReader implements TweedDataReader { + private final ByteBuf buf; + private final Deque contextStack = new ArrayDeque<>(); + private @Nullable TweedDataToken peek; + + @Override + public TweedDataToken peekToken() throws TweedDataReadException { + if (peek != null) return peek; + ensureReadable(); + return (peek = nextToken()); + } + + @Override + public TweedDataToken readToken() throws TweedDataReadException { + if (peek != null) { + TweedDataToken token = peek; + peek = null; + return token; + } + ensureReadable(); + return nextToken(); + } + + private void ensureReadable() throws TweedDataReadException { + if (!buf.isReadable()) { + throw TweedDataReadException.builder().message("Reached end of buffer").build(); + } + } + + private TweedDataToken nextToken() throws TweedDataReadException { + int b = Byte.toUnsignedInt(buf.readByte()); + switch (b) { + case ByteBufSerdeConstants.NULL_VALUE: + return wrapTokenForContext(TweedDataTokens.getNull()); + case ByteBufSerdeConstants.FALSE_VALUE: + return wrapTokenForContext(BooleanToken.FALSE); + case ByteBufSerdeConstants.TRUE_VALUE: + return wrapTokenForContext(BooleanToken.TRUE); + case ByteBufSerdeConstants.EMPTY_STRING_VALUE: + return wrapTokenForContext(new StringToken("")); + case ByteBufSerdeConstants.VARNUM_VARIANT_INT8: + return wrapTokenForContext(new ByteToken(buf.readByte())); + case ByteBufSerdeConstants.VARNUM_VARIANT_INT16: + return wrapTokenForContext(new IntToken(buf.readShort())); + case ByteBufSerdeConstants.VARNUM_VARIANT_INT32: + return wrapTokenForContext(new IntToken(buf.readInt())); + case ByteBufSerdeConstants.VARNUM_VARIANT_INT64: { + long value = buf.readLong(); + if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { + return wrapTokenForContext(new IntToken((int) value)); + } else { + return wrapTokenForContext(new TweedDataToken() { + @Override + public boolean canReadAsLong() { + return true; + } + + @Override + public long readAsLong() { + return value; + } + }); + } + } + case ByteBufSerdeConstants.VARNUM_VARIANT_FLOAT: + return wrapTokenForContext(new TweedDataToken() { + private final float value = buf.readFloat(); + @Override + public boolean canReadAsFloat() { + return true; + } + + @Override + public float readAsFloat() { + return value; + } + }); + case ByteBufSerdeConstants.VARNUM_VARIANT_DOUBLE: + return wrapTokenForContext(new TweedDataToken() { + private final double value = buf.readDouble(); + @Override + public boolean canReadAsDouble() { + return true; + } + + @Override + public double readAsDouble() { + return value; + } + }); + case ByteBufSerdeConstants.COMPLEX_VARIANT_STRING: { + int length = buf.readInt(); + ByteBuf byteBuf = buf.readBytes(length); + return wrapTokenForContext(new StringToken(byteBuf.toString(StandardCharsets.UTF_8))); + } + case ByteBufSerdeConstants.COMPLEX_VARIANT_LIST: { + TweedDataToken token = wrapTokenForContext(TweedDataTokens.getListStart(), false); + contextStack.push(Context.LIST); + return token; + } + case ByteBufSerdeConstants.COMPLEX_VARIANT_MAP: { + TweedDataToken token = wrapTokenForContext(TweedDataTokens.getMapStart(), false); + contextStack.push(Context.MAP); + return token; + } + case ByteBufSerdeConstants.COMPLEX_VARIANT_END: { + Context context = contextStack.pop(); + return wrapTokenForContext( + context == Context.MAP + ? TweedDataTokens.getMapEnd() + : TweedDataTokens.getListEnd() + ); + } + default: + int specialEmbedType = b & ByteBufSerdeConstants.SPECIAL_EMBED_TYPE_MASK; + if (specialEmbedType == ByteBufSerdeConstants.UINT6_TYPE) { + return wrapTokenForContext(new ByteToken((byte) (b & ByteBufSerdeConstants.SPECIAL_EMBED_VALUE_MASK))); + } else if (specialEmbedType == ByteBufSerdeConstants.SMALL_STRING_TYPE) { + int length = (b & ByteBufSerdeConstants.VALUE_MASK) + 1; + ByteBuf byteBuf = buf.readBytes(length); + return wrapTokenForContext(new StringToken(byteBuf.toString(StandardCharsets.UTF_8))); + } + throw TweedDataReadException.builder() + .message("Unknown type byte value " + Integer.toBinaryString(b)) + .build(); + } + } + + private TweedDataToken wrapTokenForContext(TweedDataToken token) { + return wrapTokenForContext(token, true); + } + + private TweedDataToken wrapTokenForContext(TweedDataToken token, boolean isValueEnd) { + Context context = contextStack.peek(); + if (context == null) { + return token; + } else if (context == Context.LIST) { + return TweedDataTokens.asListValue(token); + } else if (context == Context.MAP) { + contextStack.push(Context.MAP_VALUE); + return TweedDataTokens.asMapEntryKey(token); + } else { + if (isValueEnd) { + contextStack.pop(); + } + return TweedDataTokens.asMapEntryValue(token); + } + } + + @Override + public void close() { + + } + + private enum Context { + LIST, MAP, MAP_VALUE + } + + @RequiredArgsConstructor + private static class BooleanToken implements TweedDataToken { + public static final BooleanToken FALSE = new BooleanToken(false); + public static final BooleanToken TRUE = new BooleanToken(true); + + private final boolean value; + + @Override + public boolean canReadAsBoolean() { + return true; + } + + @Override + public boolean readAsBoolean() { + return value; + } + } + + @RequiredArgsConstructor + private static class ByteToken implements TweedDataToken { + private final byte value; + + @Override + public boolean canReadAsByte() { + return true; + } + + @Override + public byte readAsByte() { + return value; + } + + @Override + public boolean canReadAsShort() { + return true; + } + + @Override + public short readAsShort() { + return value; + } + + @Override + public boolean canReadAsInt() { + return true; + } + + @Override + public int readAsInt() { + return value; + } + + @Override + public boolean canReadAsLong() { + return true; + } + + @Override + public long readAsLong() { + return value; + } + } + + @RequiredArgsConstructor + public static class IntToken implements TweedDataToken { + private final int value; + + @Override + public boolean canReadAsByte() { + return value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE; + } + + @Override + public byte readAsByte() { + return (byte) value; + } + + @Override + public boolean canReadAsShort() { + return value >= Short.MIN_VALUE && value <= Short.MAX_VALUE; + } + + @Override + public short readAsShort() { + return (short) value; + } + + @Override + public boolean canReadAsInt() { + return true; + } + + @Override + public int readAsInt() { + return value; + } + + @Override + public boolean canReadAsLong() { + return true; + } + + @Override + public long readAsLong() { + return value; + } + } + + @RequiredArgsConstructor + public static class StringToken implements TweedDataToken { + private final String value; + + @Override + public boolean canReadAsString() { + return true; + } + + @Override + public String readAsString() { + return value; + } + } +} diff --git a/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/RawByteBufWriter.java b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/RawByteBufWriter.java new file mode 100644 index 0000000..f158242 --- /dev/null +++ b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/RawByteBufWriter.java @@ -0,0 +1,109 @@ +package de.siphalor.tweed5.minecraft.networking.api; + +import de.siphalor.tweed5.dataapi.api.TweedDataWriter; +import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration; +import de.siphalor.tweed5.minecraft.networking.impl.ByteBufSerdeConstants; +import io.netty.buffer.ByteBuf; +import lombok.RequiredArgsConstructor; + +import java.nio.charset.StandardCharsets; + +@RequiredArgsConstructor +public class RawByteBufWriter implements TweedDataWriter { + protected final ByteBuf buf; + + @Override + public void visitNull() { + buf.writeByte(ByteBufSerdeConstants.NULL_VALUE); + } + + @Override + public void visitBoolean(boolean value) { + if (value) { + buf.writeByte(ByteBufSerdeConstants.TRUE_VALUE); + } else { + buf.writeByte(ByteBufSerdeConstants.FALSE_VALUE); + } + } + + @Override + public void visitByte(byte value) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT8); + buf.writeByte(Byte.toUnsignedInt(value)); + } + + @Override + public void visitShort(short value) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT16); + buf.writeShort(Short.toUnsignedInt(value)); + } + + @Override + public void visitInt(int value) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT32); + buf.writeInt(value); + } + + @Override + public void visitLong(long value) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT64); + buf.writeLong(value); + } + + @Override + public void visitFloat(float value) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_FLOAT); + buf.writeFloat(value); + } + + @Override + public void visitDouble(double value) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_DOUBLE); + buf.writeDouble(value); + } + + @Override + public void visitString(String value) { + writeStringBytes(value.getBytes(StandardCharsets.UTF_8)); + } + + protected void writeStringBytes(byte[] bytes) { + buf.writeByte(ByteBufSerdeConstants.COMPLEX_VARIANT_STRING); + buf.writeInt(bytes.length); + buf.writeBytes(bytes); + } + + @Override + public void visitListStart() { + buf.writeByte(ByteBufSerdeConstants.COMPLEX_VARIANT_LIST); + } + + @Override + public void visitListEnd() { + buf.writeByte(ByteBufSerdeConstants.COMPLEX_VARIANT_END); + } + + @Override + public void visitMapStart() { + buf.writeByte(ByteBufSerdeConstants.COMPLEX_VARIANT_MAP); + } + + @Override + public void visitMapEntryKey(String key) { + visitString(key); + } + + @Override + public void visitMapEnd() { + buf.writeByte(ByteBufSerdeConstants.COMPLEX_VARIANT_END); + } + + @Override + public void visitDecoration(TweedDataDecoration decoration) { + // ignored + } + + @Override + public void close() { + } +} diff --git a/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/SlightlyCompressedByteBufWriter.java b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/SlightlyCompressedByteBufWriter.java new file mode 100644 index 0000000..bd4ecc3 --- /dev/null +++ b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/SlightlyCompressedByteBufWriter.java @@ -0,0 +1,87 @@ +package de.siphalor.tweed5.minecraft.networking.api; + +import de.siphalor.tweed5.minecraft.networking.impl.ByteBufSerdeConstants; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; + +public class SlightlyCompressedByteBufWriter extends RawByteBufWriter { + public SlightlyCompressedByteBufWriter(ByteBuf buf) { + super(buf); + } + + @Override + public void visitByte(byte value) { + int v = Byte.toUnsignedInt(value); + if (v <= 0b11_1111) { + buf.writeByte(ByteBufSerdeConstants.UINT6_TYPE + (v & 0b11_1111)); + } else { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT8); + buf.writeByte(v); + } + } + + @Override + public void visitShort(short value) { + int v = Short.toUnsignedInt(value); + if (v <= 0b11_1111) { + writeUint6(v); + } else if (v <= 0b0111_1111) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT8); + buf.writeByte(v & 0b0111_1111); + } else if (v >= 0b1000_0000_0000_0000 && v <= 0b1000_0000_0111_1111) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT8); + buf.writeByte(v & 0b0111_1111 | 0b1000_0000); + } else { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT16); + buf.writeShort(v); + } + } + + @Override + public void visitInt(int value) { + if (value >= 0 && value <= 0b11_1111) { + writeUint6(value); + } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT8); + if (value < 0) value |= 0b1000_0000; + buf.writeByte(value); + } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT16); + if (value < 0) value |= 0b1000_0000_0000_0000; + buf.writeShort(value); + } else { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT32); + buf.writeInt(value); + } + } + + @Override + public void visitLong(long value) { + if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { + visitInt((int) value); + } else { + buf.writeByte(ByteBufSerdeConstants.VARNUM_VARIANT_INT64); + buf.writeLong(value); + } + } + + private void writeUint6(int value) { + buf.writeByte(ByteBufSerdeConstants.UINT6_TYPE | (value & 0b11_1111)); + } + + @Override + public void visitString(String value) { + if (value.isEmpty()) { + buf.writeByte(ByteBufSerdeConstants.EMPTY_STRING_VALUE); + return; + } + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + if (bytes.length <= 0b100_0000) { + buf.writeByte(ByteBufSerdeConstants.SMALL_STRING_TYPE | (bytes.length - 1)); + buf.writeBytes(bytes); + } else { + writeStringBytes(bytes); + } + } +} diff --git a/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/package-info.java b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/package-info.java new file mode 100644 index 0000000..ac30483 --- /dev/null +++ b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/api/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.minecraft.networking.api; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/impl/ByteBufSerdeConstants.java b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/impl/ByteBufSerdeConstants.java new file mode 100644 index 0000000..8fb58d1 --- /dev/null +++ b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/impl/ByteBufSerdeConstants.java @@ -0,0 +1,36 @@ +package de.siphalor.tweed5.minecraft.networking.impl; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class ByteBufSerdeConstants { + public static final int TYPE_MASK = 0b1111_0000; + public static final int VALUE_MASK = 0b0000_1111; + + public static final int CONST_TYPE = 0; + public static final int NULL_VALUE = 0; + public static final int FALSE_VALUE = 1; + public static final int TRUE_VALUE = 0b10; + public static final int EMPTY_STRING_VALUE = 0b11; + + public static final int VARNUM_TYPE = 0b0001_0000; + public static final int VARNUM_VARIANT_INT8 = VARNUM_TYPE; + public static final int VARNUM_VARIANT_INT16 = VARNUM_TYPE | 0b0001; + public static final int VARNUM_VARIANT_INT32 = VARNUM_TYPE | 0b0010; + public static final int VARNUM_VARIANT_INT64 = VARNUM_TYPE | 0b0011; + public static final int VARNUM_VARIANT_FLOAT = VARNUM_TYPE | 0b1000; + public static final int VARNUM_VARIANT_DOUBLE = VARNUM_TYPE | 0b1001; + + public static final int COMPLEX_TYPE = 0b0010_0000; + public static final int COMPLEX_VARIANT_STRING = COMPLEX_TYPE; + public static final int COMPLEX_VARIANT_LIST = COMPLEX_TYPE | 0b0001; + public static final int COMPLEX_VARIANT_MAP = COMPLEX_TYPE | 0b0010; + public static final int COMPLEX_VARIANT_END = COMPLEX_TYPE | 0b1111; + + // 0b01xx_xxxx is reserved for future use + + public static final int SPECIAL_EMBED_TYPE_MASK = 0b1100_0000; + public static final int SPECIAL_EMBED_VALUE_MASK = 0b0011_1111; + public static final int UINT6_TYPE = 0b1000_0000; + public static final int SMALL_STRING_TYPE = 0b1100_0000; +} diff --git a/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/impl/package-info.java b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/impl/package-info.java new file mode 100644 index 0000000..e59c2f4 --- /dev/null +++ b/tweed5-minecraft/networking/src/main/java/de/siphalor/tweed5/minecraft/networking/impl/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.minecraft.networking.impl; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-minecraft/networking/src/test/java/de/siphalor/tweed5/minecraft/network/api/ByteBufReaderWriterTest.java b/tweed5-minecraft/networking/src/test/java/de/siphalor/tweed5/minecraft/network/api/ByteBufReaderWriterTest.java new file mode 100644 index 0000000..7562669 --- /dev/null +++ b/tweed5-minecraft/networking/src/test/java/de/siphalor/tweed5/minecraft/network/api/ByteBufReaderWriterTest.java @@ -0,0 +1,157 @@ +package de.siphalor.tweed5.minecraft.network.api; + +import de.siphalor.tweed5.dataapi.api.TweedDataReadException; +import de.siphalor.tweed5.dataapi.api.TweedDataToken; +import de.siphalor.tweed5.dataapi.api.TweedDataWriter; +import de.siphalor.tweed5.minecraft.networking.api.ByteBufReader; +import de.siphalor.tweed5.minecraft.networking.api.RawByteBufWriter; +import de.siphalor.tweed5.minecraft.networking.api.SlightlyCompressedByteBufWriter; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.SneakyThrows; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.Function; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; + +public class ByteBufReaderWriterTest { + @ParameterizedTest + @MethodSource("testParams") + @SneakyThrows + void test(Function writerConstructor) { + ByteBuf buffer = Unpooled.buffer(); + + try (TweedDataWriter writer = writerConstructor.apply(buffer)) { + writer.visitMapStart(); + writer.visitMapEntryKey("first"); + writer.visitNull(); + writer.visitMapEntryKey("bytes"); + writer.visitListStart(); + writer.visitByte((byte) 12); + writer.visitByte((byte) -12); + writer.visitByte((byte) 123); + writer.visitListEnd(); + writer.visitMapEntryKey("nums"); + writer.visitListStart(); + writer.visitShort((short) 1234); + writer.visitInt(4321); + writer.visitInt(Integer.MAX_VALUE); + writer.visitLong(Long.MAX_VALUE); + writer.visitFloat(1234.5678f); + writer.visitDouble(1234.5678); + writer.visitListEnd(); + writer.visitMapEntryKey("other"); + writer.visitString("Hello World!"); + writer.visitMapEnd(); + } + + System.out.println("Buffer size is: " + buffer.writerIndex()); + assertThat(buffer.readerIndex()).isZero(); + + try (ByteBufReader reader = new ByteBufReader(buffer)) { + assertThat(reader.readToken()).extracting(TweedDataToken::isMapStart).isEqualTo(true); + assertNextMapKey(reader.readToken(), "first"); + assertThat(reader.readToken()).extracting(TweedDataToken::isNull).isEqualTo(true); + assertNextMapKey(reader.readToken(), "bytes"); + assertThat(reader.readToken()).extracting(TweedDataToken::isListStart).isEqualTo(true); + assertByteToken(reader.readToken(), (byte) 12); + assertByteToken(reader.readToken(), (byte) -12); + assertByteToken(reader.readToken(), (byte) 123); + assertThat(reader.readToken()).extracting(TweedDataToken::isListEnd).isEqualTo(true); + assertNextMapKey(reader.readToken(), "nums"); + assertThat(reader.readToken()).extracting(TweedDataToken::isListStart).isEqualTo(true); + assertThat(reader.readToken()).satisfies( + token -> assertThat(token.canReadAsByte()).isFalse(), + token -> assertThat(token.canReadAsShort()).isTrue(), + token -> assertThat(token.readAsShort()).isEqualTo((short) 1234), + token -> assertThat(token.canReadAsInt()).isTrue(), + token -> assertThat(token.readAsInt()).isEqualTo(1234), + token -> assertThat(token.canReadAsLong()).isTrue(), + token -> assertThat(token.readAsLong()).isEqualTo(1234L) + ); + assertThat(reader.readToken()).satisfies( + token -> assertThat(token.canReadAsByte()).isFalse(), + token -> assertThat(token.canReadAsShort()).isTrue(), + token -> assertThat(token.readAsShort()).isEqualTo((short) 4321), + token -> assertThat(token.canReadAsInt()).isTrue(), + token -> assertThat(token.readAsInt()).isEqualTo(4321), + token -> assertThat(token.canReadAsLong()).isTrue(), + token -> assertThat(token.readAsLong()).isEqualTo(4321L) + ); + assertThat(reader.readToken()).satisfies( + token -> assertThat(token.canReadAsByte()).isFalse(), + token -> assertThat(token.canReadAsShort()).isFalse(), + token -> assertThat(token.canReadAsInt()).isTrue(), + token -> assertThat(token.readAsInt()).isEqualTo(Integer.MAX_VALUE), + token -> assertThat(token.canReadAsLong()).isTrue(), + token -> assertThat(token.readAsLong()).isEqualTo(Integer.MAX_VALUE) + ); + assertThat(reader.readToken()).satisfies( + token -> assertThat(token.canReadAsByte()).isFalse(), + token -> assertThat(token.canReadAsShort()).isFalse(), + token -> assertThat(token.canReadAsInt()).isFalse(), + token -> assertThat(token.canReadAsLong()).isTrue(), + token -> assertThat(token.readAsLong()).isEqualTo(Long.MAX_VALUE) + ); + assertThat(reader.readToken()).satisfies( + token -> assertThat(token.canReadAsFloat()).isTrue(), + token -> assertThat(token.readAsFloat()).isEqualTo(1234.5678f) + ); + assertThat(reader.readToken()).satisfies( + token -> assertThat(token.canReadAsDouble()).isTrue(), + token -> assertThat(token.readAsDouble()).isEqualTo(1234.5678) + ); + assertThat(reader.readToken()).extracting(TweedDataToken::isListEnd).isEqualTo(true); + assertNextMapKey(reader.readToken(), "other"); + assertThat(reader.readToken()).satisfies( + token -> assertThat(token.canReadAsString()).isTrue(), + token -> assertThat(token.readAsString()).isEqualTo("Hello World!"), + token -> assertThat(token.isMapEntryValue()).isTrue() + ); + assertThat(reader.readToken()).extracting(TweedDataToken::isMapEnd).isEqualTo(true); + assertThatThrownBy(reader::readToken).isInstanceOf(TweedDataReadException.class); + } + + buffer.release(); + } + + private void assertNextMapKey(TweedDataToken dataToken, String key) { + assertThat(dataToken).satisfies( + token -> assertThat(token.isMapEntryKey()).isTrue(), + token -> assertThat(token.canReadAsString()).isTrue(), + token -> assertThat(token.readAsString()).isEqualTo(key) + ); + } + + private void assertByteToken(TweedDataToken dataToken, byte value) { + assertThat(dataToken).satisfies( + token -> assertThat(token.canReadAsByte()).isTrue(), + token -> assertThat(token.readAsByte()).isEqualTo(value), + token -> assertThat(token.canReadAsShort()).isTrue(), + token -> assertThat(token.readAsShort()).isEqualTo(value), + token -> assertThat(token.canReadAsInt()).isTrue(), + token -> assertThat(token.readAsInt()).isEqualTo(value), + token -> assertThat(token.canReadAsLong()).isTrue(), + token -> assertThat(token.readAsLong()).isEqualTo(value) + ); + } + + static Stream testParams() { + return Stream.of( + argumentSet( + RawByteBufWriter.class.getSimpleName(), + ((Function) RawByteBufWriter::new) + ), + argumentSet( + SlightlyCompressedByteBufWriter.class.getSimpleName(), + ((Function) SlightlyCompressedByteBufWriter::new) + ) + ); + } +} diff --git a/tweed5-minecraft/settings.gradle.kts b/tweed5-minecraft/settings.gradle.kts index b22026b..c3ca759 100644 --- a/tweed5-minecraft/settings.gradle.kts +++ b/tweed5-minecraft/settings.gradle.kts @@ -56,6 +56,7 @@ includeNormalModule("bundle-pojo-weaving") includeNormalModule("coat-bridge") includeNormalModule("fabric-helper") includeNormalModule("logging") +includeNormalModule("networking") fun includeNormalModule(name: String) { includeAs("tweed5-$name", name)