diff --git a/CHANGES.txt b/CHANGES.txt index 28b75c935..e3c7df80e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ Contract Configurator 0.7.5 - Possible fix for duration counters resetting (thanks dorin6565 and many others). - Fix for HasCrew with count > 0 (thanks Yemo). +- Improvements to tracking across docked vessels. - SpawnPassengers now only spawns passengers on Kerbin (closes EPL exploit). - Sequence parameter no longer fails if child parameters complete out of order. - Added hideChildren attribute to contract parameters to hide children. diff --git a/GameData/ContractConfigurator/CC_RemoteTech.dll b/GameData/ContractConfigurator/CC_RemoteTech.dll index 861d5af3b..0d04f3881 100644 Binary files a/GameData/ContractConfigurator/CC_RemoteTech.dll and b/GameData/ContractConfigurator/CC_RemoteTech.dll differ diff --git a/GameData/ContractConfigurator/CC_SCANsat.dll b/GameData/ContractConfigurator/CC_SCANsat.dll index bab13e3a6..57c3c74e5 100644 Binary files a/GameData/ContractConfigurator/CC_SCANsat.dll and b/GameData/ContractConfigurator/CC_SCANsat.dll differ diff --git a/GameData/ContractConfigurator/ContractConfigurator.dll b/GameData/ContractConfigurator/ContractConfigurator.dll index 738f7a0c8..62874b178 100644 Binary files a/GameData/ContractConfigurator/ContractConfigurator.dll and b/GameData/ContractConfigurator/ContractConfigurator.dll differ diff --git a/source/ContractConfigurator/ContractParameter/VesselParameter.cs b/source/ContractConfigurator/ContractParameter/VesselParameter.cs index fd09fd86c..03a4fe621 100644 --- a/source/ContractConfigurator/ContractParameter/VesselParameter.cs +++ b/source/ContractConfigurator/ContractParameter/VesselParameter.cs @@ -388,7 +388,7 @@ protected virtual void OnVesselCreate(Vessel vessel) // Go through the hashes to try to set the parameters for this vessel KeyValuePair? dockedInfo = null; - foreach (uint hash in GetVesselHashes(vessel)) + foreach (uint hash in vessel.GetHashes()) { if (dockedVesselInfo.ContainsKey(hash)) { @@ -438,7 +438,7 @@ protected virtual void OnPartJointBreak(PartJoint p) if (strength == ParamStrength.MEDIUM) { strength = null; - foreach (uint hash in GetVesselHashes(p.Parent.vessel)) + foreach (uint hash in p.Parent.vessel.GetHashes()) { if (dockedVesselInfo.ContainsKey(hash)) { @@ -472,7 +472,6 @@ protected virtual void OnPartJointBreak(PartJoint p) protected virtual void OnPartAttach(GameEvents.HostTargetAction e) { - if (HighLogic.LoadedScene == GameScenes.EDITOR || e.host.vessel == null || e.target.vessel == null) { return; @@ -555,7 +554,7 @@ protected virtual void OnPartAttach(GameEvents.HostTargetAction e) /// The completion time private void SaveSubVesselInfo(Vessel vessel, ParamStrength strength, double completionTime) { - foreach (uint hash in GetVesselHashes(vessel)) + foreach (uint hash in vessel.GetHashes()) { if (!dockedVesselInfo.ContainsKey(hash) || dockedVesselInfo[hash].Key < strength) @@ -565,133 +564,6 @@ private void SaveSubVesselInfo(Vessel vessel, ParamStrength strength, double com } } - /// - /// Create a hash of the vessel. - /// - /// The vessel to hash - /// A list of hashes for this vessel - public static List GetVesselHashes(Vessel vessel) - { - LoggingUtil.LogVerbose(typeof(VesselParameter), "-> GetVesselHashes(" + vessel.id + ")"); - - if (vessel.rootPart == null) - { - return new List(); - } - - Queue queue = new Queue(); - Dictionary visited = new Dictionary(); - Dictionary dockedParts = new Dictionary(); - Queue otherVessel = new Queue(); - - // Add the root - queue.Enqueue(vessel.rootPart); - visited[vessel.rootPart] = 1; - - // Do a BFS of all parts. - List hashes = new List(); - uint hash = 0; - while (queue.Count > 0 || otherVessel.Count > 0) - { - bool decoupler = false; - - // Start a new ship - if (queue.Count == 0) - { - // Reset our hash - hashes.Add(hash); - hash = 0; - - // Find an unhandled part to use as the new vessel - Part px; - while (px = otherVessel.Dequeue()) { - if (visited[px] != 2) - { - queue.Enqueue(px); - break; - } - } - dockedParts.Clear(); - continue; - } - - Part p = queue.Dequeue(); - - // Check if this is for a new vessel - if (dockedParts.ContainsKey(p.flightID)) - { - otherVessel.Enqueue(p); - continue; - } - - // Special handling of certain modules - for (int i = 0; i < p.Modules.Count; i++) - { - PartModule pm = p.Modules.GetModule(i); - - // If this is a docking node, track the docked part - if (pm.moduleName == "ModuleDockingNode") - { - ModuleDockingNode dock = (ModuleDockingNode)pm; - if (dock.dockedPartUId != 0) - { - dockedParts[dock.dockedPartUId] = dock.dockedPartUId; - } - } - else if (pm.moduleName == "ModuleDecouple") - { - // Just assume all parts can decouple from this, it's easier and - // effectively the same thing - decoupler = true; - - // Parent may be null if this is the root of the stack - if (p.parent != null) - { - dockedParts[p.parent.flightID] = p.parent.flightID; - } - - // Add all children as possible new vessels - foreach (Part child in p.children) - { - dockedParts[child.flightID] = child.flightID; - } - } - } - - // Go through our child parts - foreach (Part child in p.children) - { - if (!visited.ContainsKey(child)) - { - queue.Enqueue(child); - visited[child] = 1; - } - } - - // Confirm if parent part has been visited - if (p.parent != null && !visited.ContainsKey(p.parent)) - { - queue.Enqueue(p.parent); - visited[p.parent] = 1; - } - - // Add this part to the hash - if (!decoupler) - { - hash ^= p.flightID; - } - - // We've processed this node - visited[p] = 2; - } - - // Add the last hash - hashes.Add(hash); - - LoggingUtil.LogVerbose(typeof(VesselParameter), "<- GetVesselHashes = " + hashes); - return hashes; - } - /// /// Checks whether the given vessel meets the condition, and completes the contract parameter as necessary. /// diff --git a/source/ContractConfigurator/ContractParameter/VesselParameterGroup.cs b/source/ContractConfigurator/ContractParameter/VesselParameterGroup.cs index d770d161b..c7e7e5149 100644 --- a/source/ContractConfigurator/ContractParameter/VesselParameterGroup.cs +++ b/source/ContractConfigurator/ContractParameter/VesselParameterGroup.cs @@ -110,7 +110,12 @@ protected override string GetTitle() { if (ParameterCount == 1) { - return GetParameter(0).Title; + output = ""; + if (trackedVessel != null) + { + output += "Vessel: " + trackedVessel.vesselName + ": "; + } + output += GetParameter(0).Title; } } diff --git a/source/ContractConfigurator/ContractVesselTracker.cs b/source/ContractConfigurator/ContractVesselTracker.cs index 8e0fd2e6c..c2e63e1e6 100644 --- a/source/ContractConfigurator/ContractVesselTracker.cs +++ b/source/ContractConfigurator/ContractVesselTracker.cs @@ -16,17 +16,50 @@ namespace ContractConfigurator GameScenes.FLIGHT, GameScenes.TRACKSTATION, GameScenes.SPACECENTER)] public class ContractVesselTracker : ScenarioModule { + private class VesselInfo + { + public Guid id; + public uint hash; + + public VesselInfo(Guid id, uint hash) + { + this.id = id; + this.hash = hash; + } + + public VesselInfo(Vessel v) + { + this.id = v.id; + this.hash = v.GetHashes().First(); + } + } + public static ContractVesselTracker Instance { get; private set; } public static EventData> OnVesselAssociation = new EventData>("OnVesselAssociation"); public static EventData> OnVesselDisassociation = new EventData>("OnVesselDisassociation"); - private Dictionary vessels = new Dictionary(); + private Dictionary vessels = new Dictionary(); + private Vessel lastBreak = null; public ContractVesselTracker() { Instance = this; } + public void Start() + { + GameEvents.onPartJointBreak.Add(new EventData.OnEvent(OnPartJointBreak)); + GameEvents.onVesselWasModified.Add(new EventData.OnEvent(OnVesselWasModified)); + GameEvents.onVesselDestroy.Add(new EventData.OnEvent(OnVesselDestroy)); + } + + public void OnDestroy() + { + GameEvents.onPartJointBreak.Remove(new EventData.OnEvent(OnPartJointBreak)); + GameEvents.onVesselWasModified.Remove(new EventData.OnEvent(OnVesselWasModified)); + GameEvents.onVesselDestroy.Remove(new EventData.OnEvent(OnVesselDestroy)); + } + public override void OnLoad(ConfigNode node) { base.OnLoad(node); @@ -35,7 +68,22 @@ public override void OnLoad(ConfigNode node) { string key = child.GetValue("key"); Guid id = new Guid(child.GetValue("id")); - vessels[key] = id; + uint hash = ConfigNodeUtil.ParseValue(child, "hash", 0); + + Vessel vessel = FlightGlobals.Vessels.Find(v => v.id == id); + if (vessel == null) + { + id = Guid.Empty; + } + else if (hash == 0 && HighLogic.LoadedScene == GameScenes.FLIGHT) + { + hash = vessel.GetHashes().First(); + } + + if (id != Guid.Empty) + { + vessels[key] = new VesselInfo(id, hash); + } } } @@ -43,12 +91,110 @@ public override void OnSave(ConfigNode node) { base.OnSave(node); - foreach (KeyValuePair p in vessels) + foreach (KeyValuePair p in vessels) { - ConfigNode child = new ConfigNode("VESSEL"); - child.AddValue("key", p.Key); - child.AddValue("id", p.Value); - node.AddNode(child); + VesselInfo vi = p.Value; + + // First find the vessel by id + Vessel vessel = FlightGlobals.Vessels.Find(v => v.id == vi.id); + + if (HighLogic.LoadedScene == GameScenes.FLIGHT) + { + // If not found, attempt to find it by hash + if (vessel == null) + { + vessel = FlightGlobals.Vessels.Find(v => v.GetHashes().Contains(vi.hash)); + } + // If found, verify the hash + else + { + IEnumerable hashes = vessel.GetHashes(); + if (!hashes.Contains(vi.hash)) + { + vi.hash = hashes.FirstOrDefault(); + } + } + + } + + if (vessel != null) + { + ConfigNode child = new ConfigNode("VESSEL"); + child.AddValue("key", p.Key); + child.AddValue("id", vi.id); + child.AddValue("hash", vi.hash); + node.AddNode(child); + } + } + } + + protected virtual void OnPartJointBreak(PartJoint p) + { + if (HighLogic.LoadedScene == GameScenes.EDITOR) + { + return; + } + + if (GetAssociatedKeys(p.Parent.vessel).Any()) + { + lastBreak = p.Parent.vessel; + } + } + + protected virtual void OnVesselWasModified(Vessel vessel) + { + LoggingUtil.LogVerbose(this, "OnVesselWasModified: " + vessel.id); + + // Check for a vessel creation after a part joint break + if (HighLogic.LoadedScene != GameScenes.FLIGHT || lastBreak == null || vessel == lastBreak) + { + return; + } + + IEnumerable hashes = vessel.GetHashes(); + + // OnVesselWasModified gets called twice, on the first call the vessels are still + // connected. Check for that case. + if (hashes.First() == lastBreak.GetHashes().First()) + { + // The second call will be for the original vessel. Swap over to check that one. + lastBreak = vessel; + return; + } + + foreach (string key in GetAssociatedKeys(lastBreak)) + { + // Check if we need to switch over to the newly created vessel + VesselInfo vi = vessels[key]; + if (hashes.Contains(vi.hash)) + { + vi.id = vessel.id; + OnVesselAssociation.Fire(new GameEvents.HostTargetAction(vessel, key)); + } + } + + lastBreak = null; + } + + protected virtual void OnVesselDestroy(Vessel vessel) + { + LoggingUtil.LogVerbose(this, "OnVesselDestroy " + vessel.id); + + // Try to change any associations over if this is due to a docking event + foreach (string key in GetAssociatedKeys(vessel).ToList()) + { + // Check if we need to switch over to the newly created vessel + VesselInfo vi = vessels[key]; + Vessel newVessel = FlightGlobals.Vessels.Find(v => v != vessel && v.GetHashes().Contains(vi.hash)); + if (newVessel != null) + { + Debug.Log("switch association over to " + newVessel.id); + vi.id = newVessel.id; + } + else + { + AssociateVessel(key, null); + } } } @@ -71,7 +217,7 @@ public void AssociateVessel(string key, Vessel vessel) // First remove whatever was there if (vessels.ContainsKey(key)) { - Guid oldVesselId = vessels[key]; + Guid oldVesselId = vessels[key].id; Vessel oldVessel = FlightGlobals.Vessels.Find(v => v.id == oldVesselId); vessels.Remove(key); @@ -85,7 +231,7 @@ public void AssociateVessel(string key, Vessel vessel) // Add the new vessel if (vessel != null) { - vessels[key] = vessel.id; + vessels[key] = new VesselInfo(vessel); LoggingUtil.LogVerbose(this, "Firing OnVesselAssociation."); OnVesselAssociation.Fire(new GameEvents.HostTargetAction(vessel, key)); } @@ -100,7 +246,7 @@ public Vessel GetAssociatedVessel(string key) { if (vessels.ContainsKey(key)) { - return FlightGlobals.Vessels.Find(v => v.id == vessels[key]); + return FlightGlobals.Vessels.Find(v => v.id == vessels[key].id); } return null; } @@ -112,7 +258,7 @@ public Vessel GetAssociatedVessel(string key) /// And enumeration of all keys public IEnumerable GetAssociatedKeys(Vessel v) { - foreach (string key in vessels.Where(p => p.Value == v.id).Select(p => p.Key)) + foreach (string key in vessels.Where(p => p.Value.id == v.id).Select(p => p.Key)) { yield return key; } diff --git a/source/ContractConfigurator/Util/Extensions.cs b/source/ContractConfigurator/Util/Extensions.cs index da76229e1..68807526c 100644 --- a/source/ContractConfigurator/Util/Extensions.cs +++ b/source/ContractConfigurator/Util/Extensions.cs @@ -83,5 +83,127 @@ public static double ResourceQuantity(this Vessel vessel, PartResourceDefinition } return quantity; } + + /// + /// Create a hash of the vessel. + /// + /// The vessel to hash + /// A list of hashes for this vessel + public static IEnumerable GetHashes(this Vessel vessel) + { + if (vessel.rootPart == null) + { + yield break; + } + + Queue queue = new Queue(); + Dictionary visited = new Dictionary(); + Dictionary dockedParts = new Dictionary(); + Queue otherVessel = new Queue(); + + // Add the root + queue.Enqueue(vessel.rootPart); + visited[vessel.rootPart] = 1; + + // Do a BFS of all parts. + uint hash = 0; + while (queue.Count > 0 || otherVessel.Count > 0) + { + bool decoupler = false; + + // Start a new ship + if (queue.Count == 0) + { + // Reset our hash + yield return hash; + hash = 0; + + // Find an unhandled part to use as the new vessel + Part px; + while (px = otherVessel.Dequeue()) + { + if (visited[px] != 2) + { + queue.Enqueue(px); + break; + } + } + dockedParts.Clear(); + continue; + } + + Part p = queue.Dequeue(); + + // Check if this is for a new vessel + if (dockedParts.ContainsKey(p.flightID)) + { + otherVessel.Enqueue(p); + continue; + } + + // Special handling of certain modules + for (int i = 0; i < p.Modules.Count; i++) + { + PartModule pm = p.Modules.GetModule(i); + + // If this is a docking node, track the docked part + if (pm.moduleName == "ModuleDockingNode") + { + ModuleDockingNode dock = (ModuleDockingNode)pm; + if (dock.dockedPartUId != 0) + { + dockedParts[dock.dockedPartUId] = dock.dockedPartUId; + } + } + else if (pm.moduleName == "ModuleDecouple") + { + // Just assume all parts can decouple from this, it's easier and + // effectively the same thing + decoupler = true; + + // Parent may be null if this is the root of the stack + if (p.parent != null) + { + dockedParts[p.parent.flightID] = p.parent.flightID; + } + + // Add all children as possible new vessels + foreach (Part child in p.children) + { + dockedParts[child.flightID] = child.flightID; + } + } + } + + // Go through our child parts + foreach (Part child in p.children) + { + if (!visited.ContainsKey(child)) + { + queue.Enqueue(child); + visited[child] = 1; + } + } + + // Confirm if parent part has been visited + if (p.parent != null && !visited.ContainsKey(p.parent)) + { + queue.Enqueue(p.parent); + visited[p.parent] = 1; + } + + // Add this part to the hash + if (!decoupler) + { + hash ^= p.flightID; + } + + // We've processed this node + visited[p] = 2; + } + + // Return the last hash + yield return hash; + } } } diff --git a/test/DockingTest.cfg b/test/DockingTest.cfg index 34854006d..ed407071e 100644 --- a/test/DockingTest.cfg +++ b/test/DockingTest.cfg @@ -67,7 +67,12 @@ CONTRACT_TYPE maxSimultaneous = 1 targetBody = Kerbin - targetVessel = AllVessels().Where(v => v.VesselType() == Unknown || v.VesselType() == SpaceObject).Random() + + DATA + { + type = Vessel + targetVessel = AllVessels().Where(v => v.VesselType() == Unknown || v.VesselType() == SpaceObject).Random() + } // Contract rewards rewardScience = 100.0 @@ -87,3 +92,68 @@ CONTRACT_TYPE } } } + +CONTRACT_TYPE +{ + // Unique name of the contract (required) + name = VesselTrackingTest + + // Contract text + title = Vessel Tracking Test + description = Stuff + synopsis = We want you to do a thing. + completedMessage = You have done the thing. + maxSimultaneous = 1 + + targetBody = Kerbin + + // Contract rewards + rewardScience = 100.0 + rewardFunds = 100000.0 + + PARAMETER + { + name = VesselParameterGroup + type = VesselParameterGroup + + define = VesselTrackingTestVessel + + PARAMETER + { + name = ReachState + type = ReachState + + situation = PRELAUNCH + } + } + + PARAMETER + { + name = VesselParameterGroup + type = VesselParameterGroup + + PARAMETER + { + name = ReachState + type = ReachState + + situation = ORBITING + } + + PARAMETER + { + name = IsNotVessel + type = IsNotVessel + + vesselKey = VesselTrackingTestVessel + } + + PARAMETER + { + name = ReachState + type = ReachState + + situation = SPLASHED + } + } +}