Created
April 9, 2025 12:47
-
-
Save abextm/e9fd4ba19fe90f921e81abab468e9c0a to your computer and use it in GitHub Desktop.
IntelliJ script to convert to gamevals
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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