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*, andInvoke*members onS1Interop.Generated.S1InteropTypeRegistry. - A
S1Interop.ScheduleOne.*facade andHandlestruct for every matching type. WhenIncludeMembersisfalse(the default), theHandleonly exposes generic members:HasValue,Instance,Value, andToString. No named member accessors are generated. This is useful for runtime type checks,As/TryAs/Iswrapping, and reflection-styleGet/TrySet/Invokecalls, but it does not give you named properties or methods on theHandle.
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
Handlereadonly 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 bothHandleand rawobject?receivers;- accessors for compatible public fields/properties and typed
Handlemethods for unambiguous public methods, discovered from the referenced Mono and IL2CPP metadata; - the underlying registry
Tagand resolution entries inS1Interop.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.VehicleNameasstring?;vehicle.CurrentThrottleasfloat?;vehicle.AssignedDriverasS1Interop.ScheduleOne.PlayerScripts.Player.HandlewhenPlayeris also declared;vehicle.StartEngine()asstring?;- 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
GetStatefor aStateproperty, the property wins onHandle; 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.Handlewill not exposeplayer.Moneyuntil you addS1InteropType("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/MethodInfolookups fromtypeof(...).GetField(...),typeof(...).GetProperty(...),typeof(...).GetMethod(...),AccessTools.Field(typeof(...), "..."),AccessTools.Property(typeof(...), "..."),AccessTools.PropertyGetter(typeof(...), "..."),AccessTools.PropertySetter(typeof(...), "..."), orAccessTools.Method(typeof(...), "..."). Migration-inferred declarations skip backing-field names such asHealthBackingFieldor<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
S1InteropGenerateUnityEventBridgeemitsS1Interop.Generated.S1InteropUnityEventBridgewithAdd/Removeoverloads for parameterless and one-argument UnityEvents, including a per-event delegate wrapper cache so the same managed listener can be removed later.S1InteropGenerateDelegateEventBridgeemitsS1Interop.Generated.S1InteropDelegateEventBridgewithCombine<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:
- Save the file.
- The IDE triggers a design-time build in the background, or you run
dotnet buildmanually. - 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.