feat(networking): Introduce Minecraft networking support with basic data reader and writers
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
`java-library`
|
`java-library`
|
||||||
jacoco
|
|
||||||
id("io.freefair.lombok")
|
id("io.freefair.lombok")
|
||||||
|
id("de.siphalor.tweed5.unit-tests")
|
||||||
id("de.siphalor.tweed5.publishing")
|
id("de.siphalor.tweed5.publishing")
|
||||||
id("de.siphalor.tweed5.local-runtime-only")
|
id("de.siphalor.tweed5.local-runtime-only")
|
||||||
id("de.siphalor.tweed5.expanded-sources-jar")
|
id("de.siphalor.tweed5.expanded-sources-jar")
|
||||||
@@ -13,12 +13,6 @@ java {
|
|||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
val testAgent = configurations.dependencyScope("mockitoAgent")
|
|
||||||
val testAgentClasspath = configurations.resolvable("testAgentClasspath") {
|
|
||||||
isTransitive = false
|
|
||||||
extendsFrom(testAgent.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
lombok {
|
lombok {
|
||||||
version = libs.versions.lombok.get()
|
version = libs.versions.lombok.get()
|
||||||
}
|
}
|
||||||
@@ -39,40 +33,9 @@ dependencies {
|
|||||||
testImplementation(libs.acl)
|
testImplementation(libs.acl)
|
||||||
testImplementation(libs.slf4j.rt)
|
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"))
|
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 {
|
publishing {
|
||||||
publications {
|
publications {
|
||||||
create<MavenPublication>("lib") {
|
create<MavenPublication>("lib") {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import java.util.Properties
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("fabric-loom")
|
id("fabric-loom")
|
||||||
|
id("de.siphalor.tweed5.unit-tests")
|
||||||
id("de.siphalor.tweed5.publishing")
|
id("de.siphalor.tweed5.publishing")
|
||||||
id("de.siphalor.tweed5.expanded-sources-jar")
|
id("de.siphalor.tweed5.expanded-sources-jar")
|
||||||
id("de.siphalor.jcyo")
|
id("de.siphalor.jcyo")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
fabric-loader = "0.17.2"
|
fabric-loader = "0.17.2"
|
||||||
fabric-loom = "1.11-SNAPSHOT"
|
fabric-loom = "1.11-SNAPSHOT"
|
||||||
jcyo = "0.5.1"
|
jcyo = "0.6.1"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
jcyo = { id = "de.siphalor.jcyo", version.ref = "jcyo" }
|
jcyo = { id = "de.siphalor.jcyo", version.ref = "jcyo" }
|
||||||
|
|||||||
13
tweed5-minecraft/networking/build.gradle.kts
Normal file
13
tweed5-minecraft/networking/build.gradle.kts
Normal file
@@ -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")
|
||||||
|
}
|
||||||
2
tweed5-minecraft/networking/gradle.properties
Normal file
2
tweed5-minecraft/networking/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module.name = Tweed 5 Netwoking
|
||||||
|
module.description = Minecraft networking support for Tweed 5
|
||||||
@@ -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<Context> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@NullMarked
|
||||||
|
package de.siphalor.tweed5.minecraft.networking.api;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@NullMarked
|
||||||
|
package de.siphalor.tweed5.minecraft.networking.impl;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
@@ -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<ByteBuf, TweedDataWriter> 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<Arguments> testParams() {
|
||||||
|
return Stream.of(
|
||||||
|
argumentSet(
|
||||||
|
RawByteBufWriter.class.getSimpleName(),
|
||||||
|
((Function<ByteBuf, ?>) RawByteBufWriter::new)
|
||||||
|
),
|
||||||
|
argumentSet(
|
||||||
|
SlightlyCompressedByteBufWriter.class.getSimpleName(),
|
||||||
|
((Function<ByteBuf, ?>) SlightlyCompressedByteBufWriter::new)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ includeNormalModule("bundle-pojo-weaving")
|
|||||||
includeNormalModule("coat-bridge")
|
includeNormalModule("coat-bridge")
|
||||||
includeNormalModule("fabric-helper")
|
includeNormalModule("fabric-helper")
|
||||||
includeNormalModule("logging")
|
includeNormalModule("logging")
|
||||||
|
includeNormalModule("networking")
|
||||||
|
|
||||||
fun includeNormalModule(name: String) {
|
fun includeNormalModule(name: String) {
|
||||||
includeAs("tweed5-$name", name)
|
includeAs("tweed5-$name", name)
|
||||||
|
|||||||
Reference in New Issue
Block a user