Renaming mixin classes and patching mixins.json (90506a4) breaks Fabric
mods at runtime, proven on LoVisual (2026-10-13):
- When obfuscated packages are randomized per class the config patcher
computes an empty common prefix and writes `package: ""`. Fabric/Mixin
then drops every mixin as "orphaned", so `RenderPipelineMixin
implements IRenderPipeline` never applies, the interface is not
injected into `RenderPipeline`, and the entrypoint dies with
`ClassCastException: RenderPipeline cannot be cast to IRenderPipeline`.
- `IMixinConfigPlugin.shouldApplyMixin` compares the runtime mixin class
name against string literals (`Set.of("dev.loki...Mixin")`); renaming
desyncs those sets and optional sodium/iris/xaero mixins get applied
when the target mod is absent.
Re-enable absorb_mixins: mixin entry classes and the config plugin keep
their names, and locking a class freezes its members too (needed for
@Accessor/@Invoker/@Overwrite name lookups). Calls from locked mixin
bytecode are still rewritten consistently, and the family mechanism keeps
interface method names (IRenderPipeline) in sync with the frozen mixin
methods. patch_mixins_json stays but is identity for locked classes.
Verified: LoVisual obf jar loads all 221 mixins (0 orphaned), the main
menu renders, the entrypoint runs.
- Remove absorb_mixins keep rule (mixin classes now renameable)
- Add mixins.rs: patch_mixins_json rewrites package + entries via
common prefix of mapped names; patch_refmap_json rewrites all strings
- Resources pass routes *.mixins.json and *refmap.json to specialized
patchers, falls back to generic text remap
Reference: OBF fabric-obfuscator patchMixinsJson/patchRefmapJson.
Gson/Jackson/fastjson2/Moshi bind fields by reflection from a Class
literal, with no field-name string for the string-based detection:
renaming such a field silently breaks deserialization. Absorb_databind
finds the nearest Const::Class before fromJson/readValue/parseObject/
adapter calls (10-insn window, control-flow boundaries stop the search)
and keeps every field of that class; the class itself still renames.
Tests: noxis-analysis/tests/dto.rs (red first), keep unit tests moved
to tests/keep.rs (file budget). Code comments migrated to English.
Proven by paranoid runs over lorep (17 classes, fully green),
LoVisual (4081 classes) and oc2r (1278 classes) with per-class
java -Xverify:all, each fix first reproduced as a red test:
- merge: Top absorbs instead of Top-wins (javac declares Top for
slots unwritten on some inflow; precise types reject those edges).
Regression: loop_head_keeps_top_for_loop_local.
- emit: collapse [Long|Double, Top] pairs on operand stacks
(javac writes one entry; the pair is rejected). Regression:
wide_stack_join_emits_single_entries.
- xfer ArrayLoad: push the wide pair, fixing lcmp underflow.
Regression: array_load_wide_pushes_pair.
- xfer <init>: convert only the receiver's own allocation site
instead of every Uninitialized (nested construction clobbered
the outer object). Regression: nested_construction_keeps_outer.
- xfer ANewArray: array operands gain one dimension ([B] -> [[B)
instead of an illegal L-wrap ([L[B;). Covered by the same test.
- jar load: duplicate class names (multi-release module-info
twins) keep the first occurrence instead of crashing save.
Test: duplicate_class_names_keep_first.
- strings/constants: skip methods past MAX_METHOD_INSNS (8000)
so giants cannot overflow the 64KB writer ceiling. Tests:
oversized_methods_are_skipped in both passes.
- merge errors now name both types being merged.
Also: em-dash removed repo-wide (724 spots, rule added to
todo.md), allow-lints only in tests (scratch example deleted),
code comments migrated to English in every file this change
touches.