Skip to content

Instantly share code, notes, and snippets.

@abextm
Created April 9, 2025 12:47
Show Gist options
  • Save abextm/e9fd4ba19fe90f921e81abab468e9c0a to your computer and use it in GitHub Desktop.
Save abextm/e9fd4ba19fe90f921e81abab468e9c0a to your computer and use it in GitHub Desktop.
IntelliJ script to convert to gamevals
/*
* This Inteliij IDE Console script migrates plugins from old *ID classes
* to newer gameval-based classes that are generated from Jagex's
* actual internal names for things.
*
* This script has a few quirks
* - InventoryID migration is not perfect, but should work for most cases
* and any failed cases will result in a compilatino error
* - star importing net.runelite.api.* will make it fully qualify any new
* references, so ideally remove this from your codebase before running it
* - Javadocs are not updated, this has to be done manually
* - This only updates referenced to existing ID classes, if you have a bunch
* of IDs (eg varbits) this will not migrate them, though they will exist
* in the new gameval VarbitID
* - Sometimes IntelliJ will red highlight this whole script, this can be ignored.
* - Running this script will entirely freeze your IDE for possibly several minutes.
* RuneLite itself takes about 10 minutes to run, Quest Helper is similar.
*
* To run this script, create a new `IDE Scripting Console` > Kotlin, then paste
* this in it, then execute it by selecting it all (Ctrl+A) then Ctrl+Enter
*/
import com.intellij.openapi.command.CommandProcessor
import com.intellij.psi.*
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.ProjectScope
import com.intellij.psi.search.searches.ReferencesSearch
import org.jetbrains.kotlin.utils.mapToSetOrEmpty
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet
val IDE = bindings["IDE"] as com.intellij.ide.script.IDE
try {
val project = IDE.project
val jpf = JavaPsiFacade.getInstance(project)
fun complain(element: PsiElement?) {
val trace = Thread.currentThread().stackTrace
val line = trace[2].lineNumber
val line2 = trace[3].lineNumber
IDE.print("$line2/$line: $element at ${element?.containingFile}")
}
fun value(e: PsiExpression?): Int? {
if (e is PsiLiteralExpression) {
return e.value as? Int
}
if (e is PsiUnaryExpression && "-".equals(e.operationSign.text)) {
return value(e.operand)?.let { -it }
}
complain(e)
return null
}
open class Refactorer(
from: List<String>,
to: List<String>,
fieldFilter: (PsiField) -> Boolean = { true },
classFiler: (PsiClass) -> Boolean = { true },
val fieldValues: Map<String, Int> = mapOf(),
) {
val from = from.mapToSetOrEmpty {
jpf.findClass(it, GlobalSearchScope.allScope(project))!!
}
val map = HashMap<Number, String>()
init {
for (toClass in to) {
val target = jpf.findClass(toClass, GlobalSearchScope.allScope(project))!!
addClass(target, fieldFilter, classFiler)
}
}
private fun addClass(target: PsiClass, filter: (PsiField) -> Boolean, classFilter: (PsiClass) -> Boolean) {
if (classFilter(target)) {
for (field in target.allFields) {
if (field.hasInitializer() && filter(field)) {
val init = field.initializer
if (init is PsiLiteralExpression) {
map.put(init.value as Number, target.qualifiedName + "." + field.name)
} else {
complain(init)
}
}
}
}
for (inner in target.innerClasses) {
addClass(inner, filter, classFilter)
}
}
fun apply(changed: MutableSet<PsiElement>) {
for (clazz in from) {
for (ref in ReferencesSearch.search(clazz, ProjectScope.getContentScope(project))) {
rebind(changed, ref.element.parent)
}
}
}
open fun rebind(changed: MutableSet<PsiElement>, p: PsiElement) {
if (p is PsiImportStaticReferenceElement || p is PsiImportStaticStatement) {
val todo = ArrayList<PsiElement>()
p.containingFile.accept(object : JavaRecursiveElementVisitor() {
override fun visitReferenceElement(reference: PsiJavaCodeReferenceElement) {
super.visitReferenceElement(reference)
if (!reference.isQualified) {
val res = reference.resolve()
if (res is PsiField && from.contains(res.containingClass)) {
todo.add(reference.element)
}
}
}
})
for (el in todo) {
rebind(changed, el)
}
} else if (p is PsiJavaCodeReferenceElement) {
val target = p.resolve()
var value: Int? = null;
if (target is PsiField) {
value = fieldValues.get(target.name);
}
if (value != null) {
} else if (target is PsiEnumConstant) {
value = value(target.argumentList?.expressions?.getOrNull(0))
} else if (target is PsiField) {
value = value(target.initializer)
}
if (value != null) {
val field = map.get(value as Number)
if (field != null) {
changed.add(p.replace(jpf.parserFacade.createExpressionFromText(field, p)))
} else if (value == -1) {
p.replace(jpf.parserFacade.createExpressionFromText("" + value, p))
} else {
complain(target)
}
} else {
complain(target)
}
} else if (p !is PsiImportStatement) {
complain(p)
}
}
override fun toString(): String {
return map.toString()
}
}
val refs = listOf(
Refactorer(
listOf("net.runelite.api.ItemID", "net.runelite.api.NullItemID"),
listOf("net.runelite.api.gameval.ItemID")
),
Refactorer(
listOf("net.runelite.api.ObjectID", "net.runelite.api.NullObjectID"),
listOf("net.runelite.api.gameval.ObjectID")
),
Refactorer(
listOf("net.runelite.api.NpcID", "net.runelite.api.NullNpcID"),
listOf("net.runelite.api.gameval.NpcID")
),
Refactorer(
listOf("net.runelite.api.AnimationID"),
listOf("net.runelite.api.gameval.AnimationID")
),
/*Refactorer(
listOf("net.runelite.api.SpriteID"),
listOf("net.runelite.api.gameval.SpriteID")
),*/
Refactorer(
listOf("net.runelite.api.GraphicID"),
listOf("net.runelite.api.gameval.SpotanimID")
),
Refactorer(
listOf("net.runelite.api.Varbits"),
listOf("net.runelite.api.gameval.VarbitID")
),
Refactorer(
listOf("net.runelite.api.VarPlayer"),
listOf("net.runelite.api.gameval.VarPlayerID")
),
Refactorer(
listOf("net.runelite.api.widgets.ComponentID"),
listOf("net.runelite.api.gameval.InterfaceID"),
classFiler = { it.innerClasses.isEmpty() && it.name != "All" },
),
Refactorer(
listOf("net.runelite.api.widgets.InterfaceID"),
listOf("net.runelite.api.gameval.InterfaceID"),
classFiler = { it.innerClasses.isNotEmpty() },
),
object : Refactorer(
listOf("net.runelite.api.InventoryID"),
listOf("net.runelite.api.gameval.InventoryID"),
fieldValues = mapOf("KINGDOM_OF_MISCELLANIA" to 390, "TRADE" to 90, "BANK" to 95, "LUNAR_CHEST" to 847, "PUZZLE_BOX" to 140, "DRIFT_NET_FISHING_REWARD" to 307, "CHAMBERS_OF_XERIC_CHEST" to 581, "MONKEY_MADNESS_PUZZLE_BOX" to 221, "GROUP_STORAGE_INV" to 660, "FISHING_TRAWLER_REWARD" to 0, "THEATRE_OF_BLOOD_CHEST" to 612, "EQUIPMENT" to 94, "TOA_REWARD_CHEST" to 811, "INVENTORY" to 93, "SEED_VAULT" to 626, "WILDERNESS_LOOT_CHEST" to 797, "FORTIS_COLOSSEUM_REWARD_CHEST" to 843, "BARROWS_REWARD" to 141, "GROUP_STORAGE" to 659, "TRADEOTHER" to 32858)
) {
override fun rebind(changed: MutableSet<PsiElement>, p: PsiElement) {
if (p is PsiTypeElement) {
changed.add(p.replace(jpf.parserFacade.createTypeElementFromText("int", p)))
} else {
super.rebind(changed, p);
}
}
},
)
val invGetId = jpf.findClass("net.runelite.api.InventoryID", GlobalSearchScope.allScope(project))!!
.findMethodsByName("getId")
CommandProcessor.getInstance().executeCommand(project, {
IDE.application.runWriteAction {
val changed = HashSet<PsiElement>()
val invGetIdRefs = ReferencesSearch.search(invGetId[0] as PsiMethod, ProjectScope.getContentScope(project))
for (ref in invGetIdRefs) {
val el = ref.element;
val p = el.parent;
if (p is PsiMethodCallExpression && el is PsiReferenceExpression && el.qualifierExpression != null) {
p.replace(el.qualifierExpression!!)
} else {
complain(el.parent)
}
}
for (ref in refs) {
ref.apply(changed)
}
val files = HashSet<PsiFile>()
for (el in changed) {
files.add(el.containingFile);
}
val jcsm = JavaCodeStyleManager.getInstance(project)
for (file in files) {
jcsm.removeRedundantImports(file as PsiJavaFile)
}
for (el in changed) {
jcsm.shortenClassReferences(el);
}
}
}, "rewrite ids", "rewrite ids")
} catch (e: Exception) {
IDE.print(e.stackTraceToString())
throw e
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment