Patches

object Patches

Cross-eval patch registry, the lifecycle layer over Patch. An execute_code snippet is stateless per call, so live patches are kept in a game-loader static map — a later eval can handle, remove or removeAll them.

The id is minted per INSTALL and comes back on the PatchHandle — it is also the key a handler is passed, so a handler can name its own patch. Re-installing a target mints a new one, which is what makes an id kept across a re-install answer null / -1 rather than report the replacement under the old name. handles lists what is live if one was not kept.

Only user patches live here — the mod's own hooks are Mixins, so removeAll is safe.

A handler outlives its eval, and so does the scriptguard woven into it — but that guard answers only to that eval's id, which is never raised again once the eval ends. Past that point nothing stops a handler that loops: cheap and non-blocking is the contract, and it is unenforced.

Install from the parallel lane: the retransform's safepoint stops the game either way, but on a tick lane it also spends that tick's budget, so the eval reports a timeout over a patch that went in fine. A suggestion, not a rule: a script that reads tick-affine state to decide what to weave has nowhere else to go.

Selecting an overload

params names the parameter types of the ONE overload to patch, or is omitted to patch every overload of the name. Each entry may be written as * (any type in that slot), a bare simple name (ItemStack, Properties), the source spelling of a nested class (Item.Properties), or the full mojmap FQN — all in mojmap regardless of the runtime's actual namespace. They are JVM type names, matched case-sensitively — int, not Kotlin's Int — and array suffixes are kept (ItemStack[]).

When params is given, exactly one SIGNATURE must match or the call throws with the real candidates listed — so listOf("*", "*") is a usable probe: it either resolves the only 2-argument overload, or fails with the list to pick from. Copies of one signature carried by several classloaders are not an ambiguity; all of them are woven.

Class, method and parameter names are all mojmap, translated forward through the loaded mapping table. Where no table is loaded the translation is the identity — so on a non-mojmap runtime whose remap provisioning failed, the runtime's own names resolve, weave, and come back in the candidate lists; they are how you patch there.

Coexisting

Two patches share a method only while neither can change what it does. Observers stack freely; a writable one (intercept, modify) evicts everything else there, because a cancelled body makes the rest fire or not depending on weave order. Give BOTH sides a tag to override that — it says "I know there are several, and I accept that they run in install order".

Removing

remove takes an id. removeEnter / removeExit / removeIntercept / removeModify take what the matching install took — the install line with its callback dropped — and mirror it one for one: an install names a phase, so an uninstall does too.

params there is the filter above, run against what a patch WOVE, so the two calls need not spell a signature alike: listOf("Level", "int") and listOf("*", "*") remove each other's patches. A patch whose class has yet to load wove nothing, so its own filter answers instead — matching there is by spelling, and only * bridges the two sides.

Types

Link copied to clipboard
private class Live(val handle: PatchHandle, val target: Patches.Target)

A live patch and the install call behind it.

Link copied to clipboard
private data class Target(val className: String, val methodName: String, val params: List<String>?, val phase: Patch.Phase, val tag: String?)

The install call a patch answers to, and — being a data class — "the same install call" is its own equality. A hand-written comparison goes stale the first time a field is added here.

Properties

Link copied to clipboard
Link copied to clipboard
private val PROCEED: PatchDecision

The one answer carrying no value, so it needs no allocation.

Functions

Link copied to clipboard

The live patch under id, or null if none holds it — including one replaced by a re-install, which mints its own id. Worth re-reading for a patch that was PatchHandle.pending at install: its targets fill in once the class loads, as do PatchHandle.fires and PatchHandle.failures.

Link copied to clipboard

Every live patch, sorted like sweep's return. A handle carries its own PatchHandle.id, so a map keyed by it would store the name twice — handle is the lookup by one, this is the walk over all.

Link copied to clipboard
fun intercept(className: String, methodName: String, params: List<String>? = null, tag: String? = null, onReturn: PatchExitCallback? = null, cb: PatchInterceptCallback): PatchHandle

As onEnter, but the callback decides: it may rewrite args, or skip the body and supply the return value. Pass onReturn to also observe what the body produced — read-only, and silent on a skipped call.

Link copied to clipboard
fun modify(className: String, methodName: String, params: List<String>? = null, tag: String? = null, cb: PatchModifyCallback): PatchHandle

As onExit, but the callback may replace the return value. It cannot stop the body, so side effects have already happened. Constructors are out of reach for every phase, this one included: resolution reads getDeclaredMethods, which does not list them.

Link copied to clipboard
fun onEnter(className: String, methodName: String, params: List<String>? = null, tag: String? = null, cb: PatchEnterCallback): PatchHandle

Patch method entry. Re-installing the same target — same class, method, params and tag — removes what held it first, so re-running a snippet doesn't stack transformers on the method. tag is how two patches coexist on one method instead.

Link copied to clipboard
fun onExit(className: String, methodName: String, params: List<String>? = null, tag: String? = null, cb: PatchExitCallback): PatchHandle

As onEnter, woven at every exit — a normal return and an exception leaving the method alike.

Link copied to clipboard

Run the body / keep the return value: the answer that changes nothing.

Link copied to clipboard
fun remove(vararg ids: String): List<String>

Remove the patches named, restoring the original bytecode. Variadic so a partial removal batches the way removeAll does: the whole set costs one retransform, not one each.

Link copied to clipboard

Remove every user patch; returns how many came off. Throws if any would not — see sweep.

Link copied to clipboard
fun removeEnter(className: String, methodName: String, params: List<String>? = null): List<String>

onEnter undone by copying its own first line, whatever id the patch holds. params narrows to one overload selection; omit it to take every one.

Link copied to clipboard
fun removeExit(className: String, methodName: String, params: List<String>? = null): List<String>

onExit undone the same way; removeEnter's contract, other phase.

Link copied to clipboard
fun removeIntercept(className: String, methodName: String, params: List<String>? = null): List<String>

intercept undone the same way. Both blueprints, since which one was woven depended on an onReturn an uninstall has no reason to repeat.

Link copied to clipboard
fun removeModify(className: String, methodName: String, params: List<String>? = null): List<String>

modify undone the same way; removeEnter's contract, other phase.

Link copied to clipboard
fun returns(value: Any?): PatchDecision

Skip the body and return value, or replace the return value with it. A value the patched method cannot return is refused and counted as a handler failure, leaving the method untouched.

Link copied to clipboard
private fun sweep(pick: (Patches.Live) -> Boolean): List<String>

Unweave every patch pick selects. An entry leaves the map only once its advice is really gone, so a sweep that threw leaves every patch it picked listed, woven and firing — and retryable.