A useful mod report identifies the first stage that failed, preserves the exact exception, and gives another modder a controlled way to reproduce it. Start with four stages: compilation, plugin loading, hook application, and target method execution.

An error at one stage calls for evidence from that stage. A compiler missing MonoMod.Cil needs reference information. A plugin type rejected by the loader needs the loader’s first exception. An IL matcher that returns false needs the target method and predicate. An InvalidProgramException after the patched method begins needs the emitted instructions and stack shape.

Build against one recorded assembly set

The diagnostic hook compilation used Roslyn directly with /nostdlib+, /target:library, /langversion:7.3, and explicit references. Those references included the installed Mono mscorlib, System, System.Core, Rain World game assemblies, Unity assemblies, BepInEx, HookGen, MonoMod.Utils, and Mono.Cecil. The public example build script references only the assemblies its examples need. An IL example also needs the appropriate local MonoMod and Cecil references.

This route restored no packages and copied nothing into the game. It confirmed syntax, member availability, and generated hook signatures against Rain World build 22785462. A normal project can use the BepInEx 5 template, but its target framework, language version, and references still need to agree with this Mono and BepInEx 5 installation. First plugin provides the repository’s public build route. Runtime reference records the baseline hashes.

Keep compile and runtime claims separate. A successful DLL build does not show that BepInEx discovered the plugin, that MonoMod applied a detour, or that a handler ran safely.

Log decisions instead of every update

BaseUnityPlugin.Logger provides a BepInEx.Logging.ManualLogSource. In the installed BepInEx 5.4.17.0, it exposes LogFatal, LogError, LogWarning, LogMessage, LogInfo, and LogDebug, along with the general Log method.

Use LogInfo for a phase marker that occurs once, such as successful hook registration. Use LogWarning when the plugin can continue with a feature disabled. Use LogError with the caught exception when setup failed unexpectedly. The configured listeners decide which levels reach the log output.

Avoid writing a line from Player.Update or another frequent hook. The logging work can become the measured slowdown, and repeated lines can hide the first useful exception. Count frequent events in memory and report after a fixed number of calls or from a separately measured test interval. The public PlayerCounter example writes one debug message per 600 observed updates. That interval counts calls rather than elapsed time.

Follow the first failing stage

SymptomControlled reproductionFirst evidenceNext check
Plugin type does not loadStart once with only the test pluginLoader log and first exceptionBase class, plugin metadata, BepInEx version, missing assemblies
On behavior is absentTrigger one known actionOne marker before and after origGenerated signature, subscription timing, duplicate subscriptions
IL patch is skippedStart on the recorded baselineMethod, build, matcher, candidate countDecompiled method body and changed nearby members
Patched method is rejectedRun only the patch and trigger its methodFull exception and target methodStack shape, return type, labels, delegate signature
Frame time risesRepeat one action for a fixed durationCounts and elapsed time outside the hookAllocations, repeated scans, logging, duplicate events

For an IL miss, let the matcher report a concise skip and keep the detailed record in the reproduction notes. IL hooks explains TryGotoNext and stack accounting. For an ordinary On hook, confirm the exact generated delegate signature and place temporary markers on both sides of orig. Remove or lower those markers after the failing side is known.

Compare one variable at a time

Use the smallest counter that identifies the work: hook calls, collection items scanned, actions completed, or matcher candidates found during setup. Measure a fixed action for a fixed duration. Keep the room, input sequence, enabled modules, and duration the same.

Compare the plugin disabled with the plugin enabled. If the feature has both On and IL parts, compare On alone with On plus IL. A single slow frame does not identify a cause. A repeated increase tied to one changed component gives the next investigation a narrower target.

Allocation counts and a profiler capture can distinguish time spent in garbage collection from time spent in the handler. Neither was collected for this baseline, so this article makes no measured frame time or allocation claim.

Record a reproducible failure

Include these fields when reporting a hook problem:

  • Rain World, Steam build, BepInEx and MonoMod versions, plus relevant assembly hashes
  • Plugin version, DLL hash, and enabled plugin list when it can be shared safely
  • Exact room, input sequence, and test duration
  • Target method, generated hook signature, and any IL predicates, insertion direction, and candidate count
  • Complete exception and stack trace, identifying whether it occurred at load, hook application, or method execution

Test an IL patch alone first, then add likely compatibility candidates one at a time. This separates a changed vanilla target from interaction between two patches. Preserve the original failing log before trying another configuration.

No game process was started for the checked examples. Plugin discovery, detour application, log listener output, frame timing, allocation behavior, and interaction with other mods remain runtime checks.

Sources