Table of Contents

Declarations

Declarations tell the S1Interop.Generators package which game types and members should be resolved through generated backend-neutral helpers. They are assembly-level attributes written to a single S1Interop-owned source file and consumed by the Roslyn generator at compile time.

Most projects should let sdkgen write these declarations first, then keep the generated file small and reviewable as the mod touches more of the game surface. See SDK generation for the CLI side.

For what the generator emits from these declarations and when, see Generated output.

Declaration roles

Declaration Use it when
S1InteropNamespace You need broad runtime type registration for a namespace, usually without generating member facades for every type.
S1InteropType You need a backend-neutral facade and compatible public member helpers for one game type.
S1InteropMember You need an explicit binding for a private member, ambiguous overload, pinned Harmony target, or migration-discovered reflection access.
S1InteropPatch You need a backend-neutral Harmony patch target that resolves to the native Mono or IL2CPP method at runtime.
S1InteropGenerateUnityEventBridge You need generated listener conversion helpers for simple UnityEvent add/remove calls.
S1InteropGenerateDelegateEventBridge You need generated delegate combine/remove helpers for migrated event assignments.

Prefer the broadest declaration that still describes the real need. Do not use S1InteropMember to manually enumerate ordinary public members after a type facade can discover them from metadata.

S1InteropNamespace

Registers every public type in a namespace for runtime type resolution without requiring per-type attributes.

[assembly: S1Interop.S1InteropNamespace(
    "ScheduleOne",
    IncludeSubnamespaces = true)]

Properties

Property Type Default Meaning
namespaceName (positional) string Required The Mono runtime namespace to include, such as ScheduleOne. Il2Cpp-prefixed names are normalized back to the Mono root.
IncludeSubnamespaces bool false Include child namespaces.
IncludeMembers bool false Discover compatible public members for matching types. Off by default so full-SDK builds stay reviewable.

What this generates

  • One registry entry per matching public, non-generic, top-level type in referenced Mono/IL2CPP assemblies.
  • Per-type *Name, *MonoName, *Il2CppName, Resolve, Create, As*, TryAs*, Is*, Get*, TrySet*, and Invoke* members on S1Interop.Generated.S1InteropTypeRegistry.
  • A S1Interop.ScheduleOne.* facade and Handle struct for every matching type. When IncludeMembers is false (the default), the Handle only exposes generic members: HasValue, Instance, Value, and ToString. No named member accessors are generated. This is useful for runtime type checks, As/TryAs/Is wrapping, and reflection-style Get/TrySet/Invoke calls, but it does not give you named properties or methods on the Handle.

To get named member accessors (fields, properties, and methods on the Handle), either set IncludeMembers = true on the namespace declaration or add a separate S1InteropType declaration for the specific types where you want member facades. Concrete signatures are emitted only where discovered metadata is backend-neutral. S1InteropType is preferred for individual types because it keeps member discovery scoped and reviewable.

Namespace declarations are the practical way to seed broad SDK coverage from sdkgen --full-sdk without emitting thousands of per-type attributes.

Important

Adding or changing declarations triggers a design-time build, but the update is not instantaneous. After saving the declaration file, there is a short delay before the IDE regenerates the source and IntelliSense reflects the new symbols. See Build timing below.

S1InteropType

Opts a single game type into a backend-neutral facade with discovered public member accessors.

[assembly: S1Interop.S1InteropType(
    "ScheduleOne.Vehicles.LandVehicle",
    Alias = "LandVehicle")]

Properties

Property Type Default Meaning
monoTypeName (positional) string Required The full Mono runtime type name, such as ScheduleOne.Vehicles.LandVehicle.
Il2CppTypeName string? Computed Override the IL2CPP wrapper type name when the default ScheduleOne.* to Il2CppScheduleOne.* mapping is wrong or ambiguous.
Alias string? Simple name The generated registry and facade name. Defaults to the type's simple name, sanitized to a valid identifier.

What this generates

A S1Interop.ScheduleOne.Vehicles.LandVehicle facade with:

  • a Handle readonly struct wrapping the runtime instance;
  • Type, TypeName, Create(...), Create<T>(...), CreateHandle(...), TryCreate(out Handle, ...);
  • Is(object?), TryAs(object?, out Handle), As(object?);
  • Get/TrySet/Invoke/Invoke<T> reflection helpers for both Handle and raw object? receivers;
  • accessors for compatible public fields/properties and typed Handle methods for unambiguous public methods, discovered from the referenced Mono and IL2CPP metadata;
  • the underlying registry Tag and resolution entries in S1Interop.Generated.S1InteropTypeRegistry.

S1InteropType is the difference between a generic wrapper and useful mod code. It opts the type into member discovery, so Handle can expose named members instead of only HasValue, Instance, and Value.

Concrete signatures are emitted when Mono and IL2CPP metadata agree. Examples:

  • vehicle.VehicleName as string?;
  • vehicle.CurrentThrottle as float?;
  • vehicle.AssignedDriver as S1Interop.ScheduleOne.PlayerScripts.Player.Handle when Player is also declared;
  • vehicle.StartEngine() as string?;
  • an S1Interop-owned enum mirror when enum values match on both backends.

Rules that matter in real mods:

  • Read-only fields and properties get getters, but no named TrySet... helper.
  • If a method collides with a property shape, such as GetState for a State property, the property wins on Handle; the static facade method remains available.
  • Undeclared game wrappers, collections, by-ref values, generic methods, overloaded methods, and unsafe conversions stay on object/generic fallback helpers.
  • Generated backing fields are skipped. Use the real public field or property.
  • Namespace-only declarations do not create named accessors. If you only declare S1InteropNamespace("ScheduleOne"), Player.Handle will not expose player.Money until you add S1InteropType("ScheduleOne.PlayerScripts.Player").

If the referenced assemblies do not contain the requested type, the generator reports S1I001. Declaration diagnostics are quiet when no game reference surface is available, so package-restore and docs-only builds do not fail.

S1InteropMember

Explicit member binding for cases the type facade cannot safely infer.

[assembly: S1Interop.S1InteropType(
    "ScheduleOne.PlayerScripts.PlayerCamera",
    Alias = "PlayerCamera")]

[assembly: S1Interop.S1InteropMember(
    "PlayerCamera",
    "Awake",
    Alias = "PlayerCameraAwake",
    Kind = S1Interop.S1InteropMemberKind.Method,
    ParameterTypeNames = new string[] { })]

Properties

Property Type Default Meaning
ownerAlias (positional) string Required The alias of a declared S1InteropType. Must match an existing type alias, not a runtime type name.
memberName (positional) string Required The runtime member name to resolve.
Alias string? memberName The generated accessor name on the owner facade and Handle.
Kind S1InteropMemberKind FieldOrProperty FieldOrProperty, Method, Field, or Property.
IsStatic bool false Whether the target member is static.
ParameterTypeNames string[] Empty Mono type names used to disambiguate overloaded methods.

What this generates

Named accessors with the chosen Alias on the owner facade and, when the instance member has a safe signature, on Handle. Use explicit declarations for private members, better aliases, ambiguous overloads, pinned bindings, and migration-inferred reflection patterns.

When Mono and IL2CPP metadata identify one compatible member, the generator enriches the declaration with return, value, parameter type, and parameter-name metadata. Explicit field/property and method bindings can still get concrete scalar, string, enum, and declared-facade Handle signatures.

If metadata is missing, ambiguous, incompatible, generic, or uses a conversion S1Interop does not understand yet, the explicit binding stays on the object/generic Get<T>, Get...Value<T>, TrySet, and Invoke<T> fallback helpers.

If the owner alias is unknown, the generator reports S1I002. If the member is not found on the resolved owner type, it reports S1I003.

Use S1InteropMember for:

  • private or internal members;
  • better aliases for readability;
  • overloaded methods that need explicit parameter type names or by-ref markers;
  • Mono/IL2CPP disagreements that need a pinned binding;
  • migration-inferred reflection patterns that cannot yet be represented by the automatic type facade, including simple cached FieldInfo/PropertyInfo/MethodInfo lookups from typeof(...).GetField(...), typeof(...).GetProperty(...), typeof(...).GetMethod(...), AccessTools.Field(typeof(...), "..."), AccessTools.Property(typeof(...), "..."), AccessTools.PropertyGetter(typeof(...), "..."), AccessTools.PropertySetter(typeof(...), "..."), or AccessTools.Method(typeof(...), "..."). Migration-inferred declarations skip backing-field names such as HealthBackingField or <Health>k__BackingField; use the real field or property instead.

S1InteropPatch

Backend-neutral Harmony patch target declaration. Put it on the patch class and mark handler methods with S1InteropPrefix, S1InteropPostfix, or S1InteropFinalizer.

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

Patch declarations generate normal type/member registry entries for the target and an internal Harmony registrar. You do not call PatchAll; S1Interop applies generated patch declarations once when the mod assembly loads.

Required = true makes target resolution or patch-application failure throw during generated startup. Leave it false for optional compatibility patches that can safely skip on one backend.

When Mono or IL2CPP references are available during compilation, patch targets validate like explicit method members. Missing target types report S1I001; missing target methods report S1I003. S1I008 warns on patch targets that need IL2CPP review, such as overloaded methods without ParameterTypeNames, accessor-like targets, operators, or methods marked for aggressive inlining/optimization. Runtime reports still matter because IL2CPP can expose metadata for a method that Harmony cannot patch safely.

See Backend-neutral Harmony patching for the full workflow.

Bridge declarations

Migration can add bridge declarations when source contains simple delegate patterns that need different runtime delegate shapes:

[assembly: S1Interop.S1InteropGenerateUnityEventBridge]
[assembly: S1Interop.S1InteropGenerateDelegateEventBridge]

What these generate

  • S1InteropGenerateUnityEventBridge emits S1Interop.Generated.S1InteropUnityEventBridge with Add/Remove overloads for parameterless and one-argument UnityEvents, including a per-event delegate wrapper cache so the same managed listener can be removed later.
  • S1InteropGenerateDelegateEventBridge emits S1Interop.Generated.S1InteropDelegateEventBridge with Combine<T>/Remove<T> helpers for delegate event fields.

These support migrated code. Keep S1API event or UI helpers when they already cover the workflow.

Where to keep declarations

Keep declarations in generated or S1Interop-owned source such as S1Interop.Generated/S1Interop.BackendNeutral.cs. That keeps the migration boundary obvious and makes rollback or regeneration easier to review.

The declarations and the S1Interop.Generators package are a compile-time unit. If a project contains S1Interop declarations, it must also reference the generator package so the attributes, registries, diagnostics, and facade helpers are emitted during the build.

Build timing

Declarations are read by the generator during compilation. After you edit the declaration file:

  1. Save the file.
  2. The IDE triggers a design-time build in the background, or you run dotnet build manually.
  3. The new or changed generated symbols appear in IntelliSense and are compiled into the assembly.

There is a short delay between saving and the generated symbols updating. If a generated symbol is missing immediately after editing a declaration, trigger a full build (dotnet build).

The most common cause of "generated type not found" is that no design-time build has completed since the declaration was added. If the symbol still does not appear after a full build, check that the project references S1Interop.Generators and that the declaration file is included in the compilation.

Important

When you start with S1InteropNamespace and later add S1InteropType, the Handle gains named member accessors after the next build. Before that build, it still only exposes generic members (HasValue, Instance, Value).

Generated symbols are emitted into the same compilation as the rest of the project. They are not separate assemblies and are not referenced from a package at runtime. The S1Interop.Generators package only ships the generator DLL under analyzers/dotnet/cs; it does not add a runtime DLL reference.