diff --git a/FreeIva/InternalModules/Hatches/Hatch.cs b/FreeIva/InternalModules/Hatches/Hatch.cs index 010830bd..61b2c438 100644 --- a/FreeIva/InternalModules/Hatches/Hatch.cs +++ b/FreeIva/InternalModules/Hatches/Hatch.cs @@ -330,11 +330,91 @@ public override void OnAwake() } } + // Auto-detect airlockName per-instance at flight time against the actual part's airlocks. + // Cannot do this at prefab-load time (OnLoad_Finalize) because GetPartPrefabForInternal + // returns only the first part using the internal, polluting the shared prefab for every + // other part that shares the same internal (same root cause as attachNodeId pollution). + // Only auto-detect for hatches without a HatchConfig (i.e. those that need auto-configuration). + if (airlockName == string.Empty && !isEvaHatch && GetComponent() == null) + { + Transform[] airlocks = part.airlock == null ? null : part.FindModelTransformsWithTag(AIRLOCK_TAG); + if (airlocks != null && airlocks.Length > 0) + { + var countByName = new Dictionary(airlocks.Length); + string[] airlockNames = new string[airlocks.Length]; + for (int i = 0; i < airlocks.Length; i++) + { + string name = airlocks[i].name; + if (countByName.TryGetValue(name, out int count)) + { + airlockNames[i] = name + "," + count; + countByName[name] = count + 1; + } + else + { + airlockNames[i] = name; + countByName[name] = 1; + } + } + + float bestDistSq = 1f; + int bestIndex = -1; + Quaternion internalToPartSpace = Quaternion.Inverse(InternalModuleFreeIva.x_partToInternalSpace); + for (int i = 0; i < airlocks.Length; i++) + { + // NOTE: some airlocks have non-identity scale so InverseTransformPoint doesn't work + Vector3 hatchInPartSpace = internalToPartSpace * transform.position; + Vector3 delta = hatchInPartSpace - airlocks[i].transform.position; + Vector3 hatchInAirlockSpace = airlocks[i].transform.InverseTransformDirection(delta); + if (hatchInAirlockSpace.z >= 0 && hatchInAirlockSpace.z <= 1) + { + hatchInAirlockSpace.z = 0; + if (hatchInAirlockSpace.sqrMagnitude <= bestDistSq) + { + bestDistSq = hatchInAirlockSpace.sqrMagnitude; + bestIndex = i; + } + } + } + + if (bestIndex >= 0) + { + airlockName = airlockNames[bestIndex]; + Log.Debug($"INTERNAL '{internalModel.internalName}' hatch PROP '{internalProp.propName}' auto-detected airlock '{airlockName}' for part '{part.partInfo.name}'"); + } + } + } + if (airlockName != string.Empty || isEvaHatch) { airlockTransform = FindAirlock(part, airlockName); } + // Auto-detect the attach node for this hatch against the actual flight part. + // This must run per-instance at flight time rather than at prefab load time, + // because multiple parts can share the same internal: detecting on the prefab + // using the first matching part would write an incorrect attachNodeId into the + // shared prefab, which every subsequent part then inherits. + if (attachNodeId == string.Empty && tubeTransform != null) + { + foreach (var attachNode in part.attachNodes) + { + Vector3 attachNodeInternalSpace = InternalModuleFreeIva.x_partToInternalSpace * attachNode.position; + Vector3 localPosition = tubeTransform.InverseTransformPoint(attachNodeInternalSpace); + + if (localPosition.y >= -1 && localPosition.y <= 0) + { + localPosition.y = 0; + if (localPosition.sqrMagnitude < 0.1f) + { + Log.Debug($"INTERNAL '{internalModel.internalName}' hatch PROP '{internalProp.propName}' auto-detected attachnode '{attachNode.id}' for part '{part.partInfo.name}'"); + attachNodeId = attachNode.id; + break; + } + } + } + } + var internalModule = InternalModuleFreeIva.GetForModel(internalModel); if (internalModule == null) { diff --git a/FreeIva/InternalModules/InternalModuleFreeIva.cs b/FreeIva/InternalModules/InternalModuleFreeIva.cs index 95d24bfa..2bcebfcd 100644 --- a/FreeIva/InternalModules/InternalModuleFreeIva.cs +++ b/FreeIva/InternalModules/InternalModuleFreeIva.cs @@ -507,7 +507,7 @@ private void OnLoad_Windows(ConfigNode node) } } - static Quaternion x_partToInternalSpace = Quaternion.Euler(90, 0, 180); + internal static Quaternion x_partToInternalSpace = Quaternion.Euler(90, 0, 180); static Quaternion x_internalToPartSpace = Quaternion.Inverse(x_partToInternalSpace); void OnLoad_Finalize() @@ -517,7 +517,6 @@ void OnLoad_Finalize() Part part = ModuleFreeIva.GetPartPrefabForInternal(internalModel.internalName); List hatches = new List(); - List boundAirlockNames = new List(); foreach (var prop in internalModel.props) { @@ -526,11 +525,6 @@ void OnLoad_Finalize() { var hatchConfig = prop.GetComponent(); - if (hatch.airlockName != string.Empty) - { - boundAirlockNames.Add(hatch.airlockName); - } - if (hatchConfig != null || hatch.cutoutTargetTransformName != string.Empty) { AddPropCut(hatch); @@ -547,89 +541,9 @@ void OnLoad_Finalize() AddPropCut(hatch); } - // for attachnodes... - if (part != null && hatch.tubeTransform != null && hatch.attachNodeId == string.Empty) - { - foreach (var attachNode in part.attachNodes) - { - Vector3 attachNodeInternalSpace = x_partToInternalSpace * attachNode.position; - Vector3 localPosition = hatch.tubeTransform.InverseTransformPoint(attachNodeInternalSpace); - - if (localPosition.y >= -1 && localPosition.y <= 0) - { - localPosition.y = 0; - if (localPosition.sqrMagnitude < 0.1f) - { - Log.Debug($"INTERNAL '{internalModel.internalName}' hatch PROP '{prop.propName}' at {prop.transform.position} auto-detected attachnode '{attachNode.id}'"); - hatch.attachNodeId = attachNode.id; - break; - } - } - } - } - } - } - } - - // auto-configure airlocks - // since hatches do not have a defined "out" direction (different hatches are authored in different orientations), we do this in part space (because the airlocks DO have a defined "in" direction) - Transform[] airlocks = part?.airlock == null ? null : part.FindModelTransformsWithTag(FreeIvaHatch.AIRLOCK_TAG); - string[] airlockNames = airlocks == null ? null : new string[airlocks.Length]; - - if (airlocks != null) - { - // get a unique name for each airlock - var countByName = new Dictionary(airlocks.Length); - for (int i = 0; i < airlocks.Length; i++) - { - string name = airlocks[i].name; - if (countByName.TryGetValue(name, out int count)) - { - airlockNames[i] = name + "," + count; - ++count; - countByName[name] = count; - } - else - { - airlockNames[i] = name; - countByName[name] = 1; - } - } - - for (int airlockIndex = 0; airlockIndex < airlocks.Length; airlockIndex++) - { - var airlock = airlocks[airlockIndex]; - float bestDistanceSquared = 1; // we're pretty generous with positioning (compared to attachnodes) because these don't need to line up perfectly - FreeIvaHatch bestHatch = null; - - if (boundAirlockNames.Contains(airlockNames[airlockIndex])) continue; - - foreach (var hatch in hatches) - { - if (hatch.airlockName != string.Empty) continue; - - Vector3 hatchInPartSpace = x_internalToPartSpace * hatch.transform.position; - - // NOTE: some airlocks have non-identity scale (e.g. mk2LanderCan from restock) so InverseTransformPoint doesn't work - Vector3 airlockToHatch = hatchInPartSpace - airlock.transform.position; - Vector3 hatchInAirlockSpace = airlock.transform.InverseTransformDirection(airlockToHatch); - - // airlock's +Z is toward the part (the way the kerbal faces) - if (hatchInAirlockSpace.z >= 0 && hatchInAirlockSpace.z <= 1) - { - hatchInAirlockSpace.z = 0; - if (hatchInAirlockSpace.sqrMagnitude <= bestDistanceSquared) - { - bestDistanceSquared = hatchInAirlockSpace.sqrMagnitude; - bestHatch = hatch; - } - } - } - - if (bestHatch != null) - { - bestHatch.airlockName = airlockNames[airlockIndex]; - Log.Debug($"INTERNAL '{internalModel.internalName}' hatch PROP '{bestHatch.internalProp.propName}' at {bestHatch.internalProp.transform.position} auto-detected airlock '{bestHatch.airlockName}'"); + // attach node and airlock auto-detection are deferred to flight time (OnAwake in Hatch.cs) + // so that each part instance detects against its own nodes/airlocks, avoiding + // shared-prefab pollution when multiple parts use the same internal } } } diff --git a/GameData/FreeIva/Squad/PartCfgs/dockingPort.cfg b/GameData/FreeIva/Squad/PartCfgs/dockingPort.cfg index c6fffdf8..c1d936fb 100644 --- a/GameData/FreeIva/Squad/PartCfgs/dockingPort.cfg +++ b/GameData/FreeIva/Squad/PartCfgs/dockingPort.cfg @@ -59,7 +59,6 @@ INTERNAL name = HatchConfig attachNodeId = top dockingPortNodeName = dockingNode - airlockName = Airlock } } } diff --git a/GameData/FreeIva/Squad/PartCfgs/dockingPortJr.cfg b/GameData/FreeIva/Squad/PartCfgs/dockingPortJr.cfg index 49960a2b..8d57f0a0 100644 --- a/GameData/FreeIva/Squad/PartCfgs/dockingPortJr.cfg +++ b/GameData/FreeIva/Squad/PartCfgs/dockingPortJr.cfg @@ -47,7 +47,6 @@ INTERNAL name = HatchConfig attachNodeId = top dockingPortNodeName = dockingNode - airlockName = Airlock hideDoorWhenConnected = true } } diff --git a/GameData/FreeIva/Squad/PartCfgs/dockingPortShielded.cfg b/GameData/FreeIva/Squad/PartCfgs/dockingPortShielded.cfg index d9fe19a5..971bfca4 100644 --- a/GameData/FreeIva/Squad/PartCfgs/dockingPortShielded.cfg +++ b/GameData/FreeIva/Squad/PartCfgs/dockingPortShielded.cfg @@ -48,7 +48,6 @@ INTERNAL name = HatchConfig dockingPortNodeName = dockingNode requireDeploy = true - airlockName = Airlock } } PROP diff --git a/GameData/FreeIva/Squad/PartCfgs/dockingPortSr.cfg b/GameData/FreeIva/Squad/PartCfgs/dockingPortSr.cfg index f9a47a73..0bdf2b25 100644 --- a/GameData/FreeIva/Squad/PartCfgs/dockingPortSr.cfg +++ b/GameData/FreeIva/Squad/PartCfgs/dockingPortSr.cfg @@ -47,7 +47,6 @@ INTERNAL name = HatchConfig attachNodeId = top dockingPortNodeName = dockingNode - airlockName = Airlock hideDoorWhenConnected = true } }