Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,16 @@ private fun renderType(type: KtTypeSpec, sb: StringBuilder, indent: Int) {
if (KtModifier.CONST in prop.modifiers) add("const")
}
if (mods.isNotEmpty()) sb.append(mods.joinToString(" ")).append(" ")
sb.append("val ").append(prop.name)
sb.append(if (prop.mutable) "var " else "val ").append(prop.name)
prop.type?.let { sb.append(": ").append(it.simpleName) }
sb.append(" = ").append(prop.initializer).append("\n")
prop.initializer?.let { sb.append(" = ").append(it) }
sb.append("\n")
Comment on lines 90 to +94

listOfNotNull(prop.getter, prop.setter).forEach { accessor ->
accessor.lines().forEach { line ->
sb.append(bodyPad).append("\t").append(line).append("\n")
}
}
}
if (type.properties.isNotEmpty()) sb.append("\n")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ data class KtPropertySpec(
val name: String,
val type: KtRef? = null,
val modifiers: Set<KtModifier> = emptySet(),
val mutable: Boolean = false,
/** Fully-rendered Kotlin initializer expression, e.g. `"\"foo\""` or `"NAMESPACE"`. */
val initializer: String,
/** Extra symbols referenced by [initializer] that must be imported. */
val initializer: String? = null,
/** Fully-rendered getter, e.g. `"get() = NAMESPACE"`. */
val getter: String? = null,
/** Fully-rendered setter, e.g. `"set(value) = error(\"...\")"`. */
val setter: String? = null,
/** Extra symbols referenced by [initializer], [getter], or [setter] that must be imported. */
val referencedTypes: List<KtRef> = emptyList(),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,54 @@ private fun createSerializableAnnotation() = KtAnnotationSpec(
/**
* Helper function to create a namespace property.
*/
private fun createNamespaceProperty(namespace: String, isConstant: Boolean = false): KtPropertySpec {
private fun createNamespaceProperty(
namespace: String,
isConstant: Boolean = false,
useGetter: Boolean = false,
): KtPropertySpec {
val modifiers = buildSet {
if (isConstant) add(KtModifier.CONST)
add(KtModifier.OVERRIDE)
}
val initializer = if (isConstant) kotlinStringLiteral(namespace) else "NAMESPACE"
return KtPropertySpec(name = "namespace", type = stringRef, modifiers = modifiers, initializer = initializer)
return KtPropertySpec(
name = "namespace",
type = stringRef,
modifiers = modifiers,
initializer = initializer.takeUnless { useGetter },
getter = "get() = $initializer".takeIf { useGetter },
)
}

private fun createFunctionNameProperty() = KtPropertySpec(
name = "name",
type = stringRef,
modifiers = setOf(KtModifier.OVERRIDE),
getter = "get() = asId().substringAfter(\":\").substringAfterLast(\"/\")",
)

private fun createFunctionDirectoryProperty() = KtPropertySpec(
name = "directory",
type = stringRef,
modifiers = setOf(KtModifier.OVERRIDE),
mutable = true,
getter = "get() = asId().substringAfter(\":\").substringBeforeLast(\"/\", \"\")",
setter = $$"set(value) = error(\"Generated function bindings are immutable; cannot set directory to '$value'\")",
)

private fun createMappedAsIdFunction(entries: List<Pair<String, String>>) = KtFunSpec(
name = "asId",
modifiers = setOf(KtModifier.OVERRIDE),
returnType = stringRef,
statements = buildList {
add("return when (this) {")
entries.forEach { (enumName, path) ->
add($$"\t$$enumName -> \"$NAMESPACE:$$path\"")
}
add("}")
},
)

/**
* Helper function to create an asId() function for simple resources.
*/
Expand All @@ -67,17 +106,25 @@ private fun createAsIdFunction(pathPrefix: String = "", tagPrefix: String = "")
/**
* Generates a simple enum for functions when there are no nested directories.
*/
fun generateSimpleFunctionsEnum(functions: List<Function>, namespace: String) = KtTypeSpec(
kind = KtTypeKind.ENUM,
name = "Functions",
annotations = listOf(createSerializableAnnotation()),
superinterfaces = listOf(functionArgumentRef),
properties = listOf(createNamespaceProperty(namespace)),
functions = listOf(createAsIdFunction()),
enumConstants = functions.map { function ->
function.id.substringAfter(":").replace(Regex("[^a-zA-Z0-9_]"), "_").snakeCase().uppercase()
},
)
fun generateSimpleFunctionsEnum(functions: List<Function>, namespace: String): KtTypeSpec {
val nameAllocator = KotlinNameAllocator()
val entriesById = functions.sortedBy(Function::id).associate { function ->
function.id to nameAllocator.allocate(function.id.substringAfter(":").kotlinEnumName(), "_FUNCTION")
}
val entries = functions.map { function ->
entriesById.getValue(function.id) to function.id.substringAfter(":")
}
Comment on lines +114 to +116

return KtTypeSpec(
kind = KtTypeKind.ENUM,
name = "Functions",
annotations = listOf(createSerializableAnnotation()),
superinterfaces = listOf(functionArgumentRef),
properties = listOf(createNamespaceProperty(namespace), createFunctionDirectoryProperty()),
functions = listOf(createMappedAsIdFunction(entries)),
enumConstants = entries.map { it.first },
)
}

/**
* Generates a simple enum for resources when there are no nested directories.
Expand Down Expand Up @@ -105,101 +152,112 @@ fun generateFunctionsEnumTree(
namespace: String,
): KtTypeSpec {
val selfRef = KtRef(packageName, "$datapackObjectName.Functions")

val sealedInterface = MutableTypeNode(
kind = KtTypeKind.INTERFACE,
name = "Functions",
modifiers = setOf(KtModifier.SEALED),
superinterfaces = listOf(functionArgumentRef),
)
sealedInterface.properties += KtPropertySpec(
name = "namespace",
type = stringRef,
modifiers = setOf(KtModifier.OVERRIDE),
initializer = "NAMESPACE",
)

// Group functions by depth and parent
val functionPaths = functions.map { it.id.substringAfter(":") }
val maxDepth = functionPaths.maxOfOrNull { it.count { c -> c.toString() == separator } } ?: 0
val typeBuilders = MutableList(maxDepth + 1) { mutableMapOf<String, MutableTypeNode>() }
sealedInterface.properties += createNamespaceProperty(namespace, useGetter = true)
sealedInterface.properties += createFunctionNameProperty()
sealedInterface.properties += createFunctionDirectoryProperty()

// Allocate directory names before function names so a directory keeps the concise name when
// `foo.mcfunction` and `foo/...` coexist. The function then becomes `FooFunction`.
val directories = buildSet {
functionPaths.forEach { path ->
val segments = path.split(separator)
for (index in 1 until segments.size) add(segments.take(index).joinToString(separator))
}
}
val scopeAllocators = mutableMapOf<String, KotlinNameAllocator>()
fun scopeAllocator(path: String) = scopeAllocators.getOrPut(path) { KotlinNameAllocator() }
fun parentPath(path: String) = path.substringBeforeLast(separator, "")

val directoryNames = mutableMapOf<String, String>()
directories.sortedWith(compareBy<String>({ it.count { char -> char.toString() == separator } }, { it })).forEach { path ->
val preferredName = path.substringAfterLast(separator).kotlinTypeName()
directoryNames[path] = scopeAllocator(parentPath(path)).allocate(preferredName, "Group")
}

// Build enums from deepest to shallowest
for (function in functions) {
val functionNames = mutableMapOf<String, String>()
functions.sortedBy(Function::id).forEach { function ->
val path = function.id.substringAfter(":")
val depth = path.count { it.toString() == separator }
val enumValue = path.substringAfterLast(separator).replace(Regex("[^a-zA-Z0-9_]"), "_").snakeCase().uppercase()
val parent = parentPath(path)
val isTopLevel = parent.isEmpty()
val preferredName = if (isTopLevel) {
path.kotlinTypeName()
} else {
path.substringAfterLast(separator).kotlinEnumName()
}
val collisionSuffix = if (isTopLevel) "Function" else "_FUNCTION"
functionNames[function.id] = scopeAllocator(parent).allocate(preferredName, collisionSuffix)
}

if (depth == 0) {
// Top-level function - create data object
val objectName = path.pascalCase()
sealedInterface.nestedTypes += KtTypeSpec(
data class FunctionEnumGroup(
val node: MutableTypeNode,
val entries: MutableList<Pair<String, String>> = mutableListOf(),
)

val rootFunctions = mutableListOf<KtTypeSpec>()
val functionGroups = mutableMapOf<String, FunctionEnumGroup>()
functions.forEach { function ->
val path = function.id.substringAfter(":")
val parent = parentPath(path)
val kotlinName = functionNames.getValue(function.id)

if (parent.isEmpty()) {
rootFunctions += KtTypeSpec(
kind = KtTypeKind.OBJECT,
name = objectName,
name = kotlinName,
modifiers = setOf(KtModifier.DATA),
superinterfaces = listOf(selfRef),
functions = listOf(
KtFunSpec(
name = "asId",
modifiers = setOf(KtModifier.OVERRIDE),
returnType = stringRef,
statements = listOf("return \"\$NAMESPACE:${path.lowercase()}\""),
statements = listOf("return \"\$NAMESPACE:$path\""),
)
),
)
} else {
// Get or create enum for this parent
val parent = path.substringBeforeLast(separator)
val enumName = parent.substringAfterLast(separator).pascalCase()

val enumBuilder = typeBuilders[depth - 1].getOrPut(parent) {
val node = MutableTypeNode(
kind = KtTypeKind.ENUM,
name = enumName,
annotations = listOf(createSerializableAnnotation()),
superinterfaces = listOf(selfRef),
)
node.functions += KtFunSpec(
name = "asId",
modifiers = setOf(KtModifier.OVERRIDE),
returnType = stringRef,
statements = listOf("return \"\$NAMESPACE:$parent/\${name.lowercase()}\""),
val group = functionGroups.getOrPut(parent) {
FunctionEnumGroup(
MutableTypeNode(
kind = KtTypeKind.ENUM,
name = directoryNames.getValue(parent),
annotations = listOf(createSerializableAnnotation()),
superinterfaces = listOf(selfRef),
)
)
node
}

enumBuilder.enumConstants += enumValue
group.node.enumConstants += kotlinName
group.entries += kotlinName to path
}
}

// Nest enums from deepest to shallowest
for (depth in typeBuilders.lastIndex downTo 1) {
for ((path, typeBuilder) in typeBuilders[depth]) {
val parent = path.substringBeforeLast(separator)
val objectName = parent.substringAfterLast(separator).pascalCase()

val parentBuilder = typeBuilders[depth - 1].getOrPut(parent) {
val node = MutableTypeNode(
kind = KtTypeKind.OBJECT,
name = objectName,
modifiers = setOf(KtModifier.DATA),
superinterfaces = listOf(selfRef),
)
node.functions += KtFunSpec(
name = "asId",
modifiers = setOf(KtModifier.OVERRIDE),
returnType = stringRef,
statements = listOf("return \"\$NAMESPACE:${parent.lowercase()}\""),
)
node
}

parentBuilder.nestedTypes += typeBuilder.build()
}
functionGroups.values.forEach { group -> group.node.functions += createMappedAsIdFunction(group.entries) }

// A directory with direct functions is an enum; a directory used only for nesting is a plain
// container object and must not pretend to be a callable function itself.
val directoryNodes = directories.associateWithTo(mutableMapOf()) { path ->
functionGroups[path]?.node ?: MutableTypeNode(
kind = KtTypeKind.OBJECT,
name = directoryNames.getValue(path),
modifiers = setOf(KtModifier.DATA),
)
}
directories.sortedWith(compareByDescending<String> { it.count { char -> char.toString() == separator } }.thenBy { it })
.forEach { path ->
val parent = parentPath(path)
if (parent.isNotEmpty()) directoryNodes.getValue(parent).nestedTypes += directoryNodes.getValue(path).build()
}

// Add top-level enums/objects to sealed interface
typeBuilders.firstOrNull()?.forEach { (_, builder) -> sealedInterface.nestedTypes += builder.build() }
directories.filter { parentPath(it).isEmpty() }.sorted().forEach { path ->
sealedInterface.nestedTypes += directoryNodes.getValue(path).build()
}
sealedInterface.nestedTypes += rootFunctions

return sealedInterface.build()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.github.ayfri.kore.bindings.generation


/** Converts a Minecraft path segment into a valid, conventional Kotlin type name. */
internal fun String.kotlinTypeName(): String {
val words = replace(Regex("[^a-zA-Z0-9]+"), "_")
.split("_")
.filter(String::isNotEmpty)
val baseName = words.joinToString("") { word ->
word.lowercase().replaceFirstChar { it.titlecase() }
}.ifEmpty { "Unnamed" }

return if (baseName.first().isDigit()) "N$baseName" else baseName
}

/** Converts a Minecraft path segment into a valid Kotlin enum-entry name. */
internal fun String.kotlinEnumName(): String {
val baseName = replace(Regex("[^a-zA-Z0-9_]"), "_")
.snakeCase()
.uppercase()
.ifEmpty { "UNNAMED" }

return if (baseName.first().isDigit()) "_$baseName" else baseName
}

/** Allocates unique Kotlin identifiers within one declaration scope. */
internal class KotlinNameAllocator {
private val allocatedNames = mutableSetOf<String>()

fun allocate(preferredName: String, collisionSuffix: String): String {
if (allocatedNames.add(preferredName)) return preferredName

val suffixedName = preferredName + collisionSuffix
if (allocatedNames.add(suffixedName)) return suffixedName

var index = 2
while (!allocatedNames.add(suffixedName + index)) index++
return suffixedName + index
}
}
Loading