Table of Contents

Backend-neutral Harmony patching

Use S1Interop patch attributes when a mod needs to patch a Schedule One method without choosing ScheduleOne.* on Mono and Il2CppScheduleOne.* on IL2CPP in source.

The patch still lands on the real native method. S1Interop only owns target lookup: it reads the Mono type and method name from your attribute, maps that target to the active backend, resolves the MethodInfo, and applies the Harmony patch through generated code.

Basic patch

Your project still needs Harmony. Normal MelonLoader mod projects already have it through 0Harmony.dll; S1Interop only removes backend-specific target lookup from your source.

Write the target as the Mono game type name. Mark the handler methods with S1Interop's patch-role attributes.

using S1Interop;

[S1InteropPatch(
    "ScheduleOne.NPCs.Behaviour.MoveItemBehaviour",
    "IsDestinationValid",
    ParameterTypeNames = new[]
    {
        "ScheduleOne.Management.TransitRoute",
        "ScheduleOne.ItemFramework.ItemInstance",
        "string&"
    },
    Required = true)]
internal static class MoveItemDestinationPatch
{
    [S1InteropPrefix]
    private static bool Prefix(object? __instance, ref string invalidReason)
    {
        return true;
    }

    [S1InteropPostfix]
    private static void Postfix(object? __instance)
    {
    }
}

The string& entry is the by-ref marker for ref string or out string. For overloaded methods, use fully qualified Mono parameter type names. S1Interop converts those names to the IL2CPP wrapper names when the mod runs on IL2CPP.

Use Required = true for patches your mod cannot run without. Required patches throw during generated startup if the target cannot be resolved, no handler can be found, or Harmony rejects the patch.

Do not call PatchAll

Do not call PatchAll for S1Interop patch attributes.

When a project contains [S1InteropPatch], the generator emits an internal patch registrar and a module initializer. That initializer applies S1Interop patches once when the mod assembly loads.

Calling PatchAll yourself can double-apply handlers. With S1Interop, the API is the attributes. The registrar is implementation detail.

If your mod also has ordinary [HarmonyPatch] or MelonLoader patch attributes, keep treating those as ordinary Harmony/MelonLoader patches. S1Interop patch attributes are only for backend-neutral Schedule One target resolution.

Runtime status

The generated registrar records one status report per S1Interop patch in S1Interop.Generated.S1InteropHarmonyPatcher.Reports:

Status Meaning
Applied The target resolved and Harmony accepted the patch.
SkippedMissingTarget The target method could not be resolved for the active backend. Check the method name, overload parameters, IL2CPP wrapper name, and local references.
SkippedMissingHandler The patch class did not resolve a marked prefix, postfix, or finalizer handler.
PatchFailed The target resolved, but Harmony or the active backend rejected the patch.

Missing and failed optional patches are logged. Required patches throw after recording the report.

Patch target lookup uses S1InteropMemberRegistry. If a patch reports SkippedMissingTarget, inspect S1Interop.Generated.S1InteropMemberRegistry.Reports as well. It can tell you whether the active backend missed the owner type, the method, a declared parameter type, or an overload binding.

Applied only means the method body was patched. It does not prove the game still calls that method on IL2CPP. If the method is tiny, accessor-like, or only used from hot paths, IL2CPP may inline callers so your handler never runs. Patch a higher-level method when possible and validate on the actual IL2CPP branch you support.

S1I008 warns when a patch target needs review before IL2CPP use: overloaded targets without ParameterTypeNames, property/event accessors, operator methods, and methods marked for aggressive inlining or optimization. Treat it as a prompt to choose a safer target or prove the handler fires in-game.

Attribute reference

S1InteropPatch goes on the patch class.

Property Meaning
monoTypeName Required positional Mono runtime type name, such as ScheduleOne.PlayerScripts.Player.
methodName Required positional method name to patch.
ParameterTypeNames Optional Mono parameter type names for overload resolution. Add & for by-ref parameters.
Il2CppTypeName Optional override when the default ScheduleOne.* to Il2CppScheduleOne.* mapping is wrong.
OwnerAlias Optional generated owner alias. Use only when you need predictable registry names.
MethodAlias Optional generated method alias. Use only when you need predictable registry names.
IsStatic Optional hint for generated invocation helpers. Target method lookup does not require it.
Required Throw during generated startup when the patch cannot be applied. Use for patches the mod cannot safely run without.

Patch handler attributes go on methods inside the patch class:

Attribute Harmony role
S1InteropPrefix Prefix handler.
S1InteropPostfix Postfix handler.
S1InteropFinalizer Finalizer handler.

S1Interop does not expose a backend-neutral transpiler attribute. Transpilers are tied to Mono-only IL assumptions too easily. Keep them runtime-specific until S1Interop has a safer abstraction.

What gets generated

A patch declaration also registers the target owner and method as generated interop metadata. When Mono or IL2CPP references are available during compilation, missing target types or methods report S1I001 or S1I003.

At runtime, the generated patcher resolves:

S1Interop.Generated.S1InteropMemberRegistry.MoveItemBehaviourIsDestinationValidMethod

or the equivalent alias for your patch target, then calls Harmony with generated HarmonyMethod objects for the marked handlers.

The same overload-resolution path is used by:

  • migrated cached MethodInfo bindings;
  • explicit S1InteropMember method declarations;
  • backend-neutral S1Interop patch attributes.

There is no second patch-only resolver.

When to use this

Use S1Interop patch attributes when the patch target is the portability problem:

  • a direct patch mod has separate Mono and IL2CPP typeof(...) targets;
  • an S1API mod uses S1API for content but still patches a vanilla Schedule One method directly;
  • a hybrid mod has cached AccessTools.Method(...) calls next to normal gameplay code;
  • a method overload is stable, but its declaring type lives under different Mono and IL2CPP wrapper namespaces.

Keep using S1API for the gameplay workflow it owns. This feature is for patch targets S1API does not need to abstract.