Class InputConflictRecordBuilder
Turns live input actions into the flat snapshot InputConflictEngine reads.
Inherited Members
Namespace: Scylla.Input
Assembly: ScyllaInput.dll
Syntax
public static class InputConflictRecordBuilder
Remarks
This is where the cost moved to. The code this replaces recomputed a binding's modifier composite path, device prefix range and registered-scheme membership on every comparison, which meant walking the binding list twice per candidate pair. Here each is computed once per binding per snapshot.
Records are emitted in action map declaration order, then action order, then binding
order, and composite containers are omitted. That order is a contract: conflict
results are deduplicated first-hit-wins, so it decides which rival gets named. Note
that the maps must come from GetAllActionMaps and not GetAllActions,
because the latter returns dictionary order and would make the reported map
nondeterministic.
Methods
BuildFromActionManager(IInputActionManager, Func<string, string, string>, Func<ScyllaInputBinding, bool>, IInputControlAliasResolver, ref InputConflictRecord[])
Builds a snapshot of every non-composite binding known to a live action manager.
Declaration
public static int BuildFromActionManager(IInputActionManager actionManager, Func<string, string, string> resolveScopeID, Func<ScyllaInputBinding, bool> isInRegisteredSchemes, IInputControlAliasResolver aliasResolver, ref InputConflictRecord[] buffer)
Parameters
| Type | Name | Description |
|---|---|---|
| IInputActionManager | actionManager | The action manager to read. |
| Func<string, string, string> | resolveScopeID | Maps a map identifier and an action identifier to their scope key. Both are needed because an action name is only unique within its map. |
| Func<ScyllaInputBinding, bool> | isInRegisteredSchemes | Reports whether a binding sits in a declared column. |
| IInputControlAliasResolver | aliasResolver | Rewrites control paths so alternative spellings of one control compare equal. Pass Shared to compare paths verbatim. |
| InputConflictRecord[] | buffer | Scratch array, grown in place as needed. Pass the same array across rebuilds so a refresh allocates nothing once it has settled. |
Returns
| Type | Description |
|---|---|
| int | How many leading entries of the buffer were populated. |
Exceptions
| Type | Condition |
|---|---|
| ArgumentNullException | Thrown when the action manager or any delegate is |
BuildFromDocument(InputActionsDocument, Func<string, string, string>, Func<InputBindingNode, bool>, IInputControlAliasResolver, ref InputConflictRecord[])
Builds a snapshot from a parsed .inputactions document, with no live module.
Declaration
public static int BuildFromDocument(InputActionsDocument document, Func<string, string, string> resolveScopeID, Func<InputBindingNode, bool> isInRegisteredSchemes, IInputControlAliasResolver aliasResolver, ref InputConflictRecord[] buffer)
Parameters
| Type | Name | Description |
|---|---|---|
| InputActionsDocument | document | The parsed document. |
| Func<string, string, string> | resolveScopeID | Maps a map name and an action name to their scope key. See CreateDocumentScopeResolver(InputActionsDocument, ConfigLayout). |
| Func<InputBindingNode, bool> | isInRegisteredSchemes | Reports whether a binding sits in a declared column. See CreateDocumentSchemeFilter(IReadOnlyList<ConfigSchemeGroup>). |
| IInputControlAliasResolver | aliasResolver | Rewrites control paths to their canonical spelling. |
| InputConflictRecord[] | buffer | Scratch array, grown in place as needed. |
Returns
| Type | Description |
|---|---|
| int | How many leading entries of the buffer were populated. |
Remarks
The edit-time counterpart of BuildFromActionManager(IInputActionManager, Func<string, string, string>, Func<ScyllaInputBinding, bool>, IInputControlAliasResolver, ref InputConflictRecord[]). Both must produce identical records for the same asset, which is what lets an editor promise exactly the conflicts the game will enforce. The differential test in the Input suite is what holds that true.
The binding index is the ordinal position within the map's binding array filtered to this action, in document order, counting composite containers. Unity numbers an action's bindings that way, so dropping the containers while numbering would shift every part that follows one.
Bindings are gathered by matching the action name rather than by taking a contiguous range, because the format permits one action's bindings to be interleaved with another's. That is unusual, since Unity's own editor appends in blocks, but a hand-edited or generated asset is still valid and must not be misread. No assertion guards against interleaving for the same reason.
Exceptions
| Type | Condition |
|---|---|
| ArgumentNullException | Thrown when the document or any delegate is |
BuildLiveLookupCandidate(string, string, string, string, string, int, string, string, bool, in InputInteractionSignature)
Builds one record for a caller that scans live Unity bindings under LiveBindingLookup.
Declaration
public static InputConflictRecord BuildLiveLookupCandidate(string actionID, string mapID, string scopeID, string schemeGroup, string partName, int bindingIndex, string rawPath, string normalizedPath, bool isComposite, in InputInteractionSignature interactionSignature)
Parameters
| Type | Name | Description |
|---|---|---|
| string | actionID | The owning action's name. |
| string | mapID | The owning map's name. |
| string | scopeID | The scope key, which for a context scope is the map name. |
| string | schemeGroup | The binding's declared groups, or empty. |
| string | partName | The composite part name, or empty. |
| int | bindingIndex | Flat binding index within the action. |
| string | rawPath | The path as authored or overridden. |
| string | normalizedPath | The path with alternative spellings resolved. |
| bool | isComposite | Whether this is a composite container. |
| InputInteractionSignature | interactionSignature | What drives the binding. Deliberately required and undefaulted: it is the one field here that is not neutral, and a caller that could omit it would silently disable the interaction rule for this whole feed. |
Returns
| Type | Description |
|---|---|
| InputConflictRecord | The record. |
Remarks
The binding manager answers "what else is on this control" by walking the live Unity objects on every call, because the answer has to reflect an override Unity wrote moments ago and no notification of that reaches this module. It therefore cannot hold a snapshot, and builds one record at a time as it scans.
Four fields are left neutral, and that is only sound under one policy.
ModifierDepth is zero, IsModifierPart is false,
CompositeSignature is empty and IsRebindable is false, because
computing the first three needs a walk over the action's sibling bindings for every
candidate on every call, which is exactly the per-comparison cost the record
snapshot exists to avoid. That is safe only because
LiveBindingLookup has
ScopeByModifierDepth and ScopeByModifierComposite off, and because
the engine never reads IsRebindable.
If one of those flags is ever turned on, the fix is to give this caller a real
snapshot, not to fabricate the fields here. Turning a flag on without doing that
would not fail loudly: the rules would read the neutral values, compare zero
against zero, and silently never fire. Policy_Presets_HaveTheExpectedFlags
in the Input test suite exists to catch that edit.
The interaction signature is not one of them. It is a required
parameter, filled by the caller from the binding it already has in hand, which is
what makes ScopeByInteraction honest on this feed rather than an example of
the mistake above. It is the worked example of the paragraph before this one: the
way to turn a rule on here is to supply the data, and the way to keep that true is
to leave the parameter undefaulted so the compiler asks.
BuildQuery(ScyllaInputAction, int, string, string, string, string, IInputControlAliasResolver)
Builds the record for a binding the caller is asking about.
Declaration
public static InputConflictRecord BuildQuery(ScyllaInputAction action, int bindingIndex, string mapID, string scopeID, string schemeGroup, string path, IInputControlAliasResolver aliasResolver)
Parameters
| Type | Name | Description |
|---|---|---|
| ScyllaInputAction | action | The owning action. |
| int | bindingIndex | Flat binding index within the action. |
| string | mapID | The owning map's identifier. |
| string | scopeID | The action's scope key. |
| string | schemeGroup | The column being asked about. |
| string | path | The control path to test. |
| IInputControlAliasResolver | aliasResolver | Rewrites the path so alternative spellings compare equal. Must be the same resolver the candidate set was built with, or the query and the candidates disagree on what a path means and the comparison silently reports nothing. |
Returns
| Type | Description |
|---|---|
| InputConflictRecord | The query record. |
Remarks
The supplied path is used rather than the binding's own, because callers legitimately ask "what would collide if this binding were on that control". Everything else, including the part name and the modifier metadata, is read from the binding that actually sits at the index.
A query is never scheme-filtered. The registered-scheme rule applies to candidates only, which is what lets a caller ask about a column the manager was not initialized with and get an empty answer rather than an error.
Exceptions
| Type | Condition |
|---|---|
| ArgumentNullException | Thrown when the resolver is |
CreateDocumentSchemeFilter(IReadOnlyList<ConfigSchemeGroup>)
Builds the registered-column test a document feed needs, from a set of columns.
Declaration
public static Func<InputBindingNode, bool> CreateDocumentSchemeFilter(IReadOnlyList<ConfigSchemeGroup> schemeGroups)
Parameters
| Type | Name | Description |
|---|---|---|
| IReadOnlyList<ConfigSchemeGroup> | schemeGroups | The declared columns. |
Returns
| Type | Description |
|---|---|
| Func<InputBindingNode, bool> | A delegate reporting whether a binding sits in a declared column. |
Remarks
Reproduces ScyllaInputBinding.BelongsToGroup, and has to keep doing so: this
is the editor's feed and that is the game's, so the two disagreeing means the
window reports conflicts the running game does not have, or misses ones it does.
Both are now a token-wise test through InputBindingGroups.Contains, so a
column named Keyboard does not match a binding in Keyboard&Mouse.
A binding declaring no group is in every column, because it belongs to no scheme and therefore always participates.
CreateDocumentScopeResolver(InputActionsDocument, ConfigLayout)
Builds the scope lookup a document feed needs, from a config layout.
Declaration
public static Func<string, string, string> CreateDocumentScopeResolver(InputActionsDocument document, ConfigLayout layout)
Parameters
| Type | Name | Description |
|---|---|---|
| InputActionsDocument | document | The parsed document. |
| ConfigLayout | layout | The config layout declaring the groups. |
Returns
| Type | Description |
|---|---|
| Func<string, string, string> | A delegate mapping a map name and an action name to their scope key. |
Remarks
Reproduces the live manager's group membership rules exactly, because a document that resolved scopes differently would report different conflicts than the game. An action claimed by two groups throws, an action claimed by none lands in UNGROUPED_ID.
Membership is keyed by map and action together, since an action name is only unique
within its map. A group naming actions rather than maps claims a bare name in every
map that declares it, and a "MapName/ActionName" name in just the one map,
which is the same reading InputRebindManager applies.
Maps named by a group are looked up by name only, never by identifier. The live manager keys maps by name, so accepting an identifier here would let a layout resolve at edit time and silently fail to at run time.
Exceptions
| Type | Condition |
|---|---|
| ArgumentNullException | Thrown when the document or the layout is |
| ArgumentException | Thrown when two groups claim the same action. |