// // Kerbal Engineer Redux // // Copyright (C) 2014 CYBUTEK // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // #region Using Directives #endregion namespace KerbalEngineer.VesselSimulator { using System; using System.Collections.Generic; using System.Linq; using System.Text; using CompoundParts; using Extensions; using UnityEngine; public class PartSim : Pool { public double baseMass; public Vector3d centerOfMass; public double cost; public int decoupledInStage; public bool fuelCrossFeed; public List fuelTargets = new List(); public bool hasModuleEngines; public bool hasModuleEnginesFX; public bool hasMultiModeEngine; public bool hasVessel; public String initialVesselName; public int inverseStage; public bool isDecoupler; public bool isEngine; public bool isFuelLine; public bool isFuelTank; public bool isLanded; public bool isNoPhysics; public bool isSepratron; public bool localCorrectThrust; public String name; public String noCrossFeedNodeKey; public PartSim parent; public AttachModes parentAttach; public Part part; // This is only set while the data structures are being initialised public int partId = 0; public ResourceContainer resourceDrains = new ResourceContainer(); public ResourceContainer resourceFlowStates = new ResourceContainer(); public ResourceContainer resources = new ResourceContainer(); public double startMass = 0d; public String vesselName; public VesselType vesselType; private readonly List attachNodes = new List(); public ResourceContainer ResourceDrains { get { return resourceDrains; } } public ResourceContainer Resources { get { return resources; } } public void CreateEngineSims(List allEngines, double atmosphere, double mach, bool vectoredThrust, bool fullThrust, LogMsg log) { bool correctThrust = SimManager.DoesEngineUseCorrectedThrust(part); if (log != null) { log.buf.AppendLine("CreateEngineSims for " + name); foreach (PartModule partMod in part.Modules) { log.buf.AppendLine("Module: " + partMod.moduleName); } log.buf.AppendLine("correctThrust = " + correctThrust); } if (hasMultiModeEngine) { // A multi-mode engine has multiple ModuleEnginesFX but only one is active at any point // The mode of the engine is the engineID of the ModuleEnginesFX that is active string mode = part.GetModule().mode; List engines = part.GetModules(); for (int i = 0; i < engines.Count; ++i) { ModuleEnginesFX engine = engines[i]; if (engine.engineID == mode) { if (log != null) { log.buf.AppendLine("Module: " + engine.moduleName); } Vector3 thrustvec = CalculateThrustVector(vectoredThrust ? engine.thrustTransforms : null, log); EngineSim engineSim = EngineSim.GetPoolObject().Initialise(this, atmosphere, (float)mach, engine.maxFuelFlow, engine.minFuelFlow, engine.thrustPercentage, thrustvec, engine.atmosphereCurve, engine.atmChangeFlow, engine.useAtmCurve ? engine.atmCurve : null, engine.useVelCurve ? engine.velCurve : null, engine.currentThrottle, engine.throttleLocked || fullThrust, engine.propellants, engine.isOperational, engine.resultingThrust, engine.thrustTransforms); allEngines.Add(engineSim); } } } else { if (hasModuleEngines) { List engines = part.GetModules(); for (int i = 0; i < engines.Count; ++i) { ModuleEngines engine = engines[i]; if (log != null) { log.buf.AppendLine("Module: " + engine.moduleName); } Vector3 thrustvec = CalculateThrustVector(vectoredThrust ? engine.thrustTransforms : null, log); EngineSim engineSim = EngineSim.GetPoolObject().Initialise(this, atmosphere, (float)mach, engine.maxFuelFlow, engine.minFuelFlow, engine.thrustPercentage, thrustvec, engine.atmosphereCurve, engine.atmChangeFlow, engine.useAtmCurve ? engine.atmCurve : null, engine.useVelCurve ? engine.velCurve : null, engine.currentThrottle, engine.throttleLocked || fullThrust, engine.propellants, engine.isOperational, engine.resultingThrust, engine.thrustTransforms); allEngines.Add(engineSim); } } } if (log != null) { log.Flush(); } } public int DecouplerCount() { int count = 0; PartSim partSim = this; while (partSim != null) { if (partSim.isDecoupler) { count++; } partSim = partSim.parent; } return count; } public void DrainResources(double time) { //MonoBehaviour.print("DrainResources(" + name + ":" + partId + ", " + time + ")"); for (int i = 0; i < resourceDrains.Types.Count; ++i) { int type = resourceDrains.Types[i]; //MonoBehaviour.print("draining " + (time * resourceDrains[type]) + " " + ResourceContainer.GetResourceName(type)); resources.Add(type, -time * resourceDrains[type]); //MonoBehaviour.print(ResourceContainer.GetResourceName(type) + " left = " + resources[type]); } } public String DumpPartAndParentsToBuffer(StringBuilder buffer, String prefix) { if (parent != null) { prefix = parent.DumpPartAndParentsToBuffer(buffer, prefix) + " "; } DumpPartToBuffer(buffer, prefix); return prefix; } public void DumpPartToBuffer(StringBuilder buffer, String prefix, List allParts = null) { buffer.Append(prefix); buffer.Append(name); buffer.AppendFormat(":[id = {0:d}, decouple = {1:d}, invstage = {2:d}", partId, decoupledInStage, inverseStage); buffer.AppendFormat(", vesselName = '{0}'", vesselName); buffer.AppendFormat(", vesselType = {0}", SimManager.GetVesselTypeString(vesselType)); buffer.AppendFormat(", initialVesselName = '{0}'", initialVesselName); buffer.AppendFormat(", fuelCF = {0}", fuelCrossFeed); buffer.AppendFormat(", noCFNKey = '{0}'", noCrossFeedNodeKey); buffer.AppendFormat(", isSep = {0}", isSepratron); foreach (int type in resources.Types) { buffer.AppendFormat(", {0} = {1:g6}", ResourceContainer.GetResourceName(type), resources[type]); } if (attachNodes.Count > 0) { buffer.Append(", attached = <"); attachNodes[0].DumpToBuffer(buffer); for (int i = 1; i < attachNodes.Count; i++) { buffer.Append(", "); attachNodes[i].DumpToBuffer(buffer); } buffer.Append(">"); } // Add more info here buffer.Append("]\n"); if (allParts != null) { String newPrefix = prefix + " "; foreach (PartSim partSim in allParts) { if (partSim.parent == this) { partSim.DumpPartToBuffer(buffer, newPrefix, allParts); } } } } public bool EmptyOf(HashSet types) { foreach (int type in types) { if (resources.HasType(type) && resourceFlowStates[type] != 0 && resources[type] > SimManager.RESOURCE_MIN) { return false; } } return true; } public double GetMass() { double mass = baseMass; for (int i = 0; i < resources.Types.Count; ++i) { mass += resources.GetResourceMass(resources.Types[i]); } return mass; } public HashSet GetSourceSet(int type, List allParts, HashSet visited, LogMsg log, String indent) { if (log != null) { log.buf.AppendLine(indent + "GetSourceSet(" + ResourceContainer.GetResourceName(type) + ") for " + name + ":" + partId); indent += " "; } HashSet allSources = new HashSet(); HashSet partSources = null; // Rule 1: Each part can be only visited once, If it is visited for second time in particular search it returns empty list. if (visited.Contains(this)) { if (log != null) { log.buf.AppendLine(indent + "Returning empty set, already visited (" + name + ":" + partId + ")"); } return allSources; } //if (log != null) // log.buf.AppendLine(indent + "Adding this to visited"); visited.Add(this); // Rule 2: Part performs scan on start of every fuel pipe ending in it. This scan is done in order in which pipes were installed. // Then it makes an union of fuel tank sets each pipe scan returned. If the resulting list is not empty, it is returned as result. //MonoBehaviour.print("foreach fuel line"); for (int i = 0; i < fuelTargets.Count; ++i) { PartSim partSim = fuelTargets[i]; if (visited.Contains(partSim)) { //if (log != null) // log.buf.AppendLine(indent + "Fuel target already visited, skipping (" + partSim.name + ":" + partSim.partId + ")"); } else { //if (log != null) // log.buf.AppendLine(indent + "Adding fuel target as source (" + partSim.name + ":" + partSim.partId + ")"); partSources = partSim.GetSourceSet(type, allParts, visited, log, indent); if (partSources.Count > 0) { allSources.UnionWith(partSources); partSources.Clear(); } } } if (allSources.Count > 0) { if (log != null) { log.buf.AppendLine(indent + "Returning " + allSources.Count + " fuel target sources (" + name + ":" + partId + ")"); } return allSources; } // Rule 3: This rule has been removed and merged with rules 4 and 7 to fix issue with fuel tanks with disabled crossfeed // Rule 4: Part performs scan on each of its axially mounted neighbors. // Couplers (bicoupler, tricoupler, ...) are an exception, they only scan one attach point on the single attachment side, // skip the points on the side where multiple points are. [Experiment] // Again, the part creates union of scan lists from each of its neighbor and if it is not empty, returns this list. // The order in which mount points of a part are scanned appears to be fixed and defined by the part specification file. [Experiment] if (fuelCrossFeed) { //MonoBehaviour.print("foreach attach node"); for (int i = 0; i < attachNodes.Count; ++i) { AttachNodeSim attachSim = attachNodes[i]; if (attachSim.attachedPartSim != null) { if (attachSim.nodeType == AttachNode.NodeType.Stack) { if (!(noCrossFeedNodeKey != null && noCrossFeedNodeKey.Length > 0 && attachSim.id.Contains(noCrossFeedNodeKey))) { if (visited.Contains(attachSim.attachedPartSim)) { //if (log != null) // log.buf.AppendLine(indent + "Attached part already visited, skipping (" + attachSim.attachedPartSim.name + ":" + attachSim.attachedPartSim.partId + ")"); } else { //if (log != null) // log.buf.AppendLine(indent + "Adding attached part as source (" + attachSim.attachedPartSim.name + ":" + attachSim.attachedPartSim.partId + ")"); partSources = attachSim.attachedPartSim.GetSourceSet(type, allParts, visited, log, indent); if (partSources.Count > 0) { allSources.UnionWith(partSources); partSources.Clear(); } } } } } } if (allSources.Count > 0) { if (log != null) { log.buf.AppendLine(indent + "Returning " + allSources.Count + " attached sources (" + name + ":" + partId + ")"); } return allSources; } } // Rule 5: If the part is fuel container for searched type of fuel (i.e. it has capability to contain that type of fuel and the fuel // type was not disabled [Experiment]) and it contains fuel, it returns itself. // Rule 6: If the part is fuel container for searched type of fuel (i.e. it has capability to contain that type of fuel and the fuel // type was not disabled) but it does not contain the requested fuel, it returns empty list. [Experiment] if (resources.HasType(type) && resourceFlowStates[type] != 0) { if (resources[type] > SimManager.RESOURCE_MIN) { allSources.Add(this); if (log != null) { log.buf.AppendLine(indent + "Returning enabled tank as only source (" + name + ":" + partId + ")"); } } return allSources; } // Rule 7: If the part is radially attached to another part and it is child of that part in the ship's tree structure, it scans its // parent and returns whatever the parent scan returned. [Experiment] [Experiment] if (parent != null && parentAttach == AttachModes.SRF_ATTACH) { if (fuelCrossFeed) { if (visited.Contains(parent)) { //if (log != null) // log.buf.AppendLine(indent + "Parent part already visited, skipping (" + parent.name + ":" + parent.partId + ")"); } else { allSources = parent.GetSourceSet(type, allParts, visited, log, indent); if (allSources.Count > 0) { if (log != null) { log.buf.AppendLine(indent + "Returning " + allSources.Count + " parent sources (" + name + ":" + partId + ")"); } return allSources; } } } } // Rule 8: If all preceding rules failed, part returns empty list. //if (log != null) // log.buf.AppendLine(indent + "Returning empty set, no sources found (" + name + ":" + partId + ")"); return allSources; } public double GetStartMass() { return startMass; } public PartSim Initialise(Part thePart, int id, double atmosphere, LogMsg log) { Reset(this); part = thePart; centerOfMass = thePart.transform.TransformPoint(thePart.CoMOffset); partId = id; name = part.partInfo.name; if (log != null) { log.buf.AppendLine("Create PartSim for " + name); } parent = null; parentAttach = part.attachMode; fuelCrossFeed = part.fuelCrossFeed; noCrossFeedNodeKey = part.NoCrossFeedNodeKey; decoupledInStage = DecoupledInStage(part); isFuelLine = part.HasModule(); isFuelTank = part is FuelTank; isSepratron = IsSepratron(); inverseStage = part.inverseStage; //MonoBehaviour.print("inverseStage = " + inverseStage); cost = part.GetCostWet(); // Work out if the part should have no physical significance isNoPhysics = part.HasModule(); if (isNoPhysics == false) { if (part.vessel != null) { baseMass = part.mass; } else { baseMass = part.mass + part.GetModuleMass(part.mass); } } if (SimManager.logOutput) { MonoBehaviour.print((isNoPhysics ? "Ignoring" : "Using") + " part.mass of " + part.mass); } for (int i = 0; i < part.Resources.Count; ++i) { PartResource resource = part.Resources[i]; // Make sure it isn't NaN as this messes up the part mass and hence most of the values // This can happen if a resource capacity is 0 and tweakable if (!Double.IsNaN(resource.amount)) { if (SimManager.logOutput) { MonoBehaviour.print(resource.resourceName + " = " + resource.amount); } resources.Add(resource.info.id, resource.amount); resourceFlowStates.Add(resource.info.id, resource.flowState ? 1 : 0); } else { MonoBehaviour.print(resource.resourceName + " is NaN. Skipping."); } } startMass = GetMass(); hasVessel = (part.vessel != null); isLanded = hasVessel && part.vessel.Landed; if (hasVessel) { vesselName = part.vessel.vesselName; vesselType = part.vesselType; } initialVesselName = part.initialVesselName; hasMultiModeEngine = part.HasModule(); hasModuleEnginesFX = part.HasModule(); hasModuleEngines = part.HasModule(); isEngine = hasMultiModeEngine || hasModuleEnginesFX || hasModuleEngines; if (SimManager.logOutput) { MonoBehaviour.print("Created " + name + ". Decoupled in stage " + decoupledInStage); } return this; } public void ReleasePart() { part = null; } public void RemoveAttachedParts(HashSet partSims) { // Loop through the attached parts for (int i = 0; i < attachNodes.Count; ++i) { AttachNodeSim attachSim = attachNodes[i]; // If the part is in the set then "remove" it by clearing the PartSim reference if (partSims.Contains(attachSim.attachedPartSim)) { attachSim.attachedPartSim = null; } } } public void SetupAttachNodes(Dictionary partSimLookup, LogMsg log) { if (log != null) { log.buf.AppendLine("SetupAttachNodes for " + name + ":" + partId + ""); } attachNodes.Clear(); for (int i = 0; i < part.attachNodes.Count; ++i) { AttachNode attachNode = part.attachNodes[i]; if (log != null) { log.buf.AppendLine("AttachNode " + attachNode.id + " = " + (attachNode.attachedPart != null ? attachNode.attachedPart.partInfo.name : "null")); } if (attachNode.attachedPart != null && attachNode.id != "Strut") { PartSim attachedSim; if (partSimLookup.TryGetValue(attachNode.attachedPart, out attachedSim)) { if (log != null) { log.buf.AppendLine("Adding attached node " + attachedSim.name + ":" + attachedSim.partId + ""); } attachNodes.Add(AttachNodeSim.GetPoolObject().Initialise(attachedSim, attachNode.id, attachNode.nodeType)); } else { if (log != null) { log.buf.AppendLine("No PartSim for attached part (" + attachNode.attachedPart.partInfo.name + ")"); } } } } for (int i = 0; i < part.fuelLookupTargets.Count; ++i) { Part p = part.fuelLookupTargets[i]; if (p != null) { PartSim targetSim; if (partSimLookup.TryGetValue(p, out targetSim)) { if (log != null) { log.buf.AppendLine("Fuel target: " + targetSim.name + ":" + targetSim.partId); } fuelTargets.Add(targetSim); } else { if (log != null) { log.buf.AppendLine("No PartSim for fuel target (" + p.name + ")"); } } } } } public void SetupParent(Dictionary partSimLookup, LogMsg log) { if (part.parent != null) { parent = null; if (partSimLookup.TryGetValue(part.parent, out parent)) { if (log != null) { log.buf.AppendLine("Parent part is " + parent.name + ":" + parent.partId); } } else { if (log != null) { log.buf.AppendLine("No PartSim for parent part (" + part.parent.partInfo.name + ")"); } } } } public double TimeToDrainResource() { //MonoBehaviour.print("TimeToDrainResource(" + name + ":" + partId + ")"); double time = double.MaxValue; for (int i = 0; i < resourceDrains.Types.Count; ++i) { int type = resourceDrains.Types[i]; if (resourceDrains[type] > 0) { time = Math.Min(time, resources[type] / resourceDrains[type]); //MonoBehaviour.print("type = " + ResourceContainer.GetResourceName(type) + " amount = " + resources[type] + " rate = " + resourceDrains[type] + " time = " + time); } } //if (time < double.MaxValue) // MonoBehaviour.print("TimeToDrainResource(" + name + ":" + partId + ") = " + time); return time; } private static void Reset(PartSim partSim) { partSim.attachNodes.Clear(); partSim.fuelTargets.Clear(); partSim.resourceDrains.Reset(); partSim.resourceFlowStates.Reset(); partSim.resources.Reset(); partSim.baseMass = 0.0; partSim.startMass = 0.0; partSim.centerOfMass = Vector3d.zero; partSim.cost = 0.0; partSim.decoupledInStage = 0; partSim.fuelCrossFeed = false; partSim.hasModuleEngines = false; partSim.hasModuleEnginesFX = false; partSim.hasMultiModeEngine = false; partSim.hasVessel = false; partSim.initialVesselName = null; partSim.inverseStage = 0; partSim.isDecoupler = false; partSim.isEngine = false; partSim.isFuelLine = false; partSim.isFuelTank = false; partSim.isLanded = false; partSim.isNoPhysics = false; partSim.isSepratron = false; partSim.localCorrectThrust = false; partSim.name = null; partSim.noCrossFeedNodeKey = null; partSim.parent = null; partSim.parentAttach = AttachModes.SRF_ATTACH; partSim.part = null; partSim.partId = 0; partSim.vesselName = null; partSim.vesselType = VesselType.Base; } private Vector3 CalculateThrustVector(List thrustTransforms, LogMsg log) { if (thrustTransforms == null) { return Vector3.forward; } Vector3 thrustvec = Vector3.zero; for (int i = 0; i < thrustTransforms.Count; ++i) { Transform trans = thrustTransforms[i]; if (log != null) { log.buf.AppendFormat("Transform = ({0:g6}, {1:g6}, {2:g6}) length = {3:g6}\n", trans.forward.x, trans.forward.y, trans.forward.z, trans.forward.magnitude); } thrustvec -= trans.forward; } if (log != null) { log.buf.AppendFormat("ThrustVec = ({0:g6}, {1:g6}, {2:g6}) length = {3:g6}\n", thrustvec.x, thrustvec.y, thrustvec.z, thrustvec.magnitude); } thrustvec.Normalize(); if (log != null) { log.buf.AppendFormat("ThrustVecN = ({0:g6}, {1:g6}, {2:g6}) length = {3:g6}\n", thrustvec.x, thrustvec.y, thrustvec.z, thrustvec.magnitude); } return thrustvec; } private int DecoupledInStage(Part thePart, int stage = -1) { if (IsDecoupler(thePart)) { if (thePart.inverseStage > stage) { stage = thePart.inverseStage; } } if (thePart.parent != null) { stage = DecoupledInStage(thePart.parent, stage); } return stage; } private bool IsActiveDecoupler(Part thePart) { return thePart.FindModulesImplementing().Any(mod => !mod.isDecoupled) || thePart.FindModulesImplementing().Any(mod => !mod.isDecoupled); } private bool IsDecoupler(Part thePart) { return thePart.HasModule() || thePart.HasModule(); } private bool IsSepratron() { if (!part.ActivatesEvenIfDisconnected) { return false; } if (part is SolidRocket) { return true; } IEnumerable modList = part.Modules.OfType(); if (modList.Count() == 0) { return false; } if (modList.First().throttleLocked) { return true; } return false; } } }