using System; using System.Collections.Generic; using System.Reflection; using Unity.Collections; using UnityEngine.InputSystem.Utilities; ////TODO: reuse interaction, processor, and composite instances from prior resolves namespace UnityEngine.InputSystem { /// /// Heart of the binding resolution machinery. Consumes lists of bindings /// and spits out out a list of resolved bindings together with their needed /// execution state. /// /// /// One or more action maps can be added to the same /// resolver. The result is a combination of the binding state of all maps. /// /// The data set up by a resolver is for consumption by . /// Essentially, InputBindingResolver does all the wiring and /// does all the actual execution based on the resulting data. /// /// internal struct InputBindingResolver : IDisposable { public int totalProcessorCount; public int totalCompositeCount; public int totalInteractionCount; public int totalMapCount => memory.mapCount; public int totalActionCount => memory.actionCount; public int totalBindingCount => memory.bindingCount; public int totalControlCount => memory.controlCount; public InputActionMap[] maps; public InputControl[] controls; public InputActionState.UnmanagedMemory memory; public IInputInteraction[] interactions; public InputProcessor[] processors; public InputBindingComposite[] composites; /// /// Binding mask used to globally mask out bindings. /// /// /// This is empty by default. /// /// The bindings of each map will be matched against this /// binding. Any bindings that don't match will get skipped and not resolved to controls. /// /// Note that regardless of whether a binding will be resolved to controls or not, it will get /// an entry in . Otherwise we would have to have a more complicated /// mapping from to a binding state in . /// public InputBinding? bindingMask; private bool m_IsControlOnlyResolve; /// /// Release native memory held by the resolver. /// public void Dispose() { memory.Dispose(); } /// /// Steal the already allocated arrays from the given state. /// /// Action map state that was previously created. /// If false, the only thing that is allowed to change in the re-resolution /// is the list of controls. In other words, devices may have been added or removed but otherwise the configuration /// is exactly the same as in the last resolve. If true, anything may have changed and the resolver will only reuse /// allocations but not contents. public void StartWithPreviousResolve(InputActionState state, bool isFullResolve) { Debug.Assert(state != null, "Received null state"); Debug.Assert(!state.isProcessingControlStateChange, "Cannot re-resolve bindings for an InputActionState that is currently executing an action callback; binding resolution must be deferred to until after the callback has completed"); m_IsControlOnlyResolve = !isFullResolve; maps = state.maps; interactions = state.interactions; processors = state.processors; composites = state.composites; controls = state.controls; // Clear the arrays so that we don't leave references around. if (isFullResolve) { if (maps != null) Array.Clear(maps, 0, state.totalMapCount); if (interactions != null) Array.Clear(interactions, 0, state.totalInteractionCount); if (processors != null) Array.Clear(processors, 0, state.totalProcessorCount); if (composites != null) Array.Clear(composites, 0, state.totalCompositeCount); } if (controls != null) // Always clear this one as every resolve will change it. Array.Clear(controls, 0, state.totalControlCount); // Null out the arrays on the state so that there is no strange bugs with // the state reading from arrays that no longer belong to it. state.maps = null; state.interactions = null; state.processors = null; state.composites = null; state.controls = null; } /// /// Resolve and add all bindings and actions from the given map. /// /// /// /// This is where all binding resolution happens for actions. The method walks through the binding array /// in and adds any controls, interactions, processors, and composites as it goes. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")] public unsafe void AddActionMap(InputActionMap actionMap) { Debug.Assert(actionMap != null, "Received null map"); InputSystem.EnsureInitialized(); var actionsInThisMap = actionMap.m_Actions; var bindingsInThisMap = actionMap.m_Bindings; var bindingCountInThisMap = bindingsInThisMap?.Length ?? 0; var actionCountInThisMap = actionsInThisMap?.Length ?? 0; var mapIndex = totalMapCount; // Keep track of indices for this map. var actionStartIndex = totalActionCount; var bindingStartIndex = totalBindingCount; var controlStartIndex = totalControlCount; var interactionStartIndex = totalInteractionCount; var processorStartIndex = totalProcessorCount; var compositeStartIndex = totalCompositeCount; // Allocate an initial block of memory. We probably will have to re-allocate once // at the end to accommodate interactions and controls added from the map. var newMemory = new InputActionState.UnmanagedMemory(); newMemory.Allocate( mapCount: totalMapCount + 1, actionCount: totalActionCount + actionCountInThisMap, bindingCount: totalBindingCount + bindingCountInThisMap, // We reallocate for the following once we know the final count. interactionCount: totalInteractionCount, compositeCount: totalCompositeCount, controlCount: totalControlCount); if (memory.isAllocated) newMemory.CopyDataFrom(memory); ////TODO: make sure composite objects get all the bindings they need ////TODO: handle case where we have bindings resolving to the same control //// (not so clear cut what to do there; each binding may have a different interaction setup, for example) var currentCompositeBindingIndex = InputActionState.kInvalidIndex; var currentCompositeIndex = InputActionState.kInvalidIndex; var currentCompositePartCount = 0; var currentCompositeActionIndexInMap = InputActionState.kInvalidIndex; InputAction currentCompositeAction = null; var bindingMaskOnThisMap = actionMap.m_BindingMask; var devicesForThisMap = actionMap.devices; var isSingletonAction = actionMap.m_SingletonAction != null; // Can't use `using` as we need to use it with `ref`. var resolvedControls = new InputControlList(Allocator.Temp); // We gather all controls in temporary memory and then move them over into newMemory once // we're done resolving. try { for (var n = 0; n < bindingCountInThisMap; ++n) { var bindingStatesPtr = newMemory.bindingStates; ref var unresolvedBinding = ref bindingsInThisMap[n]; var bindingIndex = bindingStartIndex + n; var isComposite = unresolvedBinding.isComposite; var isPartOfComposite = !isComposite && unresolvedBinding.isPartOfComposite; var bindingState = &bindingStatesPtr[bindingIndex]; try { ////TODO: if it's a composite, check if any of the children matches our binding masks (if any) and skip composite if none do var firstControlIndex = 0; // numControls dictates whether this is a valid index or not. var firstInteractionIndex = InputActionState.kInvalidIndex; var firstProcessorIndex = InputActionState.kInvalidIndex; var actionIndexForBinding = InputActionState.kInvalidIndex; var partIndex = InputActionState.kInvalidIndex; var numControls = 0; var numInteractions = 0; var numProcessors = 0; // Make sure that if it's part of a composite, we are actually part of a composite. if (isPartOfComposite && currentCompositeBindingIndex == InputActionState.kInvalidIndex) throw new InvalidOperationException( $"Binding '{unresolvedBinding}' is marked as being part of a composite but the preceding binding is not a composite"); // Try to find action. // // NOTE: We ignore actions on bindings that are part of composites. We only allow // actions to be triggered from the composite itself. var actionIndexInMap = InputActionState.kInvalidIndex; var actionName = unresolvedBinding.action; InputAction action = null; if (!isPartOfComposite) { if (isSingletonAction) { // Singleton actions always ignore names. actionIndexInMap = 0; } else if (!string.IsNullOrEmpty(actionName)) { ////REVIEW: should we fail here if we don't manage to find the action actionIndexInMap = actionMap.FindActionIndex(actionName); } if (actionIndexInMap != InputActionState.kInvalidIndex) action = actionsInThisMap[actionIndexInMap]; } else { actionIndexInMap = currentCompositeActionIndexInMap; action = currentCompositeAction; } // If it's a composite, start a chain. if (isComposite) { currentCompositeBindingIndex = bindingIndex; currentCompositeAction = action; currentCompositeActionIndexInMap = actionIndexInMap; } // Determine if the binding is disabled. // Disabled if path is empty. var path = unresolvedBinding.effectivePath; var bindingIsDisabled = string.IsNullOrEmpty(path) // Also, if we can't find the action to trigger for the binding, we just go and disable // the binding. || action == null // Also, disabled if binding doesn't match with our binding mask (might be empty). || (!isComposite && bindingMask != null && !bindingMask.Value.Matches(ref unresolvedBinding, InputBinding.MatchOptions.EmptyGroupMatchesAny)) // Also, disabled if binding doesn't match the binding mask on the map (might be empty). || (!isComposite && bindingMaskOnThisMap != null && !bindingMaskOnThisMap.Value.Matches(ref unresolvedBinding, InputBinding.MatchOptions.EmptyGroupMatchesAny)) // Finally, also disabled if binding doesn't match the binding mask on the action (might be empty). || (!isComposite && action?.m_BindingMask != null && !action.m_BindingMask.Value.Matches(ref unresolvedBinding, InputBinding.MatchOptions.EmptyGroupMatchesAny)); // If the binding isn't disabled, look up controls now. We do this first as we may still disable the // binding if it doesn't resolve to any controls or resolves only to controls already bound to by // other bindings. // // NOTE: We continuously add controls here to `resolvedControls`. Once we've completed our // pass over the bindings in the map, `resolvedControls` will have all the controls for // the current map. if (!bindingIsDisabled && !isComposite) { firstControlIndex = memory.controlCount + resolvedControls.Count; if (devicesForThisMap != null) { // Search in devices for only this map. var list = devicesForThisMap.Value; for (var i = 0; i < list.Count; ++i) { var device = list[i]; if (!device.added) continue; // Skip devices that have been removed. numControls += InputControlPath.TryFindControls(device, path, 0, ref resolvedControls); } } else { // Search globally. numControls = InputSystem.FindControls(path, ref resolvedControls); } } // If the binding isn't disabled, resolve its controls, processors, and interactions. if (!bindingIsDisabled) { // NOTE: When isFullResolve==false, it is *imperative* that we do count processor and interaction // counts here come out exactly the same as in the previous full resolve. // Instantiate processors. var processorString = unresolvedBinding.effectiveProcessors; if (!string.IsNullOrEmpty(processorString)) { // Add processors from binding. firstProcessorIndex = InstantiateWithParameters(InputProcessor.s_Processors, processorString, ref processors, ref totalProcessorCount, actionMap, ref unresolvedBinding); if (firstProcessorIndex != InputActionState.kInvalidIndex) numProcessors = totalProcessorCount - firstProcessorIndex; } if (!string.IsNullOrEmpty(action.m_Processors)) { // Add processors from action. var index = InstantiateWithParameters(InputProcessor.s_Processors, action.m_Processors, ref processors, ref totalProcessorCount, actionMap, ref unresolvedBinding); if (index != InputActionState.kInvalidIndex) { if (firstProcessorIndex == InputActionState.kInvalidIndex) firstProcessorIndex = index; numProcessors += totalProcessorCount - index; } } // Instantiate interactions. if (isPartOfComposite) { // Composite's part use composite interactions if (currentCompositeBindingIndex != InputActionState.kInvalidIndex) { firstInteractionIndex = bindingStatesPtr[currentCompositeBindingIndex].interactionStartIndex; numInteractions = bindingStatesPtr[currentCompositeBindingIndex].interactionCount; } } else { var interactionString = unresolvedBinding.effectiveInteractions; if (!string.IsNullOrEmpty(interactionString)) { // Add interactions from binding. firstInteractionIndex = InstantiateWithParameters(InputInteraction.s_Interactions, interactionString, ref interactions, ref totalInteractionCount, actionMap, ref unresolvedBinding); if (firstInteractionIndex != InputActionState.kInvalidIndex) numInteractions = totalInteractionCount - firstInteractionIndex; } if (!string.IsNullOrEmpty(action.m_Interactions)) { // Add interactions from action. var index = InstantiateWithParameters(InputInteraction.s_Interactions, action.m_Interactions, ref interactions, ref totalInteractionCount, actionMap, ref unresolvedBinding); if (index != InputActionState.kInvalidIndex) { if (firstInteractionIndex == InputActionState.kInvalidIndex) firstInteractionIndex = index; numInteractions += totalInteractionCount - index; } } } // If it's the start of a composite chain, create the composite. if (isComposite) { // The composite binding entry itself does not resolve to any controls. // It creates a composite binding object which is then populated from // subsequent bindings. // Instantiate. For composites, the path is the name of the composite. var composite = InstantiateBindingComposite(ref unresolvedBinding, actionMap); currentCompositeIndex = ArrayHelpers.AppendWithCapacity(ref composites, ref totalCompositeCount, composite); // Record where the controls for parts of the composite start. firstControlIndex = memory.controlCount + resolvedControls.Count; } else { // If we've reached the end of a composite chain, finish // off the current composite. if (!isPartOfComposite && currentCompositeBindingIndex != InputActionState.kInvalidIndex) { currentCompositePartCount = 0; currentCompositeBindingIndex = InputActionState.kInvalidIndex; currentCompositeIndex = InputActionState.kInvalidIndex; currentCompositeAction = null; currentCompositeActionIndexInMap = InputActionState.kInvalidIndex; } } } // If the binding is part of a composite, pass the resolved controls // on to the composite. if (isPartOfComposite && currentCompositeBindingIndex != InputActionState.kInvalidIndex && numControls > 0) { // Make sure the binding is named. The name determines what in the composite // to bind to. if (string.IsNullOrEmpty(unresolvedBinding.name)) throw new InvalidOperationException( $"Binding '{unresolvedBinding}' that is part of composite '{composites[currentCompositeIndex]}' is missing a name"); // Assign an index to the current part of the composite which // can be used by the composite to read input from this part. partIndex = AssignCompositePartIndex(composites[currentCompositeIndex], unresolvedBinding.name, ref currentCompositePartCount); // Keep track of total number of controls bound in the composite. bindingStatesPtr[currentCompositeBindingIndex].controlCount += numControls; // Force action index on part binding to be same as that of composite. actionIndexForBinding = bindingStatesPtr[currentCompositeBindingIndex].actionIndex; } else if (actionIndexInMap != InputActionState.kInvalidIndex) { actionIndexForBinding = actionStartIndex + actionIndexInMap; } // Store resolved binding. *bindingState = new InputActionState.BindingState { controlStartIndex = firstControlIndex, // For composites, this will be adjusted as we add each part. controlCount = numControls, interactionStartIndex = firstInteractionIndex, interactionCount = numInteractions, processorStartIndex = firstProcessorIndex, processorCount = numProcessors, isComposite = isComposite, isPartOfComposite = unresolvedBinding.isPartOfComposite, partIndex = partIndex, actionIndex = actionIndexForBinding, compositeOrCompositeBindingIndex = isComposite ? currentCompositeIndex : currentCompositeBindingIndex, mapIndex = totalMapCount, wantsInitialStateCheck = action?.wantsInitialStateCheck ?? false }; } catch (Exception exception) { Debug.LogError( $"{exception.GetType().Name} while resolving binding '{unresolvedBinding}' in action map '{actionMap}'"); Debug.LogException(exception); // Don't swallow exceptions that indicate something is wrong in the code rather than // in the data. if (exception.IsExceptionIndicatingBugInCode()) throw; } } // Re-allocate memory to accommodate controls and interaction states. The count for those // we only know once we've completed all resolution. var controlCountInThisMap = resolvedControls.Count; var newTotalControlCount = memory.controlCount + controlCountInThisMap; if (newMemory.interactionCount != totalInteractionCount || newMemory.compositeCount != totalCompositeCount || newMemory.controlCount != newTotalControlCount) { var finalMemory = new InputActionState.UnmanagedMemory(); finalMemory.Allocate( mapCount: newMemory.mapCount, actionCount: newMemory.actionCount, bindingCount: newMemory.bindingCount, controlCount: newTotalControlCount, interactionCount: totalInteractionCount, compositeCount: totalCompositeCount); finalMemory.CopyDataFrom(newMemory); newMemory.Dispose(); newMemory = finalMemory; } // Add controls to array. var controlCountInArray = memory.controlCount; ArrayHelpers.AppendListWithCapacity(ref controls, ref controlCountInArray, resolvedControls); Debug.Assert(controlCountInArray == newTotalControlCount, "Control array should have combined count of old and new controls"); // Set up control to binding index mapping. for (var i = 0; i < bindingCountInThisMap; ++i) { var bindingStatesPtr = newMemory.bindingStates; var bindingState = &bindingStatesPtr[bindingStartIndex + i]; var numControls = bindingState->controlCount; var startIndex = bindingState->controlStartIndex; for (var n = 0; n < numControls; ++n) newMemory.controlIndexToBindingIndex[startIndex + n] = bindingStartIndex + i; } // Initialize initial interaction states. for (var i = memory.interactionCount; i < newMemory.interactionCount; ++i) { ref var interactionState = ref newMemory.interactionStates[i]; interactionState.phase = InputActionPhase.Waiting; interactionState.triggerControlIndex = InputActionState.kInvalidIndex; } // Initialize action data. var runningIndexInBindingIndices = memory.bindingCount; for (var i = 0; i < actionCountInThisMap; ++i) { var action = actionsInThisMap[i]; var actionIndex = actionStartIndex + i; // Correlate action with its trigger state. action.m_ActionIndexInState = actionIndex; Debug.Assert(runningIndexInBindingIndices < ushort.MaxValue, "Binding start index on action exceeds limit"); newMemory.actionBindingIndicesAndCounts[actionIndex * 2] = (ushort)runningIndexInBindingIndices; // Collect bindings for action. var firstBindingIndexForAction = -1; var bindingCountForAction = 0; var numPossibleConcurrentActuations = 0; for (var n = 0; n < bindingCountInThisMap; ++n) { var bindingIndex = bindingStartIndex + n; var bindingState = &newMemory.bindingStates[bindingIndex]; if (bindingState->actionIndex != actionIndex) continue; if (bindingState->isPartOfComposite) continue; Debug.Assert(bindingIndex <= ushort.MaxValue, "Binding index exceeds limit"); newMemory.actionBindingIndices[runningIndexInBindingIndices] = (ushort)bindingIndex; ++runningIndexInBindingIndices; ++bindingCountForAction; if (firstBindingIndexForAction == -1) firstBindingIndexForAction = bindingIndex; // Keep track of how many concurrent actuations we may be seeing on the action so that // we know whether we need to enable conflict resolution or not. if (bindingState->isComposite) { // Composite binding. Actuates as a whole. Check if the composite has successfully // resolved any controls. If so, it adds one possible actuation. if (bindingState->controlCount > 0) ++numPossibleConcurrentActuations; } else { // Normal binding. Every successfully resolved control results in one possible actuation. numPossibleConcurrentActuations += bindingState->controlCount; } } if (firstBindingIndexForAction == -1) firstBindingIndexForAction = 0; Debug.Assert(bindingCountForAction < ushort.MaxValue, "Binding count on action exceeds limit"); newMemory.actionBindingIndicesAndCounts[actionIndex * 2 + 1] = (ushort)bindingCountForAction; // See if we may need conflict resolution on this action. Never needed for pass-through actions. // Otherwise, if we have more than one bound control or have several bindings and one of them // is a composite, we enable it. var isPassThroughAction = action.type == InputActionType.PassThrough; var isButtonAction = action.type == InputActionType.Button; var mayNeedConflictResolution = !isPassThroughAction && numPossibleConcurrentActuations > 1; // Initialize initial trigger state. newMemory.actionStates[actionIndex] = new InputActionState.TriggerState { phase = InputActionPhase.Disabled, mapIndex = mapIndex, controlIndex = InputActionState.kInvalidIndex, interactionIndex = InputActionState.kInvalidIndex, isPassThrough = isPassThroughAction, isButton = isButtonAction, mayNeedConflictResolution = mayNeedConflictResolution, bindingIndex = firstBindingIndexForAction }; } // Store indices for map. newMemory.mapIndices[mapIndex] = new InputActionState.ActionMapIndices { actionStartIndex = actionStartIndex, actionCount = actionCountInThisMap, controlStartIndex = controlStartIndex, controlCount = controlCountInThisMap, bindingStartIndex = bindingStartIndex, bindingCount = bindingCountInThisMap, interactionStartIndex = interactionStartIndex, interactionCount = totalInteractionCount - interactionStartIndex, processorStartIndex = processorStartIndex, processorCount = totalProcessorCount - processorStartIndex, compositeStartIndex = compositeStartIndex, compositeCount = totalCompositeCount - compositeStartIndex, }; actionMap.m_MapIndexInState = mapIndex; var finalActionMapCount = memory.mapCount; ArrayHelpers.AppendWithCapacity(ref maps, ref finalActionMapCount, actionMap, capacityIncrement: 4); Debug.Assert(finalActionMapCount == newMemory.mapCount, "Final action map count should match old action map count plus one"); // As a final act, swap the new memory in. memory.Dispose(); memory = newMemory; } catch (Exception) { // Don't leak our native memory when we throw an exception. newMemory.Dispose(); throw; } finally { resolvedControls.Dispose(); } } private List m_Parameters; // We retain this to reuse the allocation. private int InstantiateWithParameters(TypeTable registrations, string namesAndParameters, ref TType[] array, ref int count, InputActionMap actionMap, ref InputBinding binding) { if (!NameAndParameters.ParseMultiple(namesAndParameters, ref m_Parameters)) return InputActionState.kInvalidIndex; var firstIndex = count; for (var i = 0; i < m_Parameters.Count; ++i) { // Look up type. var objectRegistrationName = m_Parameters[i].name; var type = registrations.LookupTypeRegistration(objectRegistrationName); if (type == null) { Debug.LogError( $"No {typeof(TType).Name} with name '{objectRegistrationName}' (mentioned in '{namesAndParameters}') has been registered"); continue; } if (!m_IsControlOnlyResolve) { // Instantiate it. if (!(Activator.CreateInstance(type) is TType instance)) { Debug.LogError( $"Type '{type.Name}' registered as '{objectRegistrationName}' (mentioned in '{namesAndParameters}') is not an {typeof(TType).Name}"); continue; } // Pass parameters to it. ApplyParameters(m_Parameters[i].parameters, instance, actionMap, ref binding, objectRegistrationName, namesAndParameters); // Add to list. ArrayHelpers.AppendWithCapacity(ref array, ref count, instance); } else { Debug.Assert(type.IsInstanceOfType(array[count]), "Type of instance in array does not match expected type"); ++count; } } return firstIndex; } private static InputBindingComposite InstantiateBindingComposite(ref InputBinding binding, InputActionMap actionMap) { var nameAndParametersParsed = NameAndParameters.Parse(binding.effectivePath); // Look up. var type = InputBindingComposite.s_Composites.LookupTypeRegistration(nameAndParametersParsed.name); if (type == null) throw new InvalidOperationException( $"No binding composite with name '{nameAndParametersParsed.name}' has been registered"); // Instantiate. if (!(Activator.CreateInstance(type) is InputBindingComposite instance)) throw new InvalidOperationException( $"Registered type '{type.Name}' used for '{nameAndParametersParsed.name}' is not an InputBindingComposite"); // Set parameters. ApplyParameters(nameAndParametersParsed.parameters, instance, actionMap, ref binding, nameAndParametersParsed.name, binding.effectivePath); return instance; } private static void ApplyParameters(ReadOnlyArray parameters, object instance, InputActionMap actionMap, ref InputBinding binding, string objectRegistrationName, string namesAndParameters) { foreach (var parameter in parameters) { // Find field. var field = instance.GetType().GetField(parameter.name, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field == null) { Debug.LogError( $"Type '{instance.GetType().Name}' registered as '{objectRegistrationName}' (mentioned in '{namesAndParameters}') has no public field called '{parameter.name}'"); continue; } var fieldTypeCode = Type.GetTypeCode(field.FieldType); // See if we have a parameter override. var parameterOverride = InputActionRebindingExtensions.ParameterOverride.Find(actionMap, ref binding, parameter.name, objectRegistrationName); var value = parameterOverride != null ? parameterOverride.Value.value : parameter.value; field.SetValue(instance, value.ConvertTo(fieldTypeCode).ToObject()); } } private static int AssignCompositePartIndex(object composite, string name, ref int currentCompositePartCount) { var type = composite.GetType(); ////REVIEW: check for [InputControl] attribute? ////TODO: allow this to be a property instead // Look up field. var field = type.GetField(name, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field == null) throw new InvalidOperationException( $"Cannot find public field '{name}' used as parameter of binding composite '{composite}' of type '{type}'"); ////REVIEW: should we wrap part numbers in a struct instead of using int? // Type-check. var fieldType = field.FieldType; if (fieldType != typeof(int)) throw new InvalidOperationException( $"Field '{name}' used as a parameter of binding composite '{composite}' must be of type 'int' but is of type '{type.Name}' instead"); ////REVIEW: this creates garbage; need a better solution to get to zero garbage during re-resolving // See if we've already assigned a part index. This can happen if there are multiple bindings // for the same named slot on the composite (e.g. multiple "Negative" bindings on an axis composite). var partIndex = (int)field.GetValue(composite); if (partIndex == 0) { // No, not assigned yet. Create new part index. partIndex = ++currentCompositePartCount; field.SetValue(composite, partIndex); } return partIndex; } } }