// This is the old recursive function for STACK_PRIORITY_SEARCH public void GetSourceSet_Old(int type, bool includeSurfaceMountedParts, List <PartSim> allParts, HashSet <PartSim> visited, HashSet <PartSim> allSources, LogMsg log, String indent) { if (log != null) { log.Append(indent, "GetSourceSet_Old(", ResourceContainer.GetResourceName(type), ") for ") .AppendLine(name, ":", partId); indent += " "; } // Rule 1: Each part can be only visited once, If it is visited for second time in particular search it returns as is. if (visited.Contains(this)) { if (log != null) { log.Append(indent, "Returning empty set, already visited (", name, ":") .AppendLine(partId + ")"); } return; } if (log != null) { log.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("for each fuel line"); int lastCount = allSources.Count; for (int i = 0; i < this.fuelTargets.Count; i++) { PartSim partSim = this.fuelTargets[i]; if (partSim != null) { if (visited.Contains(partSim)) { if (log != null) { log.Append(indent, "Fuel target already visited, skipping (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } } else { if (log != null) { log.Append(indent, "Adding fuel target as source (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } partSim.GetSourceSet_Old(type, includeSurfaceMountedParts, allParts, visited, allSources, log, indent); } } } // check surface mounted fuel targets if (includeSurfaceMountedParts) { for (int i = 0; i < surfaceMountFuelTargets.Count; i++) { PartSim partSim = this.surfaceMountFuelTargets[i]; if (partSim != null) { if (visited.Contains(partSim)) { if (log != null) { log.Append(indent, "Fuel target already visited, skipping (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } } else { if (log != null) { log.Append(indent, "Adding fuel target as source (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } partSim.GetSourceSet_Old(type, true, allParts, visited, allSources, log, indent); } } } } if (allSources.Count > lastCount) { if (log != null) { log.Append(indent, "Returning ", (allSources.Count - lastCount), " fuel target sources (") .AppendLine(this.name, ":", this.partId, ")"); } return; } // 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) { lastCount = allSources.Count; //MonoBehaviour.print("for each attach node"); for (int i = 0; i < this.attachNodes.Count; i++) { AttachNodeSim attachSim = this.attachNodes[i]; if (attachSim.attachedPartSim != null) { if (attachSim.nodeType == AttachNode.NodeType.Stack) { if ((string.IsNullOrEmpty(noCrossFeedNodeKey) == false && attachSim.id.Contains(noCrossFeedNodeKey)) == false) { if (visited.Contains(attachSim.attachedPartSim)) { if (log != null) { log.Append(indent, "Attached part already visited, skipping (", attachSim.attachedPartSim.name, ":") .AppendLine(attachSim.attachedPartSim.partId, ")"); } } else { if (log != null) { log.Append(indent, "Adding attached part as source (", attachSim.attachedPartSim.name, ":") .AppendLine(attachSim.attachedPartSim.partId, ")"); } attachSim.attachedPartSim.GetSourceSet_Old(type, includeSurfaceMountedParts, allParts, visited, allSources, log, indent); } } } } } if (allSources.Count > lastCount) { if (log != null) { log.Append(indent, "Returning " + (allSources.Count - lastCount) + " attached sources (") .AppendLine(this.name, ":", this.partId, ")"); } return; } } // 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.0) { if (resources[type] > SimManager.RESOURCE_MIN) { allSources.Add(this); if (log != null) { log.Append(indent, "Returning enabled tank as only source (", name, ":") .AppendLine(partId, ")"); } return; } } else { if (log != null) { log.Append(indent, "Not fuel tank or disabled. HasType = ", resources.HasType(type)) .AppendLine(" FlowState = " + resourceFlowStates[type]); } } // 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.Append(indent, "Parent part already visited, skipping (", parent.name, ":") .AppendLine(parent.partId, ")"); } } else { lastCount = allSources.Count; this.parent.GetSourceSet_Old(type, includeSurfaceMountedParts, allParts, visited, allSources, log, indent); if (allSources.Count > lastCount) { if (log != null) { log.Append(indent, "Returning ", (allSources.Count - lastCount), " parent sources (") .AppendLine(this.name, ":", this.partId, ")"); } return; } } } } // Rule 8: If all preceding rules failed, part returns empty list. if (log != null) { log.Append(indent, "Returning empty set, no sources found (", name, ":") .AppendLine(partId, ")"); } return; }
public void GetSourceSet_Internal(int type, bool includeSurfaceMountedParts, List <PartSim> allParts, HashSet <PartSim> visited, HashSet <PartSim> allSources, ref int priMax, LogMsg log, String indent) { if (log != null) { log.Append(indent, "GetSourceSet_Internal(", ResourceContainer.GetResourceName(type), ") for ") .AppendLine(name, ":", partId); indent += " "; } // Rule 1: Each part can be only visited once, If it is visited for second time in particular search it returns as is. if (visited.Contains(this)) { if (log != null) { log.Append(indent, "Nothing added, already visited (", name, ":") .AppendLine(partId + ")"); } return; } if (log != null) { log.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("for each fuel line"); int lastCount = allSources.Count; for (int i = 0; i < this.fuelTargets.Count; i++) { PartSim partSim = this.fuelTargets[i]; if (partSim != null) { if (visited.Contains(partSim)) { if (log != null) { log.Append(indent, "Fuel target already visited, skipping (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } } else { if (log != null) { log.Append(indent, "Adding fuel target as source (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } partSim.GetSourceSet_Internal(type, includeSurfaceMountedParts, allParts, visited, allSources, ref priMax, log, indent); } } } if (fuelCrossFeed) { if (includeSurfaceMountedParts) { // check surface mounted fuel targets for (int i = 0; i < surfaceMountFuelTargets.Count; i++) { PartSim partSim = this.surfaceMountFuelTargets[i]; if (partSim != null) { if (visited.Contains(partSim)) { if (log != null) { log.Append(indent, "Surface part already visited, skipping (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } } else { if (log != null) { log.Append(indent, "Adding surface part as source (", partSim.name, ":") .AppendLine(partSim.partId, ")"); } partSim.GetSourceSet_Internal(type, includeSurfaceMountedParts, allParts, visited, allSources, ref priMax, log, indent); } } } } lastCount = allSources.Count; //MonoBehaviour.print("for each attach node"); for (int i = 0; i < this.attachNodes.Count; i++) { AttachNodeSim attachSim = this.attachNodes[i]; if (attachSim.attachedPartSim != null) { if (attachSim.nodeType == AttachNode.NodeType.Stack) { if ((string.IsNullOrEmpty(noCrossFeedNodeKey) == false && attachSim.id.Contains(noCrossFeedNodeKey)) == false) { if (visited.Contains(attachSim.attachedPartSim)) { if (log != null) { log.Append(indent, "Attached part already visited, skipping (", attachSim.attachedPartSim.name, ":") .AppendLine(attachSim.attachedPartSim.partId, ")"); } } else { if (log != null) { log.Append(indent, "Adding attached part as source (", attachSim.attachedPartSim.name, ":") .AppendLine(attachSim.attachedPartSim.partId, ")"); } attachSim.attachedPartSim.GetSourceSet_Internal(type, includeSurfaceMountedParts, allParts, visited, allSources, ref priMax, log, indent); } } } } } } // 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) and it contains fuel, it adds itself. if (resources.HasType(type) && resourceFlowStates[type] > 0.0) { if (resources[type] > resRequestRemainingThreshold) { // Get the priority of this tank int pri = GetResourcePriority(); if (pri > priMax) { // This tank is higher priority than the previously added ones so we clear the sources // and set the priMax to this priority allSources.Clear(); priMax = pri; } // If this is the correct priority then add this to the sources if (pri == priMax) { if (log != null) { log.Append(indent, "Adding enabled tank as source (", name, ":") .AppendLine(partId, ")"); } allSources.Add(this); } } } else { if (log != null) { log.Append(indent, "Not fuel tank or disabled. HasType = ", resources.HasType(type)) .AppendLine(" FlowState = " + resourceFlowStates[type]); } } }
public static EngineSim New(PartSim theEngine, ModuleEngines engineMod, double atmosphere, float machNumber, bool vectoredThrust, bool fullThrust, LogMsg log, float atmosphereDepth, CelestialBody body) { float maxFuelFlow = engineMod.maxFuelFlow; float minFuelFlow = engineMod.minFuelFlow; float thrustPercentage = engineMod.thrustPercentage; List <Transform> thrustTransforms = engineMod.thrustTransforms; List <float> thrustTransformMultipliers = engineMod.thrustTransformMultipliers; Vector3 vecThrust = CalculateThrustVector(vectoredThrust ? thrustTransforms : null, vectoredThrust ? thrustTransformMultipliers : null, log); FloatCurve atmosphereCurve = engineMod.atmosphereCurve; bool atmChangeFlow = engineMod.atmChangeFlow; FloatCurve atmCurve = engineMod.useAtmCurve ? engineMod.atmCurve : null; FloatCurve velCurve = engineMod.useVelCurve ? engineMod.velCurve : null; float currentThrottle = engineMod.currentThrottle; float IspG = engineMod.g; bool throttleLocked = engineMod.throttleLocked || fullThrust; List <Propellant> propellants = engineMod.propellants; bool active = engineMod.isOperational; float resultingThrust = engineMod.resultingThrust; bool isFlamedOut = engineMod.flameout; EngineSim engineSim = pool.Borrow(); engineSim.isp = 0.0; engineSim.vacuumIsp = 0.0; engineSim.maxMach = 0.0f; engineSim.actualThrust = 0.0; engineSim.partSim = theEngine; engineSim.isActive = active; engineSim.thrustVec = vecThrust; engineSim.isFlamedOut = isFlamedOut; engineSim.resourceConsumptions.Reset(); engineSim.resourceFlowModes.Reset(); engineSim.appliedForces.Clear(); double flowRate = 0.0; if (engineSim.partSim.hasVessel) { if (log != null) { log.AppendLine("hasVessel is true"); } float flowModifier = GetFlowModifier(atmChangeFlow, atmCurve, engineSim.partSim.part.atmDensity, velCurve, machNumber, ref engineSim.maxMach); float vacuumFlowModifier = GetFlowModifier(atmChangeFlow, atmCurve, 0, velCurve, machNumber, ref engineSim.maxMach); engineSim.isp = atmosphereCurve.Evaluate((float)atmosphere); engineSim.vacuumIsp = atmosphereCurve.Evaluate(0); engineSim.thrust = GetThrust(Mathf.Lerp(minFuelFlow, maxFuelFlow, GetThrustPercent(thrustPercentage)) * flowModifier, engineSim.isp); engineSim.vacuumThrust = GetThrust(Mathf.Lerp(minFuelFlow, maxFuelFlow, GetThrustPercent(thrustPercentage)) * vacuumFlowModifier, engineSim.vacuumIsp); engineSim.actualThrust = engineSim.isActive ? resultingThrust : 0.0; if (log != null) { log.buf.AppendFormat("flowMod = {0:g6}\n", flowModifier); log.buf.AppendFormat("isp = {0:g6}\n", engineSim.isp); log.buf.AppendFormat("thrust = {0:g6}\n", engineSim.thrust); log.buf.AppendFormat("actual = {0:g6}\n", engineSim.actualThrust); } if (throttleLocked) { if (log != null) { log.AppendLine("throttleLocked is true, using thrust for flowRate"); } flowRate = GetFlowRate(engineSim.thrust, engineSim.isp); } else { if (currentThrottle > 0.0f && engineSim.partSim.isLanded == false) { // TODO: This bit doesn't work for RF engines if (log != null) { log.AppendLine("throttled up and not landed, using actualThrust for flowRate"); } flowRate = GetFlowRate(engineSim.actualThrust, engineSim.isp); } else { if (log != null) { log.AppendLine("throttled down or landed, using thrust for flowRate"); } flowRate = GetFlowRate(engineSim.thrust, engineSim.isp); } } } else { if (log != null) { log.buf.AppendLine("hasVessel is false"); } float altitude = atmosphereDepth; float flowModifier = GetFlowModifier(atmChangeFlow, atmCurve, body.GetDensity(body.GetPressure(altitude), body.GetTemperature(altitude)), velCurve, machNumber, ref engineSim.maxMach); float vacuumFlowModifier = GetFlowModifier(atmChangeFlow, atmCurve, 0, velCurve, machNumber, ref engineSim.maxMach); engineSim.isp = atmosphereCurve.Evaluate((float)atmosphere); engineSim.vacuumIsp = atmosphereCurve.Evaluate(0); engineSim.thrust = GetThrust(Mathf.Lerp(minFuelFlow, maxFuelFlow, GetThrustPercent(thrustPercentage)) * flowModifier, engineSim.isp); engineSim.vacuumThrust = GetThrust(Mathf.Lerp(minFuelFlow, maxFuelFlow, GetThrustPercent(thrustPercentage)) * vacuumFlowModifier, engineSim.vacuumIsp); engineSim.actualThrust = 0d; if (log != null) { log.buf.AppendFormat("flowMod = {0:g6}\n", flowModifier); log.buf.AppendFormat("isp = {0:g6}\n", engineSim.isp); log.buf.AppendFormat("thrust = {0:g6}\n", engineSim.thrust); log.buf.AppendFormat("actual = {0:g6}\n", engineSim.actualThrust); log.AppendLine("no vessel, using thrust for flowRate"); } flowRate = GetFlowRate(engineSim.thrust, engineSim.isp); } if (log != null) { log.buf.AppendFormat("flowRate = {0:g6}\n", flowRate); } float flowMass = 0f; for (int i = 0; i < propellants.Count; ++i) { Propellant propellant = propellants[i]; if (!propellant.ignoreForIsp) { flowMass += propellant.ratio * ResourceContainer.GetResourceDensity(propellant.id); } } if (log != null) { log.buf.AppendFormat("flowMass = {0:g6}\n", flowMass); } for (int i = 0; i < propellants.Count; ++i) { Propellant propellant = propellants[i]; if (propellant.name == "ElectricCharge" || propellant.name == "IntakeAir") { continue; } double consumptionRate = propellant.ratio * flowRate / flowMass; if (log != null) { log.buf.AppendFormat( "Add consumption({0}, {1}:{2:d}) = {3:g6}\n", ResourceContainer.GetResourceName(propellant.id), theEngine.name, theEngine.partId, consumptionRate); } engineSim.resourceConsumptions.Add(propellant.id, consumptionRate); engineSim.resourceFlowModes.Add(propellant.id, (double)propellant.GetFlowMode()); } for (int i = 0; i < thrustTransforms.Count; i++) { Transform thrustTransform = thrustTransforms[i]; Vector3d direction = thrustTransform.forward.normalized; Vector3d position = thrustTransform.position; AppliedForce appliedForce = AppliedForce.New(direction * engineSim.thrust * thrustTransformMultipliers[i], position); engineSim.appliedForces.Add(appliedForce); } return(engineSim); }
public void DumpPartToLog(LogMsg log, String prefix, List <PartSim> allParts = null) { if (log == null) { return; } log.Append(prefix); log.Append(name); log.Append(":[id = ", partId, ", decouple = ", decoupledInStage); log.Append(", invstage = ", inverseStage); //log.Append(", vesselName = '", vesselName, "'"); //log.Append(", vesselType = ", SimManager.GetVesselTypeString(vesselType)); //log.Append(", initialVesselName = '", initialVesselName, "'"); log.Append(", isNoPhys = ", isNoPhysics); log.buf.AppendFormat(", baseMass = {0}", baseMass); log.buf.AppendFormat(", baseMassForCoM = {0}", baseMassForCoM); log.Append(", fuelCF = {0}", fuelCrossFeed); log.Append(", noCFNKey = '{0}'", noCrossFeedNodeKey); log.Append(", isSep = {0}", isSepratron); for (int i = 0; i < resources.Types.Count; i++) { int type = resources.Types[i]; log.buf.AppendFormat(", {0} = {1:g6}", ResourceContainer.GetResourceName(type), resources[type]); } if (attachNodes.Count > 0) { log.Append(", attached = <"); attachNodes[0].DumpToLog(log); for (int i = 1; i < attachNodes.Count; i++) { log.Append(", "); attachNodes[i].DumpToLog(log); } log.Append(">"); } if (surfaceMountFuelTargets.Count > 0) { log.Append(", surface = <"); log.Append(surfaceMountFuelTargets[0].name, ":", surfaceMountFuelTargets[0].partId); for (int i = 1; i < surfaceMountFuelTargets.Count; i++) { log.Append(", ", surfaceMountFuelTargets[i].name, ":", surfaceMountFuelTargets[i].partId); } log.Append(">"); } // Add more info here log.AppendLine("]"); if (allParts != null) { String newPrefix = prefix + " "; for (int i = 0; i < allParts.Count; i++) { PartSim partSim = allParts[i]; if (partSim.parent == this) { partSim.DumpPartToLog(log, newPrefix, allParts); } } } }
public bool SetResourceDrains(LogMsg log, List <PartSim> allParts, List <PartSim> allFuelLines, HashSet <PartSim> drainingParts) { //DumpSourcePartSets(log, "before clear"); foreach (HashSet <PartSim> sourcePartSet in sourcePartSets.Values) { sourcePartSet.Clear(); } //DumpSourcePartSets(log, "after clear"); for (int index = 0; index < this.resourceConsumptions.Types.Count; index++) { int type = this.resourceConsumptions.Types[index]; HashSet <PartSim> sourcePartSet; if (!sourcePartSets.TryGetValue(type, out sourcePartSet)) { sourcePartSet = new HashSet <PartSim>(); sourcePartSets.Add(type, sourcePartSet); } switch ((ResourceFlowMode)this.resourceFlowModes[type]) { case ResourceFlowMode.NO_FLOW: if (partSim.resources[type] > SimManager.RESOURCE_MIN && partSim.resourceFlowStates[type] != 0) { sourcePartSet.Add(partSim); } break; case ResourceFlowMode.ALL_VESSEL: case ResourceFlowMode.ALL_VESSEL_BALANCE: for (int i = 0; i < allParts.Count; i++) { PartSim aPartSim = allParts[i]; if (aPartSim.resources[type] > SimManager.RESOURCE_MIN && aPartSim.resourceFlowStates[type] != 0) { sourcePartSet.Add(aPartSim); } } break; case ResourceFlowMode.STAGE_PRIORITY_FLOW: case ResourceFlowMode.STAGE_PRIORITY_FLOW_BALANCE: if (log != null) { log.Append("Find ", ResourceContainer.GetResourceName(type), " sources for ", partSim.name) .AppendLine(":", partSim.partId); } foreach (HashSet <PartSim> stagePartSet in stagePartSets.Values) { stagePartSet.Clear(); } var maxStage = -1; for (int i = 0; i < allParts.Count; i++) { var aPartSim = allParts[i]; //if (log != null) log.Append(aPartSim.name, ":" + aPartSim.partId, " contains ", aPartSim.resources[type]) // .AppendLine((aPartSim.resourceFlowStates[type] == 0) ? " (disabled)" : ""); if (aPartSim.resources[type] <= SimManager.RESOURCE_MIN || aPartSim.resourceFlowStates[type] == 0) { continue; } int stage = aPartSim.inverseStage; if (stage > maxStage) { maxStage = stage; } HashSet <PartSim> tempPartSet; if (!stagePartSets.TryGetValue(stage, out tempPartSet)) { tempPartSet = new HashSet <PartSim>(); stagePartSets.Add(stage, tempPartSet); } tempPartSet.Add(aPartSim); } for (int j = maxStage; j >= -1; j--) { //if (log != null) log.AppendLine("Testing stage ", j); HashSet <PartSim> stagePartSet; if (stagePartSets.TryGetValue(j, out stagePartSet) && stagePartSet.Count > 0) { //if (log != null) log.AppendLine("Not empty"); // We have to copy the contents of the set here rather than copying the set reference or // bad things (tm) happen foreach (PartSim aPartSim in stagePartSet) { sourcePartSet.Add(aPartSim); } break; } } break; case ResourceFlowMode.STACK_PRIORITY_SEARCH: case ResourceFlowMode.STAGE_STACK_FLOW: case ResourceFlowMode.STAGE_STACK_FLOW_BALANCE: visited.Clear(); if (log != null) { log.Append("Find ", ResourceContainer.GetResourceName(type), " sources for ", partSim.name) .AppendLine(":", partSim.partId); } partSim.GetSourceSet(type, true, allParts, visited, sourcePartSet, log, ""); break; default: if (log != null) { log.Append("SetResourceDrains(", partSim.name, ":", partSim.partId) .AppendLine(") Unexpected flow type for ", ResourceContainer.GetResourceName(type), ")"); } break; } if (log != null && sourcePartSet.Count > 0) { log.AppendLine("Source parts for ", ResourceContainer.GetResourceName(type), ":"); foreach (PartSim partSim in sourcePartSet) { log.AppendLine(partSim.name, ":", partSim.partId); } } //DumpSourcePartSets(log, "after " + ResourceContainer.GetResourceName(type)); } // If we don't have sources for all the needed resources then return false without setting up any drains for (int i = 0; i < this.resourceConsumptions.Types.Count; i++) { int type = this.resourceConsumptions.Types[i]; HashSet <PartSim> sourcePartSet; if (!sourcePartSets.TryGetValue(type, out sourcePartSet) || sourcePartSet.Count == 0) { if (log != null) { log.AppendLine("No source of ", ResourceContainer.GetResourceName(type)); } isActive = false; return(false); } } // Now we set the drains on the members of the sets and update the draining parts set for (int i = 0; i < this.resourceConsumptions.Types.Count; i++) { int type = this.resourceConsumptions.Types[i]; HashSet <PartSim> sourcePartSet = sourcePartSets[type]; ResourceFlowMode mode = (ResourceFlowMode)resourceFlowModes[type]; double consumption = resourceConsumptions[type]; double amount = 0d; double total = 0d; if (mode == ResourceFlowMode.ALL_VESSEL_BALANCE || mode == ResourceFlowMode.STAGE_PRIORITY_FLOW_BALANCE || mode == ResourceFlowMode.STAGE_STACK_FLOW_BALANCE || mode == ResourceFlowMode.STACK_PRIORITY_SEARCH) { foreach (PartSim partSim in sourcePartSet) { total += partSim.resources[type]; } } else { amount = consumption / sourcePartSet.Count; } // Loop through the members of the set foreach (PartSim partSim in sourcePartSet) { if (total != 0d) { amount = consumption * partSim.resources[type] / total; } if (log != null) { log.Append("Adding drain of ", amount, " ", ResourceContainer.GetResourceName(type)) .AppendLine(" to ", partSim.name, ":", partSim.partId); } partSim.resourceDrains.Add(type, amount); drainingParts.Add(partSim); } } return(true); }