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
A live patch and the install call behind it.
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
Functions
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.
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.
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.
As onEnter, woven at every exit — a normal return and an exception leaving the method alike.
Run the body / keep the return value: the answer that changes nothing.
Remove every user patch; returns how many came off. Throws if any would not — see sweep.
onExit undone the same way; removeEnter's contract, other phase.
modify undone the same way; removeEnter's contract, other phase.
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.