Skip to content

Commit 22750a8

Browse files
authored
New interface format & add dispatcher annotation support (#768)
* Convert interface components into a flat structure * Add method annotations to ScriptMetadataTask * Fix conflicting cow names * Fix dispatcher processing * Remove unnecessary code * Delete wildcard annotation * Add support for option wildcards * Split shared gradle and script metadata task to avoid polluting other modules with embeddable compiler dependency * Fixes
1 parent 68dcf6c commit 22750a8

File tree

103 files changed

+8735
-3201
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+8735
-3201
lines changed

build-logic/build.gradle.kts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
`kotlin-dsl`
3+
}
4+
5+
repositories {
6+
gradlePluginPortal()
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
implementation(libs.kotlin.embeddable)
12+
}
13+
14+
gradlePlugin {
15+
plugins {
16+
create("metadataTask") {
17+
id = "tasks.metadata"
18+
implementationClass = "MetadataPlugin"
19+
}
20+
}
21+
}

build-logic/settings.gradle.kts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pluginManagement {
2+
repositories {
3+
gradlePluginPortal()
4+
mavenCentral()
5+
}
6+
}
7+
8+
dependencyResolutionManagement {
9+
@Suppress("UnstableApiUsage")
10+
repositories {
11+
mavenCentral()
12+
}
13+
14+
versionCatalogs {
15+
create("libs") {
16+
from(files("../gradle/libs.versions.toml"))
17+
}
18+
}
19+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import org.gradle.api.Plugin
2+
import org.gradle.api.Project
3+
4+
class MetadataPlugin : Plugin<Project> {
5+
override fun apply(project: Project) {}
6+
}
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import org.gradle.api.DefaultTask
2+
import org.gradle.api.file.DirectoryProperty
3+
import org.gradle.api.tasks.*
4+
import org.gradle.work.ChangeType
5+
import org.gradle.work.Incremental
6+
import org.gradle.work.InputChanges
7+
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
8+
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
9+
import org.jetbrains.kotlin.com.intellij.openapi.Disposable
10+
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
11+
import org.jetbrains.kotlin.com.intellij.psi.PsiManager
12+
import org.jetbrains.kotlin.config.CompilerConfiguration
13+
import org.jetbrains.kotlin.lexer.KtTokens
14+
import org.jetbrains.kotlin.psi.KtClass
15+
import org.jetbrains.kotlin.psi.KtFile
16+
import org.jetbrains.kotlin.psi.KtNamedFunction
17+
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
18+
import java.io.File
19+
20+
/**
21+
* Gradle task which incrementally collects annotation info about classes inside a given directory.
22+
* Collects:
23+
* - @Script annotations for invocation
24+
* - Overridden methods
25+
* - Annotations on methods and it's data
26+
* - Processes annotation wildcards
27+
*/
28+
abstract class ScriptMetadataTask : DefaultTask() {
29+
30+
private enum class WildcardType {
31+
NpcId,
32+
InterfaceId,
33+
ComponentId,
34+
ObjectId,
35+
ItemId,
36+
NpcOption,
37+
InterfaceOption,
38+
FloorItemOption,
39+
ObjectOption,
40+
ItemOption,
41+
}
42+
43+
// List of annotation names and their parameters
44+
private val annotations: Map<String, List<Pair<String, WildcardType>>> = mapOf()
45+
46+
@get:Incremental
47+
@get:InputFiles
48+
@get:PathSensitive(PathSensitivity.RELATIVE)
49+
abstract val inputDirectory: DirectoryProperty
50+
51+
@get:Internal
52+
abstract var dataDirectory: File
53+
54+
@get:Internal
55+
abstract var resourceDirectory: File
56+
57+
@get:OutputFile
58+
abstract var scriptsFile: File
59+
60+
init {
61+
description = "Analyzes Kotlin files and extracts annotation information"
62+
group = "metadata"
63+
}
64+
65+
@TaskAction
66+
fun execute(inputChanges: InputChanges) {
67+
val start = System.currentTimeMillis()
68+
69+
val npcIds = mutableSetOf<String>()
70+
val itemIds = mutableSetOf<String>()
71+
val objectIds = mutableSetOf<String>()
72+
val interfaceIds = mutableSetOf<String>()
73+
val componentIds = mutableSetOf<String>()
74+
collectIds(npcIds, itemIds, objectIds, interfaceIds, componentIds)
75+
val options = System.currentTimeMillis()
76+
val npcOptions = loadOptions("npc-options")
77+
val itemOptions = loadOptions("item-options")
78+
val floorItemOptions = loadOptions("floor-item-options")
79+
val objectOptions = loadOptions("object-options")
80+
val interfaceOptions = loadOptions("interface-options")
81+
println("Loaded ${npcOptions.size} npc, ${itemOptions.size} item, ${floorItemOptions.size} floor item, ${objectOptions.size} object, ${interfaceOptions.size} interface options in ${System.currentTimeMillis() - options}ms")
82+
83+
val lines: MutableList<String>
84+
if (!inputChanges.isIncremental) {
85+
// Clean output for non-incremental runs
86+
scriptsFile.delete()
87+
logger.info("Non-incremental run: analyzing all files")
88+
lines = mutableListOf()
89+
} else {
90+
lines = if (scriptsFile.exists()) scriptsFile.readLines().toMutableList() else mutableListOf()
91+
}
92+
val disposable = Disposer.newDisposable()
93+
val environment = createKotlinEnvironment(disposable)
94+
var scripts = 0
95+
var methodCount = 0
96+
var annotationCount = 0
97+
val instance = PsiManager.getInstance(environment.project)
98+
val scriptClasses = mutableListOf<Pair<KtClass, String>>()
99+
for (change in inputChanges.getFileChanges(inputDirectory)) {
100+
val file = change.file
101+
if (change.changeType == ChangeType.REMOVED) {
102+
removeName(lines, file.nameWithoutExtension)
103+
continue
104+
}
105+
if (!file.isFile || !file.name.endsWith(".kt")) {
106+
continue
107+
}
108+
val localFile = environment.findLocalFile(file.path)
109+
if (localFile == null) {
110+
println("Local file not found: ${file.path}")
111+
continue
112+
}
113+
val psiFile: KtFile = instance.findFile(localFile) as KtFile
114+
115+
val classes = psiFile.collectDescendantsOfType<KtClass>()
116+
val packageName = psiFile.packageFqName.asString()
117+
if (change.changeType == ChangeType.MODIFIED) {
118+
for (name in classes.map { it.name }) {
119+
if (!lines.removeIf { it.endsWith("$packageName.$name") }) {
120+
removeName(lines, name)
121+
}
122+
}
123+
}
124+
if (change.changeType == ChangeType.MODIFIED || change.changeType == ChangeType.ADDED) {
125+
for (ktClass in classes) {
126+
val className = ktClass.name ?: "Anonymous"
127+
if (ktClass.annotationEntries.any { anno -> anno.shortName!!.asString() == "Script" }) {
128+
scriptClasses.add(ktClass to "$packageName.$className")
129+
}
130+
}
131+
}
132+
}
133+
134+
for ((ktClass, packagePath) in scriptClasses) {
135+
val methods = ktClass.declarations.filterIsInstance<KtNamedFunction>().filter { it.hasModifier(KtTokens.OVERRIDE_KEYWORD) }
136+
scripts++
137+
if (methods.isEmpty()) {
138+
lines.add(packagePath)
139+
continue
140+
}
141+
for (method in methods) {
142+
methodCount++
143+
val returnType = method.typeReference
144+
val signature = "${method.name}(${method.valueParameters.joinToString(",") { param -> param.typeReference!!.getTypeText() }})${if (returnType == null) "" else ":${returnType.getTypeText()}"}"
145+
val entries = method.annotationEntries
146+
if (entries.isEmpty()) {
147+
lines.add("${signature}|$packagePath")
148+
continue
149+
}
150+
for (annotation in entries) {
151+
val annotationName = annotation.shortName?.asString() ?: ""
152+
val info = annotations[annotationName] ?: error("Annotation $annotationName metadata not found. Make sure your annotation is registered in ScriptMetadataTask.kt")
153+
val params = Array<MutableList<String>>(info.size) { mutableListOf() }
154+
// Resolve annotation field names
155+
var index = 0
156+
for (arg in annotation.valueArguments) {
157+
val name = arg.getArgumentName()?.asName?.asString()
158+
val value = arg.getArgumentExpression()?.text?.trim('"') ?: ""
159+
val idx = if (name != null) info.indexOfFirst { it.first == name } else index++
160+
params[idx].add(value)
161+
}
162+
for (i in info.indices) {
163+
val value = params[i].first()
164+
// Expand wildcards into matches
165+
if (value.contains("*") || value.contains("#")) {
166+
val set = when (info[i].second) {
167+
WildcardType.NpcId -> npcIds
168+
WildcardType.InterfaceId -> interfaceIds
169+
WildcardType.ComponentId -> componentIds
170+
WildcardType.ObjectId -> objectIds
171+
WildcardType.ItemId -> itemIds
172+
WildcardType.NpcOption -> npcOptions
173+
WildcardType.InterfaceOption -> interfaceOptions
174+
WildcardType.FloorItemOption -> floorItemOptions
175+
WildcardType.ObjectOption -> objectOptions
176+
WildcardType.ItemOption -> itemOptions
177+
}
178+
val matches = set.filter { wildcardEquals(value, it) }
179+
if (matches.isEmpty()) {
180+
error("No matches for wildcard '${value}' in $packagePath ${annotation.text}")
181+
}
182+
params[i].removeAt(0)
183+
params[i].addAll(matches)
184+
}
185+
}
186+
generateCombinations(params) { args ->
187+
annotationCount++
188+
lines.add("@${annotation.shortName}|${args.joinToString(":")}|$signature|$packagePath")
189+
}
190+
}
191+
}
192+
}
193+
scriptsFile.writeText(lines.joinToString("\n"))
194+
disposable.dispose()
195+
println("Metadata for $scripts scripts, $methodCount methods and $annotationCount annotations collected in ${System.currentTimeMillis() - start} ms")
196+
}
197+
198+
private fun generateCombinations(arrays: Array<MutableList<String>>, index: Int = 0, current: MutableList<String> = mutableListOf(), call: (List<String>) -> Unit) {
199+
if (index == arrays.size) {
200+
call.invoke(current)
201+
return
202+
}
203+
val currentArray = arrays[index]
204+
for (element in currentArray) {
205+
current.add(element)
206+
generateCombinations(arrays, index + 1, current, call)
207+
current.removeAt(current.size - 1)
208+
}
209+
}
210+
211+
private fun loadOptions(type: String): Set<String> {
212+
return ScriptMetadataTask::class.java.getResource("$type.txt")!!.readText().lines().toSet()
213+
}
214+
215+
private fun collectIds(
216+
npcIds: MutableSet<String>,
217+
itemIds: MutableSet<String>,
218+
objectIds: MutableSet<String>,
219+
interfaceIds: MutableSet<String>,
220+
componentIds: MutableSet<String>,
221+
) {
222+
val start = System.currentTimeMillis()
223+
for (file in dataDirectory.walkTopDown()) {
224+
if (!file.isFile) {
225+
continue
226+
}
227+
if (file.name.endsWith(".npcs.toml")) {
228+
for (line in file.readLines()) {
229+
if (line.startsWith('[')) {
230+
npcIds.add(line.substringBefore(']').trim('['))
231+
}
232+
}
233+
} else if (file.name.endsWith(".items.toml")) {
234+
for (line in file.readLines()) {
235+
if (line.startsWith('[')) {
236+
itemIds.add(line.substringBefore(']').trim('['))
237+
}
238+
}
239+
} else if (file.name.endsWith(".objs.toml")) {
240+
for (line in file.readLines()) {
241+
if (line.startsWith('[')) {
242+
objectIds.add(line.substringBefore(']').trim('['))
243+
}
244+
}
245+
} else if (file.name.endsWith(".ifaces.toml")) {
246+
for (line in file.readLines()) {
247+
if (line.startsWith('[')) {
248+
val key = line.substringBefore(']').trim('[')
249+
if (key.contains(".")) {
250+
componentIds.add(key.substringAfter('.'))
251+
} else {
252+
interfaceIds.add(key)
253+
}
254+
}
255+
}
256+
}
257+
}
258+
println("Collected ${npcIds.size} npcs, ${itemIds.size} items, ${objectIds.size} objects, ${interfaceIds.size} interfaces, ${componentIds.size} components in ${System.currentTimeMillis() - start}ms")
259+
}
260+
261+
private fun removeName(scriptsList: MutableList<String>, name: String?) {
262+
if (scriptsList.filter { it.endsWith(".$name") }.map { it.split("|").last() }.distinct().count() > 1) {
263+
error("Deletion failed due to duplicate script names: ${scriptsList.filter { it.endsWith(".$name") }.map { it.split("|").last() }.distinct()}. Please update scripts.txt or run `gradle cleanScriptMetadata`.")
264+
}
265+
scriptsList.removeIf { it.endsWith(".$name") }
266+
}
267+
268+
private fun createKotlinEnvironment(disposable: Disposable): KotlinCoreEnvironment = KotlinCoreEnvironment.createForProduction(
269+
disposable,
270+
CompilerConfiguration.EMPTY,
271+
EnvironmentConfigFiles.JVM_CONFIG_FILES,
272+
)
273+
274+
275+
private fun wildcardEquals(wildcard: String, other: String): Boolean {
276+
if (wildcard == "*") {
277+
return true
278+
}
279+
var wildIndex = 0
280+
var otherIndex = 0
281+
var starIndex = -1
282+
var matchIndex = -1
283+
284+
while (otherIndex < other.length) {
285+
when {
286+
wildIndex < wildcard.length && (wildcard[wildIndex] == '#' && other[otherIndex].isDigit()) -> {
287+
wildIndex++
288+
otherIndex++
289+
}
290+
wildIndex < wildcard.length && wildcard[wildIndex] == '*' -> {
291+
starIndex = wildIndex
292+
matchIndex = otherIndex
293+
wildIndex++
294+
}
295+
wildIndex < wildcard.length && wildcard[wildIndex] == other[otherIndex] -> {
296+
wildIndex++
297+
otherIndex++
298+
}
299+
starIndex != -1 -> {
300+
wildIndex = starIndex + 1
301+
matchIndex++
302+
otherIndex = matchIndex
303+
}
304+
else -> return false
305+
}
306+
}
307+
308+
while (wildIndex < wildcard.length && wildcard[wildIndex] == '*') {
309+
wildIndex++
310+
}
311+
312+
return wildIndex == wildcard.length && otherIndex == other.length
313+
}
314+
315+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Activate
2+
Destroy
3+
Examine
4+
Inspect
5+
Lay
6+
Light
7+
Observe
8+
Remove
9+
Reset
10+
Study
11+
Take
12+
Target

0 commit comments

Comments
 (0)