Skip to content

Commit 937e624

Browse files
authored
Merge pull request #211 from e-psi-lon/master
2 parents 89bb489 + c9c5f78 commit 937e624

File tree

9 files changed

+252
-16
lines changed

9 files changed

+252
-16
lines changed

bindings/src/main/kotlin/io/github/ayfri/kore/bindings/api/Configuration.kt

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,56 @@ class DatapackConfiguration {
4949
/**
5050
* Override the generated Kotlin object name.
5151
*/
52-
var remappedName: String? = null
52+
@Deprecated("Use remappings { objectName(...) } instead", level = DeprecationLevel.WARNING)
53+
var remappedName: String?
54+
get() = remappings.objectName
55+
set(value) = if (value != null) remappings.objectName(value) else Unit
56+
/**
57+
* Configuration for remapping namespaces within this datapack.
58+
*/
59+
internal val remappings = RemappingConfiguration()
5360
/**
5461
* Select a subfolder within the downloaded datapack.
5562
*/
5663
var subPath: String? = null
64+
/**
65+
* Configures namespace remappings for this datapack.
66+
*/
67+
fun remappings(block: RemappingConfiguration.() -> Unit) = remappings.apply(block)
68+
}
69+
70+
/**
71+
* Configuration for remapping namespaces within a datapack.
72+
*/
73+
class RemappingConfiguration {
74+
/**
75+
* Map of namespace names to their remapped names.
76+
*/
77+
internal val namespaces = mutableMapOf<String, String>()
78+
79+
/**
80+
* Override the generated Kotlin object name.
81+
*/
82+
var objectName: String? = null
83+
84+
/**
85+
* Checks if any remappings have been configured.
86+
*/
87+
fun hasRemappings() = namespaces.isNotEmpty() || objectName != null
88+
89+
/**
90+
* Remaps a namespace to a new name.
91+
*/
92+
fun namespace(namespace: String, remappedName: String) {
93+
namespaces[namespace] = remappedName
94+
}
95+
96+
/**
97+
* Renames the generated Kotlin object.
98+
*/
99+
fun objectName(name: String) {
100+
objectName = name
101+
}
102+
103+
internal fun toState() = RemappingState(namespaces.toMap(), objectName)
57104
}

bindings/src/main/kotlin/io/github/ayfri/kore/bindings/api/DatapackImport.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,6 @@ class DatapackImportDsl {
120120
if (datapackConfig.packageName != null) {
121121
importer.packageNameOverride = datapackConfig.packageName!!
122122
}
123-
if (datapackConfig.remappedName != null) {
124-
importer.remappedNameOverride = datapackConfig.remappedName!!
125-
}
126123
if (datapackConfig.subPath != null) {
127124
importer.subPath = datapackConfig.subPath!!
128125
}
@@ -132,6 +129,9 @@ class DatapackImportDsl {
132129
if (datapackConfig.excludes.isNotEmpty()) {
133130
importer.excludes = datapackConfig.excludes
134131
}
132+
if (datapackConfig.remappings.hasRemappings()) {
133+
importer.remappings = datapackConfig.remappings.toState()
134+
}
135135

136136
// If no custom package name, use global prefix with datapack name
137137
if (datapackConfig.packageName == null) {

bindings/src/main/kotlin/io/github/ayfri/kore/bindings/api/DatapackImporter.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal class DatapackImporter(val source: String) {
1818
var includes: List<String> = emptyList()
1919
var outputDirectory: Path? = null
2020
var packageNameOverride: String? = null
21-
var remappedNameOverride: String? = null
21+
var remappings: RemappingState = RemappingState(emptyMap(), null)
2222
var skipCache: Boolean = false
2323
var subPath: String? = null
2424

@@ -69,6 +69,11 @@ internal class DatapackImporter(val source: String) {
6969
*/
7070
fun write(datapack: Datapack) {
7171
val outputPath = outputDirectory ?: Path("src/main/kotlin")
72-
generateDatapackFile(datapack, outputPath, packageNameOverride, remappedNameOverride)
72+
generateDatapackFile(datapack, outputPath, packageNameOverride, remappings)
7373
}
7474
}
75+
76+
class RemappingState(
77+
val namespaces: Map<String, String> = emptyMap(),
78+
val objectName: String? = null
79+
)

bindings/src/main/kotlin/io/github/ayfri/kore/bindings/writer.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.squareup.kotlinpoet.FileSpec
44
import com.squareup.kotlinpoet.KModifier
55
import com.squareup.kotlinpoet.PropertySpec
66
import com.squareup.kotlinpoet.TypeSpec
7+
import io.github.ayfri.kore.bindings.api.RemappingState
78
import io.github.ayfri.kore.bindings.generation.*
89
import java.net.URLDecoder
910
import java.nio.file.Files
@@ -14,23 +15,27 @@ import kotlin.io.path.absolute
1415
* Main entry point for generating Kotlin bindings from a datapack.
1516
* Normalizes the datapack name and delegates to generateDatapackFile.
1617
*/
17-
fun writeFiles(datapack: Datapack, outputPath: Path) {
18+
fun writeFiles(datapack: Datapack, outputPath: Path, remappings: RemappingState = RemappingState()) {
1819
val baseName = datapack.name.removeSuffix(".zip")
1920
// Decode URL-encoded characters and remove invalid characters
2021
val decodedName = URLDecoder.decode(baseName, "UTF-8")
2122
val normalizedName = decodedName
2223
.replace(".", "-")
2324
.replace(Regex("[^a-zA-Z0-9_-]"), "")
2425
val packageName = "kore.dependencies.${normalizedName.lowercase().replace(Regex("[^a-z0-9]"), "")}"
26+
val actualRemappings = RemappingState(
27+
namespaces = remappings.namespaces,
28+
objectName = remappings.objectName ?: normalizedName
29+
)
2530

26-
generateDatapackFile(datapack, outputPath, packageName, normalizedName)
31+
generateDatapackFile(datapack, outputPath, packageName, actualRemappings)
2732
}
2833

2934
/**
3035
* Generates the main datapack Kotlin file with all resources.
3136
* Creates a single data object containing all functions, resources, and pack metadata.
3237
*/
33-
fun generateDatapackFile(datapack: Datapack, outputDir: Path, packageNameOverride: String? = null, normalizedNameOverride: String? = null) {
38+
fun generateDatapackFile(datapack: Datapack, outputDir: Path, packageNameOverride: String? = null, remappings: RemappingState = RemappingState()) {
3439
// Decode and normalize the datapack name
3540
val baseName = datapack.name.removeSuffix(".zip")
3641
val decodedName = try {
@@ -39,7 +44,7 @@ fun generateDatapackFile(datapack: Datapack, outputDir: Path, packageNameOverrid
3944
baseName // If decoding fails, use the original name
4045
}
4146

42-
val normalizedName = normalizedNameOverride ?: decodedName
47+
val normalizedName = remappings.objectName ?: decodedName
4348
.replace(".", "-")
4449
.replace(Regex("[^a-zA-Z0-9_-]"), "")
4550

@@ -143,7 +148,9 @@ fun generateDatapackFile(datapack: Datapack, outputDir: Path, packageNameOverrid
143148
} else {
144149
// Multiple namespaces - create intermediate objects for each namespace
145150
allNamespaces.forEach { namespace ->
146-
val namespaceObjectName = namespace.pascalCase()
151+
val namespaceObjectName = remappings.namespaces[namespace] ?: namespace
152+
.replace(Regex("[^a-zA-Z0-9_]"), "_")
153+
.pascalCase()
147154
val namespaceObject = TypeSpec.objectBuilder(namespaceObjectName)
148155
.addModifiers(KModifier.DATA)
149156
.addProperty(

bindings/src/test/kotlin/io/github/ayfri/kore/bindings/Main.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ fun main() {
1515
dungeonsAndTavernsTests()
1616
tagsTests()
1717
downloadTests()
18+
writerTests()
1819
println("All tests passed!")
1920
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package io.github.ayfri.kore.bindings
2+
3+
import io.github.ayfri.kore.bindings.api.RemappingState
4+
import io.github.ayfri.kore.commands.say
5+
import io.github.ayfri.kore.features.advancements.advancement
6+
import io.github.ayfri.kore.features.advancements.criteria
7+
import io.github.ayfri.kore.features.advancements.triggers.tick
8+
import io.github.ayfri.kore.functions.function
9+
import kotlin.io.path.readText
10+
11+
fun writerTests() {
12+
testNamespaceWithDotsInMultiNamespace()
13+
testNamespaceWithHyphensInMultiNamespace()
14+
testNamespaceWithMixedSpecialCharsInMultiNamespace()
15+
testNamespaceRemappingRenamesObject()
16+
testObjectNameAndNamespaceRemappingCombined()
17+
}
18+
19+
fun testNamespaceWithDotsInMultiNamespace() = newTest("ns_normalize_dots") {
20+
val pack = createDataPack("ns_normalize_dots") {
21+
function("fn1", namespace = "my.ns.one") { say("1") }
22+
function("fn2", namespace = "my.ns.two") { say("2") }
23+
}
24+
25+
pack.generate()
26+
27+
val explored = explore(pack.path.toString())
28+
val srcDir = srcDir()
29+
writeFiles(explored, srcDir)
30+
31+
val content = srcDir.resolve("NsNormalizeDots.kt").readText()
32+
33+
// Dots are valid in Minecraft namespaces but not in Kotlin identifiers, writer must normalize them
34+
content.contains("data object my.ns.one") assertsIs false
35+
content.contains("data object my.ns.two") assertsIs false
36+
content.contains("data object MyNsOne") assertsIs true
37+
content.contains("data object MyNsTwo") assertsIs true
38+
}
39+
40+
fun testNamespaceWithHyphensInMultiNamespace() = newTest("ns_normalize_hyphens") {
41+
val pack = createDataPack("ns_normalize_hyphens") {
42+
function("fn1", namespace = "some-ns") { say("1") }
43+
function("fn2", namespace = "another-ns") { say("2") }
44+
}
45+
46+
pack.generate()
47+
48+
val explored = explore(pack.path.toString())
49+
val srcDir = srcDir()
50+
writeFiles(explored, srcDir)
51+
52+
val content = srcDir.resolve("NsNormalizeHyphens.kt").readText()
53+
54+
// Hyphens are valid in Minecraft namespaces but not in Kotlin identifiers, writer must normalize them
55+
content.contains("data object some-ns") assertsIs false
56+
content.contains("data object another-ns") assertsIs false
57+
content.contains("data object SomeNs") assertsIs true
58+
content.contains("data object AnotherNs") assertsIs true
59+
}
60+
61+
fun testNamespaceWithMixedSpecialCharsInMultiNamespace() = newTest("ns_normalize_mixed") {
62+
val pack = createDataPack("ns_normalize_mixed") {
63+
function("fn1", namespace = "foo.bar-baz_qux") { say("a") }
64+
function("fn2", namespace = "hello.world") { say("b") }
65+
}
66+
67+
pack.generate()
68+
69+
val explored = explore(pack.path.toString())
70+
val srcDir = srcDir()
71+
writeFiles(explored, srcDir)
72+
73+
val content = srcDir.resolve("NsNormalizeMixed.kt").readText()
74+
75+
content.contains("data object foo.bar-baz_qux") assertsIs false
76+
content.contains("data object hello.world") assertsIs false
77+
content.contains("data object FooBarBazQux") assertsIs true
78+
content.contains("data object HelloWorld") assertsIs true
79+
}
80+
81+
fun testNamespaceRemappingRenamesObject() = newTest("remapping_namespace") {
82+
val pack = createDataPack("remap_ns_test") {
83+
function("fn1", namespace = "internal_ns") { say("1") }
84+
function("fn2", namespace = "other_ns") { say("2") }
85+
}
86+
87+
pack.generate()
88+
89+
val explored = explore(pack.path.toString())
90+
val srcDir = srcDir()
91+
92+
writeFiles(
93+
explored,
94+
srcDir,
95+
RemappingState(
96+
namespaces = mapOf(
97+
"internal_ns" to "PublicApi",
98+
"other_ns" to "PrivateImpl",
99+
)
100+
)
101+
)
102+
103+
val content = srcDir.resolve("RemapNsTest.kt").readText()
104+
105+
content.contains("data object PublicApi") assertsIs true
106+
content.contains("data object PrivateImpl") assertsIs true
107+
108+
// Default PascalCase-of-namespace names must not appear
109+
content.contains("data object InternalNs") assertsIs false
110+
content.contains("data object OtherNs") assertsIs false
111+
112+
// NAMESPACE constants must still hold the original namespace strings
113+
val publicApiSection = content.substringAfter("data object PublicApi")
114+
publicApiSection.contains("const val NAMESPACE: String = \"internal_ns\"") assertsIs true
115+
116+
val privateImplSection = content.substringAfter("data object PrivateImpl")
117+
privateImplSection.contains("const val NAMESPACE: String = \"other_ns\"") assertsIs true
118+
}
119+
120+
fun testObjectNameAndNamespaceRemappingCombined() = newTest("remapping_combined") {
121+
val pack = createDataPack("combined_remap") {
122+
function("fn1", namespace = "ns_a") { say("a") }
123+
function("fn2", namespace = "ns_b") { say("b") }
124+
125+
advancement("adv1") {
126+
namespace = "ns_a"
127+
criteria { tick("test") }
128+
}
129+
}
130+
131+
pack.generate()
132+
133+
val explored = explore(pack.path.toString())
134+
val srcDir = srcDir()
135+
136+
writeFiles(
137+
explored,
138+
srcDir,
139+
RemappingState(
140+
namespaces = mapOf("ns_a" to "CoreFeatures", "ns_b" to "CompatLayer"),
141+
objectName = "MyDatapack"
142+
)
143+
)
144+
145+
val generatedFile = srcDir.resolve("MyDatapack.kt")
146+
generatedFile.toFile().exists() assertsIs true
147+
148+
val content = generatedFile.readText()
149+
150+
content.contains("data object MyDatapack") assertsIs true
151+
content.contains("data object CoreFeatures") assertsIs true
152+
content.contains("data object CompatLayer") assertsIs true
153+
content.contains("data object NsA") assertsIs false
154+
content.contains("data object NsB") assertsIs false
155+
content.contains("data object CombinedRemap") assertsIs false
156+
157+
// NAMESPACE constants must preserve the original namespace strings
158+
val coreFeaturesSection = content.substringAfter("data object CoreFeatures")
159+
coreFeaturesSection.contains("const val NAMESPACE: String = \"ns_a\"") assertsIs true
160+
161+
val compatLayerSection = content.substringAfter("data object CompatLayer")
162+
compatLayerSection.contains("const val NAMESPACE: String = \"ns_b\"") assertsIs true
163+
}

bindings/src/test/kotlin/io/github/ayfri/kore/bindings/api/DatapackImporterTests.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ fun testCustomRemapName() = newTest("custom_remap") {
119119
}
120120

121121
url(pack.path.toString()) {
122-
remappedName = "MyCustomName"
122+
remappings { objectName("MyCustomName") }
123123
}
124124
}
125125

@@ -219,7 +219,7 @@ fun testMixedConfiguration() = newTest("mixed_config") {
219219
}
220220

221221
url(pack1.path.toString()) {
222-
remappedName = "FirstPack"
222+
remappings { objectName("FirstPack") }
223223
}
224224

225225
url(pack2.path.toString()) {

bindings/src/test/kotlin/io/github/ayfri/kore/bindings/dungeonsAndTaverns.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.ayfri.kore.bindings
22

3-
import io.github.ayfri.kore.bindings.*
43
import io.github.ayfri.kore.bindings.api.importDatapacks
54

65
fun dungeonsAndTavernsTests() = newTest("dungeonsAndTaverns") {

website/src/jsMain/resources/markdown/doc/advanced/Bindings.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ nav-title: Bindings
55
description: Import existing datapacks and generate Kotlin bindings.
66
keywords: kore, bindings, import, datapack, github, modrinth, curseforge
77
date-created: 2026-01-23
8-
date-modified: 2026-02-03
8+
date-modified: 2026-03-04
99
routeOverride: /docs/advanced/bindings
1010
position: 3
1111
---
@@ -204,14 +204,28 @@ Defined in the block following a source:
204204

205205
```kotlin
206206
github("user.repo") {
207-
remappedName = "MyPack" // Change the generated object name
208207
packageName = "custom.pkg" // Change the package for this pack
209208
subPath = "datapacks/main" // Only import from this subfolder
210209
includes = listOf("data/**") // Only include files matching these patterns
211210
excludes = listOf("**/test/**") // Exclude files matching these patterns
211+
212+
remappings {
213+
objectName("MyPack") // Change the generated object name
214+
namespace("old_namespace", "NewNamespace") // Rename a specific namespace object
215+
}
212216
}
213217
```
214218

219+
> [!NOTE]
220+
> The `remappedName` property is deprecated. Use `remappings { objectName("...") }` instead.
221+
222+
### Namespace normalization
223+
224+
Namespace names are automatically normalized when generating Kotlin object names: dots (`.`) and other
225+
non-alphanumeric characters are replaced with underscores, then converted to PascalCase. For example,
226+
`my.namespace` becomes `MyNamespace`. You can override this behavior for any namespace using the
227+
`remappings {}` block described above.
228+
215229
## Cache
216230

217231
Downloaded files are cached in `~/.kore/cache/datapacks` to speed up subsequent runs. Use

0 commit comments

Comments
 (0)