diff --git a/.gitignore b/.gitignore
index 9c0b3167..067c4ea8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,5 @@ run/*
changelog.txt
*.class
+
+src/generated/
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 9382b8e6..0c6f567f 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -11,24 +11,6 @@
-
-
-
-
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 7af1fbdd..513f11ac 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -2,6 +2,10 @@
+
+
+
+
diff --git a/README.md b/README.md
index 0c326026..28a85a02 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,14 @@ bridges.
1. Download multiconnect from the [releases page](https://github.com/Earthcomputer/multiconnect/releases)
and move it to the mods folder (`.minecraft/mods`).
+## Build Instructions
+1. Building requires JDK17.
+2.
+ 1. On Windows, run `gradlew build`
+ 2. On Linux and MacOS, run `./gradlew build`
+ 3. Note: sometimes, especially on development versions, the tests may fail. To skip the tests, use `./gradlew build -x test`
+3. The JAR file can be found in `build/libs` (it's the one with the shortest name).
+
## Installation for Mod Developers
This section is for when you are developing your own mod and want to use the multiconnect API, or run multiconnect alongside your mod in the IDE. Aside from the first step, you ONLY need to follow the steps applicable to you and your mod.
1. Explicitly setting a repository is not necessary, as multiconnect is hosted on Maven Central.
@@ -65,26 +73,4 @@ This section is for when you are developing your own mod and want to use the mul
- Note: this step is only necessary if you want to run the full mod in the IDE. Otherwise you can skip this step.
## Contributing
-1. Clone the repository
- ```
- git clone https://github.com/Earthcomputer/multiconnect
- cd multiconnect
- ```
-1. Generate the Minecraft source code
- ```
- ./gradlew genSources
- ```
- - Note: on Windows, use `gradlew` rather than `./gradlew`.
-1. Import the project into your preferred IDE.
- 1. If you use IntelliJ (the preferred option), you can simply import the project as a Gradle project.
- 1. If you use Eclipse, you need to `./gradlew eclipse` before importing the project as an Eclipse project.
-1. Edit the code.
-1. After testing in the IDE, build a JAR to test whether it works outside the IDE too
- ```
- ./gradlew build
- ```
- The mod JAR may be found in the `build/libs` directory
-1. [Create a pull request](https://help.github.com/en/articles/creating-a-pull-request)
- so that your changes can be integrated into multiconnect
- - Note: for large contributions, create an issue before doing all that
- work, to ask whether your pull request is likely to be accepted
+See [contributing.md](docs/contributing.md)
diff --git a/annotationProcessor/build.gradle.kts b/annotationProcessor/build.gradle.kts
new file mode 100644
index 00000000..f673537e
--- /dev/null
+++ b/annotationProcessor/build.gradle.kts
@@ -0,0 +1,19 @@
+
+plugins {
+ kotlin("jvm") version "1.6.10"
+ kotlin("plugin.serialization") version "1.6.10"
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation(project(":annotations"))
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
+ implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10")
+}
+
+tasks.withType() {
+ kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/ErrorConsumer.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/ErrorConsumer.kt
new file mode 100644
index 00000000..18d3e329
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/ErrorConsumer.kt
@@ -0,0 +1,7 @@
+package net.earthcomputer.multiconnect.ap
+
+import javax.lang.model.element.Element
+
+interface ErrorConsumer {
+ fun report(message: String, element: Element)
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageProcessor.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageProcessor.kt
new file mode 100644
index 00000000..5c876178
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageProcessor.kt
@@ -0,0 +1,29 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.encodeToString
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.TypeElement
+import javax.tools.StandardLocation
+
+object MessageProcessor {
+ fun process(type: Element, errorConsumer: ErrorConsumer, processingEnv: ProcessingEnvironment) {
+ if (type !is TypeElement) return
+ if (type.kind != ElementKind.INTERFACE) {
+ errorConsumer.report("@Message type must be an interface", type)
+ return
+ }
+
+ val packageName = processingEnv.elementUtils.getPackageOf(type).qualifiedName.toString()
+ val jsonFile = processingEnv.filer.createResource(
+ StandardLocation.CLASS_OUTPUT,
+ packageName,
+ type.qualifiedName.toString().substring(packageName.length + 1) + ".json"
+ )
+ val messageType = MessageType()
+ jsonFile.openWriter().use { writer ->
+ writer.write(JSON.encodeToString(messageType))
+ }
+ }
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageType.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageType.kt
new file mode 100644
index 00000000..060efaa6
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageType.kt
@@ -0,0 +1,10 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.EncodeDefault
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+
+@Serializable
+class MessageType @OptIn(ExperimentalSerializationApi::class) constructor(
+ @EncodeDefault(EncodeDefault.Mode.ALWAYS) val type: String = "message"
+)
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageVariantProcessor.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageVariantProcessor.kt
new file mode 100644
index 00000000..d8f92943
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageVariantProcessor.kt
@@ -0,0 +1,567 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.encodeToString
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.tools.StandardLocation
+
+object MessageVariantProcessor {
+ fun process(type: Element, errorConsumer: ErrorConsumer, processingEnv: ProcessingEnvironment) {
+ if (type !is TypeElement) return
+ if (type.interfaces.size > 1) {
+ errorConsumer.report("@MessageVariant cannot implement multiple interfaces", type)
+ return
+ }
+
+ val messageVariant = type.getAnnotation(MessageVariant::class) ?: return
+ val variantOf = type.interfaces.firstOrNull()
+ val minVersion = messageVariant.minVersion.takeIf { it != -1 }
+ val maxVersion = messageVariant.maxVersion.takeIf { it != -1 }
+
+ if (variantOf != null) {
+ if (!variantOf.isMessage) {
+ errorConsumer.report("variantOf must refer to a @Message type", type)
+ return
+ }
+ }
+ if (minVersion != null && maxVersion != null && maxVersion < minVersion) {
+ errorConsumer.report("maxVersion < minVersion", type)
+ return
+ }
+
+ val multiconnectFields = mutableListOf()
+ val multiconnectFunctions = mutableListOf()
+
+ // check type is constructable
+ val defaultConstruct = type.getAnnotation(DefaultConstruct::class)
+ if (!type.isPolymorphicRoot) {
+ if (defaultConstruct == null && !type.isConstructable(processingEnv)) {
+ errorConsumer.report("@MessageVariant type must be constructable", type)
+ return
+ }
+ if (type.polymorphicParent != null) {
+ val polymorphic = type.getAnnotation(Polymorphic::class)
+ if (polymorphic != null) {
+ if (polymorphic.condition.isNotEmpty()) {
+ val func = type.findMulticonnectFunction(processingEnv, polymorphic.condition)
+ if (func != null) {
+ multiconnectFunctions += func
+ }
+ }
+ }
+ }
+ }
+
+ if (defaultConstruct != null) {
+ fun validateDefaultConstruct(): Boolean {
+ val compute = defaultConstruct.compute
+ val hasSubType = !toTypeMirror { defaultConstruct.subType }.hasQualifiedName(JAVA_LANG_OBJECT)
+ if (compute.isEmpty()) {
+ if (hasSubType && type.isPolymorphicRoot) {
+ return true
+ }
+ errorConsumer.report("A default construct computation must be specified", type)
+ return false
+ }
+ if (defaultConstruct.booleanValue || defaultConstruct.intValue.isNotEmpty() || defaultConstruct.doubleValue.isNotEmpty() || defaultConstruct.stringValue.isNotEmpty() || hasSubType) {
+ errorConsumer.report("Cannot specify a primitive default value", type)
+ return false
+ }
+ val multiconnectFunction = type.findMulticonnectFunction(
+ processingEnv,
+ compute,
+ errorConsumer = errorConsumer,
+ errorElement = type
+ ) ?: return false
+ if (!processingEnv.typeUtils.isAssignable(multiconnectFunction.returnType, type.asType())) {
+ errorConsumer.report("Default construct function return type must be assignable to the default constructed type", type)
+ return false
+ }
+ if (multiconnectFunction.positionalParameters.isNotEmpty()) {
+ errorConsumer.report("Default construct function cannot have positional parameters", type)
+ return false
+ }
+ if (multiconnectFunction.parameters.any { it is MulticonnectParameter.Argument }) {
+ errorConsumer.report("Default construct function for type cannot accept captured @Arguments", type)
+ return false
+ }
+ return true
+ }
+ if (!validateDefaultConstruct()) {
+ return
+ }
+ }
+
+ val recordFields = type.recordFields
+ fieldLoop@
+ for ((fieldIndex, field) in recordFields.withIndex()) {
+ if (!field.hasModifier(Modifier.PUBLIC) || field.hasModifier(Modifier.FINAL)) {
+ errorConsumer.report("@MessageVariant field must be public and non-final", field)
+ continue
+ }
+ val multiconnectType = field.getMulticonnectType(processingEnv)
+ if (multiconnectType == null) {
+ errorConsumer.report("Invalid @MessageVariant field type", field)
+ continue
+ }
+ multiconnectFields += MulticonnectField(field.simpleName.toString(), multiconnectType)
+ val deepComponentType = multiconnectType.realType.deepComponentType(processingEnv)
+
+ if (!MulticonnectType.isWireTypeCompatible(deepComponentType, multiconnectType.wireType)) {
+ errorConsumer.report("Type ${multiconnectType.realType} is not compatible with declared wire type ${multiconnectType.wireType.name}", field)
+ continue
+ }
+
+ if (multiconnectType.registry != null) {
+ if (!MulticonnectType.isRegistryCompatible(deepComponentType)) {
+ errorConsumer.report("@Registry must only be used on registry compatible types", field)
+ continue
+ }
+ }
+
+ if (multiconnectType.lengthInfo != null) {
+ if (!multiconnectType.lengthInfo.raw && !multiconnectType.realType.hasLength) {
+ errorConsumer.report("Non-raw @Length can only be used on fields with a length", field)
+ continue
+ }
+ if (!multiconnectType.lengthInfo.type.isIntegral) {
+ errorConsumer.report("@Length type must be integral", field)
+ continue
+ }
+ if (count(
+ multiconnectType.lengthInfo.remainingBytes,
+ multiconnectType.lengthInfo.compute.isNotEmpty(),
+ multiconnectType.lengthInfo.constant != -1,
+ multiconnectType.lengthInfo.raw
+ ) > 1) {
+ errorConsumer.report("@Length cannot have more than one way to compute the length", field)
+ continue
+ }
+ if (multiconnectType.lengthInfo.remainingBytes) {
+ if (multiconnectType.wireType != Types.BYTE
+ || !multiconnectType.realType.hasLength
+ || multiconnectType.realType.componentType(processingEnv).kind != TypeKind.BYTE) {
+ errorConsumer.report("@Length(remainingBytes = true) can only be applied to byte arrays", field)
+ continue
+ }
+ if (fieldIndex != recordFields.lastIndex) {
+ errorConsumer.report("@Length(remainingBytes = true) can only be applied to the last field", field)
+ continue
+ }
+ }
+ if (multiconnectType.lengthInfo.constant < -1) {
+ errorConsumer.report("@Length must not be negative", field)
+ continue
+ }
+ if (multiconnectType.lengthInfo.compute.isNotEmpty()) {
+ val multiconnectFunction = type.findMulticonnectFunction(
+ processingEnv,
+ multiconnectType.lengthInfo.compute,
+ errorConsumer = errorConsumer,
+ errorElement = field
+ ) ?: continue
+ if (!multiconnectFunction.returnType.isIntegral) {
+ errorConsumer.report("Length computation must return an integer", field)
+ continue
+ }
+ if (multiconnectFunction.positionalParameters.isNotEmpty()) {
+ errorConsumer.report("Length computation cannot have any positional parameters", field)
+ continue
+ }
+ if (!validateFunctionCaptures(multiconnectFunction, type, field)) {
+ errorConsumer.report("Length computation cannot depend on later fields", field)
+ continue
+ }
+ multiconnectFunctions += multiconnectFunction
+ }
+ }
+
+ if (multiconnectType.defaultConstructInfo != null) {
+ val defaultInfo = validateDefaultInfo(
+ type,
+ processingEnv,
+ multiconnectType,
+ DefaultInfo(type, multiconnectType.defaultConstructInfo),
+ errorConsumer,
+ field
+ ) ?: continue
+ if (defaultInfo is MulticonnectFunction) {
+ if (!validateFunctionCaptures(defaultInfo, type, field)) {
+ errorConsumer.report("Default construct computation cannot depend on later fields", field)
+ continue
+ }
+ multiconnectFunctions += defaultInfo
+ }
+ }
+
+ if (multiconnectType.onlyIf != null) {
+ val multiconnectFunction = type.findMulticonnectFunction(
+ processingEnv,
+ multiconnectType.onlyIf,
+ errorConsumer = errorConsumer,
+ errorElement = field
+ ) ?: continue
+ if (multiconnectFunction.returnType.kind != TypeKind.BOOLEAN) {
+ errorConsumer.report("@OnlyIf function must return boolean", field)
+ continue
+ }
+ if (multiconnectFunction.positionalParameters.isNotEmpty()) {
+ errorConsumer.report("@OnlyIf function cannot have any positional parameters", field)
+ continue
+ }
+ if (!validateFunctionCaptures(multiconnectFunction, type, field)) {
+ errorConsumer.report("@OnlyIf computation cannot depend on later fields", field)
+ continue
+ }
+ multiconnectFunctions += multiconnectFunction
+ }
+
+ if (multiconnectType.datafixInfo != null) {
+ if (!deepComponentType.hasQualifiedName(MINECRAFT_NBT_COMPOUND)) {
+ errorConsumer.report("@Datafix can only be used on fields of type NbtCompound", field)
+ continue
+ }
+ if (multiconnectType.datafixInfo.preprocess.isNotEmpty()) {
+ val multiconnectFunction = type.findMulticonnectFunction(
+ processingEnv,
+ multiconnectType.datafixInfo.preprocess,
+ errorConsumer = errorConsumer,
+ errorElement = field
+ ) ?: continue
+ if (multiconnectFunction.positionalParameters.size != 1) {
+ errorConsumer.report("@Datafix preprocess function must have exactly 1 positional parameter", field)
+ continue
+ }
+ if (!multiconnectFunction.positionalParameters.first().hasQualifiedName(MINECRAFT_NBT_COMPOUND)) {
+ errorConsumer.report("@Datafix preprocess function positional parameter must be of type NbtCompound", field)
+ continue
+ }
+ if (!validateFunctionCaptures(multiconnectFunction, type, field)) {
+ errorConsumer.report("@Datafix preprocess function cannot depend on later fields", field)
+ continue
+ }
+ multiconnectFunctions += multiconnectFunction
+ }
+ }
+
+ if (multiconnectType.polymorphicBy != null) {
+ val polymorphicType = deepComponentType.asTypeElement()
+ if (polymorphicType?.isPolymorphicRoot != true && polymorphicType?.isMessage != true) {
+ errorConsumer.report("@PolymorphicBy can only be used on fields of a polymorphic root type", field)
+ continue
+ }
+ val polymorphicByField = recordFields.firstOrNull { it.simpleName.contentEquals(multiconnectType.polymorphicBy) }
+ if (polymorphicByField == null) {
+ errorConsumer.report("Cannot resolve field \"${multiconnectType.polymorphicBy}\"", field)
+ continue
+ }
+ if (recordFields.indexOf(polymorphicByField) >= fieldIndex) {
+ errorConsumer.report("@PolymorphicBy field must be before the target field", field)
+ continue
+ }
+ if (!polymorphicType.isMessage) {
+ val typeField = polymorphicType.recordFields.firstOrNull() ?: continue
+ if (!processingEnv.typeUtils.isAssignable(polymorphicByField.asType(), typeField.asType())) {
+ errorConsumer.report("@PolymorphicBy field must be assignable to the type field", field)
+ continue
+ }
+ }
+ }
+
+ if (multiconnectType.introduce.distinctBy { it.direction }.size != multiconnectType.introduce.size) {
+ errorConsumer.report("Duplicate @Introduce directions", field)
+ continue
+ }
+ for (introduce in multiconnectType.introduce) {
+ // TODO: move validate of @Introduce into the compiler step
+// if (introduce.direction == Introduce.Direction.AUTO && translateFromNewer != null && translateFromOlder != null) {
+// errorConsumer.report("Ambiguous AUTO @Introduce direction", field)
+// continue@fieldLoop
+// }
+// val translateFromType = when (introduce.direction) {
+// Introduce.Direction.AUTO -> translateFromNewer?.let { toTypeMirror { it.type } } ?: translateFromOlder?.let { toTypeMirror { it.type } }
+// Introduce.Direction.FROM_NEWER -> translateFromNewer?.let { toTypeMirror { it.type } }
+// Introduce.Direction.FROM_OLDER -> translateFromOlder?.let { toTypeMirror { it.type } }
+// }?.asTypeElement()
+// if (translateFromType == null) {
+// errorConsumer.report("There is no type to translate from in that direction", field)
+// continue@fieldLoop
+// }
+// val defaultInfo = validateDefaultInfo(
+// type,
+// processingEnv,
+// multiconnectType,
+// DefaultInfo(translateFromType, introduce),
+// errorConsumer,
+// field
+// ) ?: continue@fieldLoop
+ if (introduce.compute.isNotEmpty()) {
+ val function = type.findMulticonnectFunction(
+ processingEnv,
+ introduce.compute,
+ argumentResolveContext = null,
+ errorConsumer = errorConsumer,
+ errorElement = field
+ )
+ if (function != null) {
+ multiconnectFunctions += function
+ }
+ }
+ }
+ if (multiconnectType.customFix.distinctBy { it.direction }.size != multiconnectType.customFix.size) {
+ errorConsumer.report("Duplicate @CustomFix directions", field)
+ continue
+ }
+ for (customFix in multiconnectType.customFix) {
+ val function = type.findMulticonnectFunction(
+ processingEnv,
+ customFix.value,
+ errorConsumer = null,
+ errorElement = null,
+ )
+ if (function != null) {
+ if (function.positionalParameters.size != 1) {
+ errorConsumer.report("@CustomFix function must have exactly 1 positional parameter", field)
+ continue
+ }
+ if (!processingEnv.typeUtils.isSameType(function.positionalParameters.first(), multiconnectType.realType)) {
+ errorConsumer.report("@CustomFix function positional parameter must have the same type as the field", field)
+ continue
+ }
+ if (!processingEnv.typeUtils.isSameType(function.returnType, multiconnectType.realType)) {
+ errorConsumer.report("@CustomFix function return type must have the same type as the field", field)
+ continue
+ }
+ if (!validateFunctionCaptures(function, type, field)) {
+ errorConsumer.report("@CustomFix function cannot depend on later fields", field)
+ continue
+ }
+ multiconnectFunctions += function
+ }
+ }
+ }
+
+ checkForRecursiveTypes(type, type, mutableSetOf(), mutableMapOf(), errorConsumer, processingEnv)
+
+ val handler = type.handler
+ val handlerProtocol = handler?.getAnnotation(Handler::class.java)?.protocol?.takeIf { it != -1 }
+ if (handler != null) {
+ val multiconnectFunction = type.findMulticonnectFunction(
+ processingEnv,
+ handler.simpleName.toString(),
+ errorConsumer = errorConsumer,
+ errorElement = handler
+ )
+ if (multiconnectFunction != null) {
+ multiconnectFunctions += multiconnectFunction
+ }
+ }
+ val partialHandlers = type.getPartialHandlers(processingEnv, errorConsumer, type)
+ for (partialHandler in partialHandlers) {
+ multiconnectFunctions += partialHandler
+ }
+
+ val packageName = processingEnv.elementUtils.getPackageOf(type).qualifiedName.toString()
+ val jsonFile = processingEnv.filer.createResource(
+ StandardLocation.CLASS_OUTPUT,
+ packageName,
+ type.qualifiedName.toString().substring(packageName.length + 1) + ".json"
+ )
+ val messageVariantType = MessageVariantType(
+ multiconnectFields,
+ multiconnectFunctions,
+ type.polymorphicParent?.qualifiedName?.toString(),
+ type.getAnnotation(Polymorphic::class),
+ defaultConstruct,
+ handler?.simpleName?.toString(),
+ handlerProtocol,
+ partialHandlers.map { it.name },
+ variantOf?.asTypeElement()?.qualifiedName?.toString(),
+ minVersion,
+ maxVersion,
+ type.getAnnotation(Sendable::class)?.from?.toList(),
+ type.getAnnotation(Sendable::class)?.fromLatest ?: false,
+ type.hasAnnotation(ExplicitConstructible::class),
+ messageVariant.tailrec
+ )
+ jsonFile.openWriter().use { writer ->
+ writer.write(JSON.encodeToString(messageVariantType))
+ }
+ }
+
+ private fun checkForRecursiveTypes(
+ originalType: TypeElement,
+ type: TypeElement,
+ seenTypes: MutableSet,
+ polymorphicSubclasses: MutableMap>,
+ errorConsumer: ErrorConsumer,
+ processingEnv: ProcessingEnvironment
+ ) {
+ type.polymorphicParent?.let {
+ polymorphicSubclasses.computeIfAbsent(it.qualifiedName.toString()) { mutableSetOf() } += type.qualifiedName.toString()
+ }
+ val fields = type.recordFields
+ for ((index, field) in fields.withIndex()) {
+ val fieldType = field.asType().asTypeElement()
+ if (fieldType?.isMessageVariant != true) continue
+ val fieldTypeName = fieldType.qualifiedName.toString()
+ if (fieldTypeName in seenTypes) {
+ if (type.qualifiedName != originalType.qualifiedName) continue
+ if (index == fields.lastIndex) {
+ if (type.qualifiedName.contentEquals(fieldTypeName)) {
+ if (type.getAnnotation(MessageVariant::class)?.tailrec == true
+ && field.hasAnnotation(OnlyIf::class)
+ && !type.isPolymorphicRoot) {
+ continue
+ }
+ } else {
+ val polymorphicParent = type.polymorphicParent
+ if (polymorphicParent != null) {
+ if (polymorphicParent.qualifiedName.contentEquals(fieldTypeName)) {
+ if (polymorphicParent.getAnnotation(MessageVariant::class)?.tailrec == true) {
+ continue
+ }
+ }
+ }
+ }
+ }
+ errorConsumer.report("MessageVariant is self-referential and not marked as tailrec", field)
+ } else {
+ seenTypes += fieldTypeName
+ checkForRecursiveTypes(originalType, fieldType, seenTypes, polymorphicSubclasses, errorConsumer, processingEnv)
+ seenTypes.remove(fieldTypeName)
+ }
+ }
+ polymorphicSubclasses[type.qualifiedName.toString()]?.let { subclasses ->
+ for (subclass in subclasses) {
+ if (subclass in seenTypes) continue
+ val subType = processingEnv.elementUtils.getTypeElement(subclass) ?: continue
+ seenTypes += subclass
+ checkForRecursiveTypes(originalType, subType, seenTypes, polymorphicSubclasses, errorConsumer, processingEnv)
+ seenTypes.remove(subclass)
+ }
+ }
+ }
+
+ private fun validateFunctionCaptures(
+ multiconnectFunction: MulticonnectFunction,
+ type: TypeElement,
+ field: VariableElement
+ ): Boolean {
+ val allRecordFields = type.allRecordFields
+ val fieldIndex = allRecordFields.indexOf(field)
+ for (parameter in multiconnectFunction.parameters) {
+ if (parameter is MulticonnectParameter.Argument) {
+ val dependentFieldIndex = allRecordFields.indexOfFirst { it.simpleName.contentEquals(parameter.name) }
+ if (dependentFieldIndex >= fieldIndex) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+
+ class DefaultInfo(
+ val booleanValue: Boolean,
+ val intValue: LongArray,
+ val doubleValue: DoubleArray,
+ val stringValue: Array,
+ val subType: TypeMirror?,
+ val defaultConstruct: Boolean,
+ val compute: String,
+ val computeContext: TypeElement
+ ) {
+ constructor(enclosingType: TypeElement, a: DefaultConstruct)
+ : this(a.booleanValue, a.intValue, a.doubleValue,a.stringValue, toTypeMirror { a.subType }, false, a.compute, enclosingType)
+ constructor(introducedFrom: TypeElement, a: Introduce)
+ : this(a.booleanValue, a.intValue, a.doubleValue, a.stringValue, null, a.defaultConstruct, a.compute, introducedFrom)
+ object DefaultConstructSenetial
+ }
+
+ private fun validateDefaultInfo(
+ enclosingType: TypeElement,
+ processingEnv: ProcessingEnvironment,
+ multiconnectType: MulticonnectType,
+ defaultInfo: DefaultInfo,
+ errorConsumer: ErrorConsumer,
+ errorElement: Element
+ ): Any? {
+ val explicits = mutableListOf()
+ if (multiconnectType.realType.kind == TypeKind.BOOLEAN) {
+ explicits += defaultInfo.booleanValue
+ }
+ explicits.addAll(defaultInfo.intValue.toList())
+ explicits.addAll(defaultInfo.doubleValue.toList())
+ explicits.addAll(defaultInfo.stringValue.toList())
+ if (defaultInfo.subType?.hasQualifiedName(JAVA_LANG_OBJECT) == false) {
+ explicits += defaultInfo.subType
+ }
+ if (defaultInfo.compute.isNotEmpty()) {
+ if (defaultInfo.defaultConstruct || explicits.any { it != false }) {
+ errorConsumer.report("Cannot specify both compute and a default value", errorElement)
+ return null
+ }
+ val multiconnectFunction = enclosingType.findMulticonnectFunction(
+ processingEnv,
+ defaultInfo.compute,
+ argumentResolveContext = defaultInfo.computeContext,
+ errorConsumer = errorConsumer,
+ errorElement = errorElement
+ ) ?: return null
+ if (!processingEnv.typeUtils.isAssignable(multiconnectFunction.returnType, multiconnectType.realType)) {
+ errorConsumer.report("Compute function must return a value assignable to the field", errorElement)
+ return null
+ }
+ if (multiconnectFunction.positionalParameters.isNotEmpty()) {
+ errorConsumer.report("Compute function must not have any positional parameters", errorElement)
+ return null
+ }
+ return multiconnectFunction
+ }
+ if (defaultInfo.defaultConstruct) {
+ if (explicits.any { it != false }) {
+ errorConsumer.report("Cannot specify both default construct and a default value", errorElement)
+ return null
+ }
+ return DefaultInfo.DefaultConstructSenetial
+ }
+ val singleVal = explicits.singleOrNull()
+ if (singleVal == null) {
+ errorConsumer.report("Must only specify exactly one default value", errorElement)
+ return null
+ }
+ val compatibleTypes = when {
+ multiconnectType.canCoerceFromString() ->
+ singleVal is String
+ multiconnectType.realType.kind == TypeKind.BOOLEAN ->
+ singleVal is Boolean
+ multiconnectType.realType.isIntegral ->
+ singleVal is Long
+ multiconnectType.realType.isFloatingPoint ->
+ singleVal is Double
+ multiconnectType.realType.asTypeElement()?.isPolymorphicRoot == true -> {
+ if (singleVal !is TypeMirror) {
+ false
+ } else {
+ singleVal.asTypeElement()?.polymorphicParent?.let { polymorphicParent ->
+ processingEnv.typeUtils.isSameType(polymorphicParent.asType(), multiconnectType.realType)
+ } ?: false
+ }
+ }
+ else -> {
+ errorConsumer.report("No way to default construct this type was specified", errorElement)
+ return null
+ }
+ }
+ if (!compatibleTypes) {
+ errorConsumer.report("Default construct value is incompatible with the field type", errorElement)
+ return null
+ }
+ return singleVal
+ }
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageVariantType.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageVariantType.kt
new file mode 100644
index 00000000..cc109f51
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MessageVariantType.kt
@@ -0,0 +1,26 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.EncodeDefault
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+
+@Serializable
+class MessageVariantType @OptIn(ExperimentalSerializationApi::class) constructor(
+ val fields: List,
+ val functions: List,
+ val polymorphicParent: String?,
+ @Contextual val polymorphic: Polymorphic?,
+ @Contextual val defaultConstruct: DefaultConstruct?,
+ val handler: String?,
+ val handlerProtocol: Int?,
+ val partialHandlers: List,
+ val variantOf: String?,
+ val minVersion: Int?,
+ val maxVersion: Int?,
+ val sendableFrom: List?,
+ val sendableFromLatest: Boolean,
+ val explicitConstructible: Boolean,
+ val tailrec: Boolean = false,
+ @EncodeDefault(EncodeDefault.Mode.ALWAYS) val type: String = "messageVariant"
+)
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectAP.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectAP.kt
new file mode 100644
index 00000000..bc0000cd
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectAP.kt
@@ -0,0 +1,42 @@
+package net.earthcomputer.multiconnect.ap
+
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.RoundEnvironment
+import javax.annotation.processing.SupportedAnnotationTypes
+import javax.annotation.processing.SupportedSourceVersion
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.Element
+import javax.lang.model.element.TypeElement
+import javax.tools.Diagnostic
+
+@SupportedAnnotationTypes("net.earthcomputer.multiconnect.ap.*")
+@SupportedSourceVersion(SourceVersion.RELEASE_16)
+class MulticonnectAP : AbstractProcessor(), ErrorConsumer {
+ override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean {
+ if (annotations.any { it.qualifiedName.contentEquals(messageName) }) {
+ for (element in roundEnv.getElementsAnnotatedWith(Message::class.java)) {
+ MessageProcessor.process(element, this, processingEnv)
+ }
+ }
+ if (annotations.any { it.qualifiedName.contentEquals(messageVariantName) }) {
+ for (element in roundEnv.getElementsAnnotatedWith(MessageVariant::class.java)) {
+ MessageVariantProcessor.process(element, this, processingEnv)
+ }
+ }
+ if (annotations.any { it.qualifiedName.contentEquals(polymorphicName) }) {
+ for (element in roundEnv.getElementsAnnotatedWith(Polymorphic::class.java)) {
+ PolymorphicProcessor.process(element, this, processingEnv)
+ }
+ }
+ if (annotations.any { it.qualifiedName.contentEquals(networkEnumName) }) {
+ for (element in roundEnv.getElementsAnnotatedWith(NetworkEnum::class.java)) {
+ NetworkEnumProcessor.process(element, this, processingEnv)
+ }
+ }
+ return true
+ }
+
+ override fun report(message: String, element: Element) {
+ processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, message, element)
+ }
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectField.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectField.kt
new file mode 100644
index 00000000..2eb6f8f9
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectField.kt
@@ -0,0 +1,9 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class MulticonnectField(
+ val name: String,
+ val type: MulticonnectType
+)
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectFunction.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectFunction.kt
new file mode 100644
index 00000000..33700d01
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectFunction.kt
@@ -0,0 +1,42 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import javax.lang.model.type.TypeMirror
+
+@Serializable
+data class MulticonnectFunction(
+ val name: String,
+ val returnType: TypeMirror,
+ val positionalParameters: List,
+ val parameters: List,
+ val possibleReturnTypes: List?,
+)
+
+@Serializable
+sealed class MulticonnectParameter {
+ abstract val paramType: TypeMirror
+
+ @Serializable
+ @SerialName("argument")
+ class Argument(override val paramType: TypeMirror, val name: String, val translate: Boolean): MulticonnectParameter()
+ @Serializable
+ @SerialName("defaultConstructed")
+ class DefaultConstructed(override val paramType: TypeMirror): MulticonnectParameter()
+ @Serializable
+ @SerialName("suppliedDefaultConstructed")
+ class SuppliedDefaultConstructed(override val paramType: TypeMirror, val suppliedType: TypeMirror): MulticonnectParameter()
+ @Serializable
+ @SerialName("filled")
+ class Filled(
+ override val paramType: TypeMirror,
+ @Contextual val fromRegistry: FilledArgument.FromRegistry?,
+ val fromVersion: Int?,
+ val toVersion: Int?,
+ ): MulticonnectParameter()
+
+ @Serializable
+ @SerialName("globalData")
+ class GlobalData(override val paramType: TypeMirror): MulticonnectParameter()
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectType.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectType.kt
new file mode 100644
index 00000000..c682ab56
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/MulticonnectType.kt
@@ -0,0 +1,100 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.Serializable
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+@Serializable
+data class MulticonnectType(
+ val realType: TypeMirror,
+ val wireType: Types,
+ val registry: Registries?,
+ @Contextual val lengthInfo: Length?,
+ @Contextual val defaultConstructInfo: DefaultConstruct?,
+ val onlyIf: String?,
+ @Contextual val datafixInfo: Datafix?,
+ val polymorphicBy: String?,
+ val introduce: List<@Contextual Introduce>,
+ val customFix: List<@Contextual CustomFix>,
+) {
+ fun isConstantRepresentable(): Boolean {
+ return realType.kind.isPrimitive
+ || realType.isEnum
+ || realType.hasQualifiedName(JAVA_LANG_STRING)
+ || realType.hasQualifiedName(MINECRAFT_IDENTIFIER)
+ }
+
+ fun canCoerceFromString(): Boolean {
+ return realType.isEnum
+ || realType.hasQualifiedName(JAVA_LANG_STRING)
+ || realType.hasQualifiedName(MINECRAFT_IDENTIFIER)
+ || registry != null
+ }
+
+ companion object {
+ fun isSupportedType(processingEnv: ProcessingEnvironment, realType: TypeMirror): Boolean {
+ return defaultWireType(realType.deepComponentType(processingEnv)) != null
+ }
+
+ fun defaultWireType(realType: TypeMirror): Types? {
+ if (realType.isEnum) {
+ return Types.VAR_INT
+ }
+ if (realType.isMessage || realType.isMessageVariant) {
+ return Types.MESSAGE
+ }
+ return when (realType.kind) {
+ TypeKind.BOOLEAN -> Types.BOOLEAN
+ TypeKind.BYTE -> Types.BYTE
+ TypeKind.SHORT -> Types.SHORT
+ TypeKind.INT -> Types.VAR_INT
+ TypeKind.LONG -> Types.VAR_LONG
+ TypeKind.FLOAT -> Types.FLOAT
+ TypeKind.DOUBLE -> Types.DOUBLE
+ TypeKind.DECLARED -> when(realType.asTypeElement()?.qualifiedName?.toString()) {
+ JAVA_LANG_STRING -> Types.STRING
+ JAVA_UTIL_BITSET -> Types.BITSET
+ JAVA_UTIL_UUID -> Types.UUID
+ MINECRAFT_IDENTIFIER -> Types.IDENTIFIER
+ MINECRAFT_NBT_COMPOUND -> Types.NBT_COMPOUND
+ else -> null
+ }
+ else -> null
+ }
+ }
+
+ fun isWireTypeCompatible(realType: TypeMirror, wireType: Types): Boolean {
+ val defaultWireType = defaultWireType(realType) ?: return false
+ if (defaultWireType.isIntegral && wireType.isIntegral) {
+ // make sure the default wire type is "wider" than the actual wire type
+ return INTEGRAL_SIZES[defaultWireType]!! >= INTEGRAL_SIZES[wireType]!!
+ }
+ return defaultWireType == wireType
+ }
+
+ fun isRegistryCompatible(realType: TypeMirror): Boolean {
+ return realType.isIntegral || realType.hasQualifiedName(MINECRAFT_IDENTIFIER)
+ }
+
+ fun canAutoFill(realType: TypeMirror): Boolean {
+ return realType.hasQualifiedName(MINECRAFT_NETWORK_HANDLER)
+ || realType.hasQualifiedName(MULTICONNECT_TYPED_MAP)
+ || realType.hasQualifiedName(MULTICONNECT_DELAYED_PACKET_SENDER)
+ }
+ }
+}
+
+private val INTEGRAL_SIZES = mapOf(
+ Types.BOOLEAN to 1,
+ Types.BYTE to 1,
+ Types.UNSIGNED_BYTE to 1,
+ Types.SHORT to 2,
+ Types.INT to 4,
+ Types.VAR_INT to 4,
+ Types.LONG to 8,
+ Types.VAR_LONG to 8
+)
+val Types.isIntegral: Boolean
+ get() = this in INTEGRAL_SIZES
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/NetworkEnumProcessor.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/NetworkEnumProcessor.kt
new file mode 100644
index 00000000..b939e9ae
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/NetworkEnumProcessor.kt
@@ -0,0 +1,39 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.TypeElement
+import javax.tools.StandardLocation
+
+object NetworkEnumProcessor {
+ fun process(enum: Element, errorConsumer: ErrorConsumer, processingEnv: ProcessingEnvironment) {
+ if (enum !is TypeElement) return
+ if (enum.kind != ElementKind.ENUM) {
+ errorConsumer.report("@NetworkEnum must be used on enums", enum)
+ return
+ }
+ val enumConstants = enum.enumConstants
+ if (enumConstants.isEmpty()) {
+ errorConsumer.report("@NetworkEnum must have at least one enum constant", enum)
+ return
+ }
+
+ val packageName = processingEnv.elementUtils.getPackageOf(enum).qualifiedName.toString()
+ val jsonFile = processingEnv.filer.createResource(
+ StandardLocation.CLASS_OUTPUT,
+ packageName,
+ enum.qualifiedName.toString().substring(packageName.length + 1) + ".json"
+ )
+ jsonFile.openWriter().use { writer ->
+ writer.write(JSON.encodeToString(JsonObject(mapOf(
+ "type" to JsonPrimitive("enum"),
+ "values" to JsonArray(enumConstants.map { JsonPrimitive(it.simpleName.toString()) })
+ ))))
+ }
+ }
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/PolymorphicProcessor.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/PolymorphicProcessor.kt
new file mode 100644
index 00000000..891641e3
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/PolymorphicProcessor.kt
@@ -0,0 +1,125 @@
+package net.earthcomputer.multiconnect.ap
+
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeKind
+
+object PolymorphicProcessor {
+ fun process(type: Element, errorConsumer: ErrorConsumer, processingEnv: ProcessingEnvironment) {
+ if (type !is TypeElement) return
+ if (!type.hasAnnotation(MessageVariant::class)) {
+ errorConsumer.report("@Polymorphic class must be annotation with @Message", type)
+ return
+ }
+ val polymorphic = type.getAnnotation(Polymorphic::class) ?: return
+
+ if (type.hasModifier(Modifier.ABSTRACT)) {
+ if (!type.isPolymorphicRoot) {
+ errorConsumer.report("Abstract @Polymorphic classes must extend Object", type)
+ return
+ }
+ val typeField = type.recordFields.firstOrNull()
+ if (typeField == null) {
+ errorConsumer.report("@Polymorphic root must contain a type field", type)
+ return
+ }
+ val multiconnectType = typeField.getMulticonnectType(processingEnv) ?: return
+ if (!multiconnectType.isConstantRepresentable()) {
+ errorConsumer.report("@Polymorphic type field must have constant representable type", typeField)
+ return
+ }
+
+ val defaultConstruct = type.getAnnotation(DefaultConstruct::class)
+ if (defaultConstruct != null) {
+ fun validateDefaultConstruct(): Boolean {
+ val subType = toTypeMirror { defaultConstruct.subType }.asTypeElement()
+ if (subType == null || subType.qualifiedName.contentEquals(JAVA_LANG_OBJECT)) {
+ if (defaultConstruct.compute.isNotEmpty()) {
+ return true
+ }
+ errorConsumer.report("@Polymorphic root default construct must specify a sub-type", type)
+ return false
+ }
+ val polymorphicParent = subType.polymorphicParent
+ if (polymorphicParent == null || !processingEnv.typeUtils.isSameType(polymorphicParent.asType(), type.asType())) {
+ errorConsumer.report("@Polymorphic root default construct must specify a valid sub-type", type)
+ return false
+ }
+ return true
+ }
+ if (!validateDefaultConstruct()) {
+ return
+ }
+ }
+ } else {
+ val polymorphicParent = type.polymorphicParent
+ if (polymorphicParent?.isPolymorphicRoot != true) {
+ errorConsumer.report("@Polymorphic subclasses must extend a @Polymorphic root class", type)
+ return
+ }
+ val typeField = polymorphicParent.recordFields.firstOrNull() ?: return
+ val multiconnectType = typeField.getMulticonnectType(processingEnv) ?: return
+
+ val matchValues = mutableListOf()
+ if (multiconnectType.realType.kind == TypeKind.BOOLEAN) {
+ matchValues += polymorphic.booleanValue
+ }
+ matchValues.addAll(polymorphic.intValue.toList())
+ matchValues.addAll(polymorphic.doubleValue.toList())
+ matchValues.addAll(polymorphic.stringValue.toList())
+
+ if (polymorphic.condition.isNotEmpty()) {
+ if (polymorphic.otherwise || matchValues.any { it != false }) {
+ errorConsumer.report("Cannot specify both a condition and a match value", type)
+ return
+ }
+ val multiconnectFunction = type.findMulticonnectFunction(
+ processingEnv,
+ polymorphic.condition,
+ errorConsumer = errorConsumer,
+ errorElement = type
+ ) ?: return
+ if (multiconnectFunction.returnType.kind != TypeKind.BOOLEAN) {
+ errorConsumer.report("Match condition must return boolean", type)
+ return
+ }
+ val positionalParam = multiconnectFunction.positionalParameters.singleOrNull()
+ if (positionalParam == null) {
+ errorConsumer.report("Match condition must have a single positional parameter", type)
+ return
+ }
+ if (!processingEnv.typeUtils.isAssignable(multiconnectType.realType, positionalParam)) {
+ errorConsumer.report("Match condition positional parameter must match the type of the type field", type)
+ return
+ }
+ if (multiconnectFunction.parameters.any { it is MulticonnectParameter.Argument }) {
+ errorConsumer.report("Match condition cannot capture @Arguments", type)
+ return
+ }
+ } else if (polymorphic.otherwise) {
+ if (matchValues.any { it != false }) {
+ errorConsumer.report("Cannot specify match values when otherwise = true", type)
+ return
+ }
+ } else {
+ if (matchValues.isEmpty()) {
+ errorConsumer.report("Must specify at least one match value", type)
+ return
+ }
+ val isValid = when {
+ multiconnectType.canCoerceFromString() -> matchValues.all { it is String }
+ multiconnectType.realType.kind == TypeKind.BOOLEAN -> matchValues.all { it is Boolean }
+ multiconnectType.realType.isIntegral -> matchValues.all { it is Long }
+ multiconnectType.realType.isFloatingPoint -> matchValues.all { it is Double }
+ else -> false
+ }
+ if (!isValid) {
+ errorConsumer.report("Match values must match the type field", type)
+ return
+ }
+ }
+ }
+ }
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/Serializers.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/Serializers.kt
new file mode 100644
index 00000000..b938b551
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/Serializers.kt
@@ -0,0 +1,186 @@
+package net.earthcomputer.multiconnect.ap
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.CompositeEncoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.encodeStructure
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import kotlinx.serialization.serializer
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Method
+import javax.lang.model.type.ArrayType
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlin.reflect.full.createType
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.full.starProjectedType
+import kotlin.reflect.jvm.jvmErasure
+
+object FromRegistrySerializer : AnnotationSerializer(FilledArgument.FromRegistry::class)
+object LengthSerializer : AnnotationSerializer(Length::class)
+object DefaultConstructSerializer : AnnotationSerializer(DefaultConstruct::class)
+object IntroduceSerializer : AnnotationSerializer(Introduce::class)
+object PolymorphicSerializer : AnnotationSerializer(Polymorphic::class)
+object DatafixSerializer : AnnotationSerializer(Datafix::class)
+object CustomFixSerializer : AnnotationSerializer(CustomFix::class)
+
+abstract class AnnotationSerializer(private val clazz: KClass): KSerializer {
+ @OptIn(InternalSerializationApi::class)
+ override val descriptor by lazy {
+ buildClassSerialDescriptor(clazz.java.simpleName) {
+ for (method in clazz.java.declaredMethods) {
+ element(method.name, method.serializer().descriptor, isOptional = true)
+ }
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): T {
+ throw UnsupportedOperationException()
+ }
+
+ @OptIn(InternalSerializationApi::class)
+ override fun serialize(encoder: Encoder, value: T) {
+ @Suppress("UNCHECKED_CAST")
+ fun CompositeEncoder.encode(index: Int, serializer: KSerializer, value: Any) {
+ encodeSerializableElement(descriptor, index, serializer, value as T)
+ }
+ encoder.encodeStructure(descriptor) {
+ for ((index, method) in clazz.java.declaredMethods.withIndex()) {
+ val v = surrogateValue(method.kType!!) {
+ try {
+ method.invoke(value)
+ } catch (e: InvocationTargetException) {
+ throw e.cause ?: e
+ }
+ }
+ if (v == ""
+ || (v is List<*> && v.isEmpty())
+ || (v.javaClass.isArray && java.lang.reflect.Array.getLength(v) == 0)
+ || (v is DeclaredType && v.hasQualifiedName(JAVA_LANG_OBJECT))) {
+ continue
+ }
+ encode(index, method.serializer(), v)
+ }
+ }
+ }
+
+ @OptIn(InternalSerializationApi::class)
+ private fun Method.serializer(): KSerializer<*> {
+ return JSON.serializersModule.serializer(surrogateType(kType!!))
+ }
+
+ private val Method.kType: KType?
+ get() = declaringClass.kotlin.memberProperties.firstOrNull { it.name == name }?.returnType
+
+ private fun surrogateType(type: KType): KType {
+ return when {
+ type.classifier == Array::class -> List::class.createType(type.arguments)
+ type.jvmErasure == KClass::class -> TypeMirror::class.starProjectedType
+ else -> type
+ }
+ }
+
+ private fun surrogateValue(type: KType, value: () -> Any): Any {
+ return when {
+ type.classifier == Array::class -> {
+ val v = value()
+ (0 until java.lang.reflect.Array.getLength(v)).map {
+ index -> surrogateValue(type.arguments[0].type!!) { java.lang.reflect.Array.get(v, index) }
+ }
+ }
+ type.jvmErasure == KClass::class -> {
+ toTypeMirror { value() as KClass<*> }
+ }
+ else -> value()
+ }
+ }
+}
+
+object PrimitiveTypeMirrorSerializer : SerializationStrategy {
+ override val descriptor = buildClassSerialDescriptor("primitive") {
+ element("kind")
+ }
+
+ override fun serialize(encoder: Encoder, value: TypeMirror) {
+ encoder.encodeStructure(descriptor) {
+ encodeSerializableElement(descriptor, 0, serializer(), value.kind)
+ }
+ }
+}
+
+object ArrayTypeMirrorSerializer : SerializationStrategy {
+ override val descriptor = buildClassSerialDescriptor("array") {
+ element("elementType")
+ }
+
+ override fun serialize(encoder: Encoder, value: TypeMirror) {
+ encoder.encodeStructure(descriptor) {
+ encodeSerializableElement(descriptor, 0, serializer(), (value as ArrayType).componentType)
+ }
+ }
+}
+
+object DeclaredTypeMirrorSerializer : SerializationStrategy {
+ override val descriptor = buildClassSerialDescriptor("declared") {
+ element("name")
+ element>("typeArguments")
+ }
+
+ override fun serialize(encoder: Encoder, value: TypeMirror) {
+ encoder.encodeStructure(descriptor) {
+ encodeStringElement(descriptor, 0, value.asTypeElement()!!.qualifiedName.toString())
+ encodeSerializableElement(descriptor, 1, serializer>(), (value as DeclaredType).typeArguments)
+ }
+ }
+}
+
+object ClassSerializer : KSerializer> {
+ override val descriptor = String.serializer().descriptor
+
+ override fun deserialize(decoder: Decoder): Class<*> {
+ throw UnsupportedOperationException()
+ }
+
+ override fun serialize(encoder: Encoder, value: Class<*>) {
+ encoder.encodeString(value.canonicalName)
+ }
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+val JSON = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ explicitNulls = false
+
+ serializersModule = SerializersModule {
+ polymorphicDefaultSerializer(TypeMirror::class) { instance ->
+ when {
+ instance.kind.isPrimitive || instance.kind == TypeKind.VOID -> PrimitiveTypeMirrorSerializer
+ instance.kind == TypeKind.ARRAY -> ArrayTypeMirrorSerializer
+ instance.kind == TypeKind.DECLARED -> DeclaredTypeMirrorSerializer
+ else -> null
+ }
+ }
+
+ contextual(Class::class, ClassSerializer)
+
+ contextual(FilledArgument.FromRegistry::class, FromRegistrySerializer)
+ contextual(Length::class, LengthSerializer)
+ contextual(DefaultConstruct::class, DefaultConstructSerializer)
+ contextual(Introduce::class, IntroduceSerializer)
+ contextual(Polymorphic::class, PolymorphicSerializer)
+ contextual(Datafix::class, DatafixSerializer)
+ contextual(CustomFix::class, CustomFixSerializer)
+ }
+}
diff --git a/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/Utils.kt b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/Utils.kt
new file mode 100644
index 00000000..083bd5c8
--- /dev/null
+++ b/annotationProcessor/src/main/kotlin/net/earthcomputer/multiconnect/ap/Utils.kt
@@ -0,0 +1,407 @@
+package net.earthcomputer.multiconnect.ap
+
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.ArrayType
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.MirroredTypeException
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.reflect.KClass
+
+const val JAVA_LANG_OBJECT = "java.lang.Object"
+const val JAVA_LANG_STRING = "java.lang.String"
+const val JAVA_LANG_RUNTIME_EXCEPTION = "java.lang.RuntimeException"
+const val JAVA_LANG_ERROR = "java.lang.Error"
+const val JAVA_UTIL_BITSET = "java.util.BitSet"
+const val JAVA_UTIL_LIST = "java.util.List"
+const val JAVA_UTIL_OPTIONAL = "java.util.Optional"
+const val JAVA_UTIL_OPTIONAL_INT = "java.util.OptionalInt"
+const val JAVA_UTIL_OPTIONAL_LONG = "java.util.OptionalLong"
+const val JAVA_UTIL_UUID = "java.util.UUID"
+const val JAVA_UTIL_FUNCTION_CONSUMER = "java.util.function.Consumer"
+const val JAVA_UTIL_FUNCTION_FUNCTION = "java.util.function.Function"
+const val JAVA_UTIL_FUNCTION_SUPPLIER = "java.util.function.Supplier"
+const val FASTUTIL_INT_LIST = "it.unimi.dsi.fastutil.ints.IntList"
+const val FASTUTIL_LONG_LIST = "it.unimi.dsi.fastutil.longs.LongList"
+const val MINECRAFT_NBT_COMPOUND = "net.minecraft.nbt.NbtCompound"
+const val MINECRAFT_IDENTIFIER = "net.minecraft.util.Identifier"
+const val MINECRAFT_NETWORK_HANDLER = "net.minecraft.client.network.ClientPlayNetworkHandler"
+const val MULTICONNECT_TYPED_MAP = "net.earthcomputer.multiconnect.protocols.generic.TypedMap"
+const val MULTICONNECT_DELAYED_PACKET_SENDER = "net.earthcomputer.multiconnect.impl.DelayedPacketSender"
+
+val messageName: String = Message::class.java.canonicalName
+val messageVariantName: String = MessageVariant::class.java.canonicalName
+val polymorphicName: String = Polymorphic::class.java.canonicalName
+val networkEnumName: String = NetworkEnum::class.java.canonicalName
+
+fun count(vararg values: Boolean): Int {
+ return values.count { it }
+}
+
+fun Element.getAnnotation(clazz: KClass): T? {
+ return getAnnotation(clazz.java)
+}
+
+fun Element.hasAnnotation(clazz: KClass): Boolean {
+ return getAnnotation(clazz) != null
+}
+
+fun Element.hasModifier(modifier: Modifier): Boolean {
+ return modifiers.contains(modifier)
+}
+
+val Element.isEnum: Boolean
+ get() = hasAnnotation(NetworkEnum::class)
+
+val Element.isMessage: Boolean
+ get() = hasAnnotation(Message::class)
+
+val Element.isMessageVariant: Boolean
+ get() = hasAnnotation(MessageVariant::class)
+
+val Element.isPolymorphicRoot: Boolean
+ get() {
+ if (this !is TypeElement) return false
+ if (!this.hasAnnotation(Polymorphic::class)) return false
+ if (!this.hasModifier(Modifier.ABSTRACT)) return false
+ return superclass.hasQualifiedName(JAVA_LANG_OBJECT)
+ }
+
+val Element.polymorphicParent: TypeElement?
+ get() = (this as? TypeElement)?.superclass?.asTypeElement()?.takeIf { !it.qualifiedName.contentEquals(JAVA_LANG_OBJECT) }
+
+val TypeElement.recordFields: List
+ get() = enclosedElements.mapNotNull { elt ->
+ if (!elt.kind.isField) return@mapNotNull null
+ if (elt.hasModifier(Modifier.STATIC)) return@mapNotNull null
+ elt as VariableElement
+ }
+
+val TypeElement.allRecordFields: List
+ get() {
+ val polymorphicParent = polymorphicParent
+ return if (polymorphicParent != null) {
+ polymorphicParent.recordFields + recordFields
+ } else {
+ recordFields
+ }
+ }
+
+val TypeElement.enumConstants: List
+ get() = enclosedElements.mapNotNull { elt ->
+ if (elt.kind != ElementKind.ENUM_CONSTANT) return@mapNotNull null
+ elt as VariableElement
+ }
+
+val TypeElement.handler: ExecutableElement?
+ get() {
+ if (!isMessageVariant) return null
+ return (enclosedElements.singleOrNull { it.hasAnnotation(Handler::class) } as? ExecutableElement)?.takeIf { it.kind == ElementKind.METHOD }
+}
+
+fun TypeElement.getPartialHandlers(
+ processingEnv: ProcessingEnvironment,
+ errorConsumer: ErrorConsumer? = null,
+ errorElement: Element? = null
+): List {
+ if (!isMessageVariant) return emptyList()
+ return enclosedElements
+ .filter { it.hasAnnotation(PartialHandler::class) && it.kind == ElementKind.METHOD }
+ .mapNotNull { it as? ExecutableElement }
+ .mapNotNull { findMulticonnectFunction(processingEnv, it.simpleName.toString(), errorConsumer = errorConsumer, errorElement = errorElement) }
+}
+
+fun TypeElement.isConstructable(processingEnv: ProcessingEnvironment): Boolean {
+ if (kind != ElementKind.CLASS) return false
+ if (hasModifier(Modifier.ABSTRACT)) return false
+ if (enclosingElement.kind.isClass || enclosingElement.kind.isInterface) {
+ if (!hasModifier(Modifier.STATIC)) return false
+ }
+ val constructors = enclosedElements.mapNotNull { ctor -> (ctor as? ExecutableElement)?.takeIf { it.kind == ElementKind.CONSTRUCTOR } }
+ if (constructors.isEmpty()) return true
+ val noArg = constructors.firstOrNull { it.parameters.isEmpty() } ?: return false
+ if (!noArg.hasModifier(Modifier.PUBLIC)) return false
+ return noArg.isThrowSafe(processingEnv)
+}
+
+fun TypeElement.findMulticonnectFunction(
+ processingEnv: ProcessingEnvironment,
+ name: String,
+ argumentResolveContext: TypeElement? = this,
+ errorConsumer: ErrorConsumer? = null,
+ errorElement: Element? = null
+): MulticonnectFunction? {
+ val matchingMethods = enclosedElements.filter {
+ it.kind == ElementKind.METHOD && it.simpleName.contentEquals(name)
+ }
+ if (matchingMethods.isEmpty()) {
+ errorConsumer?.report("No methods named \"name\" were found in type $simpleName", errorElement!!)
+ return null
+ }
+ if (matchingMethods.size > 1) {
+ errorConsumer?.report("Multiconnect methods cannot have overloads", errorElement!!)
+ return null
+ }
+ val method = matchingMethods.single() as ExecutableElement
+ if (!method.hasModifier(Modifier.STATIC)) {
+ errorConsumer?.report("Multiconnect method must be static", method)
+ return null
+ }
+ if (!method.hasModifier(Modifier.PUBLIC)) {
+ errorConsumer?.report("Multiconnect method must be public", method)
+ return null
+ }
+ if (!method.isThrowSafe(processingEnv)) {
+ errorConsumer?.report("Multiconnect methods must be throw safe", method)
+ return null
+ }
+ val positionalParameters = mutableListOf()
+ val parameters = mutableListOf()
+ for (parameter in method.parameters) {
+ val argument = parameter.getAnnotation(Argument::class)
+ val isDefaultConstruct = parameter.hasAnnotation(DefaultConstruct::class)
+ val filledArgument = parameter.getAnnotation(FilledArgument::class)
+ val isGlobalData = parameter.hasAnnotation(GlobalData::class)
+ val paramType = parameter.asType()
+ when (count(argument != null, isDefaultConstruct, filledArgument != null, isGlobalData)) {
+ 0 -> {
+ if (parameters.isEmpty()) {
+ positionalParameters += paramType
+ } else {
+ errorConsumer?.report("Positional parameter detected after non-positional parameter", parameter)
+ return null
+ }
+ }
+ 1 -> {
+ when {
+ argument != null -> {
+ if (argumentResolveContext != null
+ && !argument.value.startsWith("outer.")
+ && argument.value != "this"
+ && !argumentResolveContext.allRecordFields.any { it.simpleName.contentEquals(argument.value) }
+ ) {
+ errorConsumer?.report("Could not resolve argument \"${argument.value}\"", parameter)
+ return null
+ }
+ parameters += MulticonnectParameter.Argument(paramType, argument.value, argument.translate)
+ }
+ isDefaultConstruct -> {
+ if (paramType.hasQualifiedName(JAVA_UTIL_FUNCTION_SUPPLIER)) {
+ val typeArgument = paramType.typeArguments?.singleOrNull()
+ if (typeArgument == null) {
+ errorConsumer?.report("Default construct supplier must have a type argument", parameter)
+ return null
+ }
+ if (!MulticonnectType.isSupportedType(processingEnv, typeArgument)) {
+ errorConsumer?.report("Cannot default-construct non-multiconnect type", parameter)
+ return null
+ }
+ parameters += MulticonnectParameter.SuppliedDefaultConstructed(paramType, typeArgument)
+ } else {
+ if (!MulticonnectType.isSupportedType(processingEnv, paramType)) {
+ errorConsumer?.report("Cannot default-construct non-multiconnect type", parameter)
+ return null
+ }
+ parameters += MulticonnectParameter.DefaultConstructed(paramType)
+ }
+ }
+ filledArgument != null -> {
+ val fromRegistry = filledArgument.fromRegistry.takeIf { it.value.isNotEmpty() }
+ val fromVersion = filledArgument.fromVersion.takeIf { it != -1 }
+ val toVersion = filledArgument.toVersion.takeIf { it != -1 }
+ if (fromRegistry != null) {
+ if (fromVersion != null || toVersion != null) {
+ errorConsumer?.report("Cannot specify fromRegistry and fromVersion or toVersion", parameter)
+ return null
+ }
+ if (!MulticonnectType.isRegistryCompatible(paramType)) {
+ errorConsumer?.report("Cannot fill non-registry type from a registry", parameter)
+ return null
+ }
+ } else if (fromVersion != null || toVersion != null) {
+ if (fromVersion == null || toVersion == null) {
+ errorConsumer?.report("Cannot specify fromVersion without toVersion or vice versa", parameter)
+ return null
+ }
+ var validType = false
+ if (paramType.hasQualifiedName(JAVA_UTIL_FUNCTION_FUNCTION)) {
+ val typeArguments = paramType.typeArguments
+ if (typeArguments.size == 2) {
+ val type1 = typeArguments[0]
+ val type2 = typeArguments[1]
+ if (type1.isMessageVariant && type2.isMessageVariant) {
+ if (processingEnv.typeUtils.isSameType(type1, type2)) {
+ validType = true
+ } else {
+ val group1 = type1.asTypeElement()?.interfaces?.singleOrNull()
+ val group2 = type2.asTypeElement()?.interfaces?.singleOrNull()
+ if (group1 != null && group2 != null && processingEnv.typeUtils.isSameType(group1, group2)) {
+ validType = true
+ }
+ }
+ }
+ }
+ }
+ if (!validType) {
+ errorConsumer?.report("Invalid filled variant conversion type", parameter)
+ return null
+ }
+ } else {
+ if (!MulticonnectType.canAutoFill(paramType)) {
+ errorConsumer?.report("Cannot fill type $paramType", parameter)
+ return null
+ }
+ }
+ parameters += MulticonnectParameter.Filled(paramType, fromRegistry, fromVersion, toVersion)
+ }
+ isGlobalData -> {
+ val isValidType = if (paramType.hasQualifiedName(JAVA_UTIL_FUNCTION_CONSUMER)) {
+ val consumedType = paramType.typeArguments.singleOrNull()
+ consumedType is DeclaredType
+ } else {
+ paramType is DeclaredType
+ }
+ if (!isValidType) {
+ errorConsumer?.report("Invalid global type $paramType", parameter)
+ return null
+ }
+ parameters += MulticonnectParameter.GlobalData(paramType)
+ }
+ }
+ }
+ else -> {
+ errorConsumer?.report("Only one multiconnect parameter annotation is allowed", parameter)
+ return null
+ }
+ }
+ }
+
+ val possibleReturnTypes = method.getAnnotationsByType(ReturnType::class.java).mapTo(mutableSetOf()) {
+ toTypeMirror { it.value }
+ }
+ if (possibleReturnTypes.isNotEmpty()) {
+ if (!method.returnType.hasQualifiedName(JAVA_UTIL_LIST)
+ || !method.returnType.componentType(processingEnv).hasQualifiedName(JAVA_LANG_OBJECT)
+ ) {
+ errorConsumer?.report("@ReturnType is only allowed on methods that return List