static double CurvedPanelOutput(Vessel vessel, ProtoPartSnapshot part, Part prefab, Vector3d sun_dir, double sun_dist, double atmo_factor) { // if, for whatever reason, sun_dist is zero (or negative), we do not return any output if (sun_dist <= double.Epsilon) return 0.0; // shortcuts Quaternion rot = part.rotation; // get values from part string transform_name = part.partData.GetValue("PanelTransformName"); float k = Convert.ToSingle(part.partData.GetValue("chargePerTransform")); // get components Transform[] components = prefab.FindModelTransforms(transform_name); if (components.Length == 0) return 0.0; // calculate solar flux double solar_flux = Sim.SolarFlux(sun_dist); // reduce solar flux inside atmosphere solar_flux *= atmo_factor; // normalize against solar flux at home solar_flux /= Sim.SolarFluxAtHome(); solar_flux *= k; // for each one of the components the curved panel is composed of double output = 0.0; foreach(Transform t in prefab.FindModelTransforms(transform_name)) { double cosine_factor = Math.Max(Vector3d.Dot(sun_dir, (vessel.transform.rotation * rot * t.forward).normalized), 0.0); output += cosine_factor * solar_flux; } return output; }
static void ProcessCurvedPanel(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, PartModule curved_panel, Part part_prefab, vessel_info info, resource_info ec, double elapsed_s) { // note: we assume deployed, this is a current limitation // if in sunlight if (info.sunlight > double.Epsilon) { // get values from module string transform_name = Lib.ReflectionValue <string>(curved_panel, "PanelTransformName"); float tot_rate = Lib.ReflectionValue <float>(curved_panel, "TotalEnergyRate"); // get components Transform[] components = part_prefab.FindModelTransforms(transform_name); if (components.Length == 0) { return; } // calculate normalized solar flux // note: this include fractional sunlight if integrated over orbit // note: this include atmospheric absorption if inside an atmosphere double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome(); // calculate rate per component double rate = (double)tot_rate / (double)components.Length; // calculate world-space part rotation quaternion // note: a possible optimization here is to cache the transform lookup (unity was coded by monkeys) Quaternion rot = v.transform.rotation * p.rotation; // calculate output of all components double output = 0.0; foreach (Transform t in components) { output += rate // nominal rate per-component at 1 AU * norm_solar_flux // normalized solar flux at panel distance from sun * Math.Max(Vector3d.Dot(info.sun_dir, (rot * t.forward).normalized), 0.0); // cosine factor of component orientation } // produce EC ec.Produce(output * elapsed_s); } }
static void ProcessPanel(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, ModuleDeployableSolarPanel panel, Vessel_info info, Resource_info ec, double elapsed_s) { // note: we ignore temperature curve, and make sure it is not relevant in the MM patch // note: cylindrical and spherical panels are not supported // note: we assume the tracking target is SUN // if in sunlight and extended if (info.sunlight > double.Epsilon && m.moduleValues.GetValue("deployState") == "EXTENDED") { // get panel normal/pivot direction in world space Transform tr = panel.part.FindModelComponent <Transform>(panel.pivotName); Vector3d dir = panel.isTracking ? tr.up : tr.forward; dir = (v.transform.rotation * p.rotation * dir).normalized; float age = (float)(v.missionTime / (Lib.HoursInDay() * 3600)); float effic_factor = panel.timeEfficCurve != null?panel.timeEfficCurve.Evaluate(age) : 1.0f; // calculate cosine factor // - fixed panel: clamped cosine // - tracking panel, tracking pivot enabled: around the pivot // - tracking panel, tracking pivot disabled: assume perfect alignment double cosine_factor = !panel.isTracking ? Math.Max(Vector3d.Dot(info.sun_dir, dir), 0.0) : Settings.TrackingPivot ? Math.Cos(1.57079632679 - Math.Acos(Vector3d.Dot(info.sun_dir, dir))) : 1.0; // calculate normalized solar flux // - this include fractional sunlight if integrated over orbit // - this include atmospheric absorption if inside an atmosphere double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome(); // calculate output double output = panel.resHandler.outputResources[0].rate // nominal panel charge rate at 1 AU * norm_solar_flux // normalized flux at panel distance from sun * cosine_factor // cosine factor of panel orientation * effic_factor; // produce EC ec.Produce(output * elapsed_s, "panel"); } }
static void ProcessPanel(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, ModuleDeployableSolarPanel panel, vessel_info info, resource_info ec, double elapsed_s) { // note: we ignore temperature curve, and make sure it is not relavant in the MM patch // note: we ignore power curve, that is used by no panel as far as I know // play nice with BackgroundProcessing if (Kerbalism.detected_mods.BackgroundProcessing) { return; } // if in sunlight and extended if (info.sunlight > double.Epsilon && m.moduleValues.GetValue("stateString") == "EXTENDED") { // get panel normal direction Vector3d normal = panel.part.FindModelComponent <Transform>(panel.raycastTransformName).forward; // calculate cosine factor // note: for gameplay reasons, we ignore tracking panel pivots // note: a possible optimization here is to cache the transform lookup (unity was coded by monkeys) double cosine_factor = panel.sunTracking ? 1.0 : Math.Max(Vector3d.Dot(info.sun_dir, (v.transform.rotation * p.rotation * normal).normalized), 0.0); // calculate normalized solar flux // note: this include fractional sunlight if integrated over orbit // note: this include atmospheric absorption if inside an atmosphere double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome(); // calculate output double output = panel.chargeRate // nominal panel charge rate at 1 AU * norm_solar_flux // normalized flux at panel distance from sun * cosine_factor // cosine factor of panel orientation * Reliability.Penalty(p, "Panel"); // malfunctioned panel penalty // produce EC ec.Produce(output * elapsed_s); } }
// return normalized natural lighting at specified distance from the sun (1 in the home world) public static double NaturalLighting(double sun_dist) { // return natural lighting // note: should be 1 at kerbin return Sim.SolarFlux(sun_dist) / Sim.SolarFluxAtHome(); }
public void FixedUpdate() { // do nothing in editor if (Lib.IsEditor()) { return; } // do nothing if there isn't a solar panel if (panel == null) { return; } // get resource handler resource_info ec = ResourceCache.Info(vessel, "ElectricCharge"); // get vessel data from cache vessel_info info = Cache.VesselInfo(vessel); // do nothing if vessel is invalid if (!info.is_valid) { return; } // detect if sunlight is evaluated analytically bool analytical_sunlight = info.sunlight > 0.0 && info.sunlight < 1.0; // detect occlusion from other vessel parts // - we are only interested when the sunlight evaluation is discrete var collider = panel.hit.collider; bool locally_occluded = !analytical_sunlight && collider != null && info.sunlight > 0.0; // if panel is enabled and extended, and if sun is not occluded, not even locally if (panel.isEnabled && panel.deployState == ModuleDeployablePart.DeployState.EXTENDED && info.sunlight > 0.0 && !locally_occluded) { // calculate cosine factor // - the stock module is already computing the tracking direction double cosine_factor = Math.Max(Vector3d.Dot(info.sun_dir, panel.trackingDotTransform.forward), 0.0); // calculate normalized solar flux // - this include fractional sunlight if integrated over orbit // - this include atmospheric absorption if inside an atmosphere double norm_solar_flux = info.solar_flux / Sim.SolarFluxAtHome(); // calculate output double output = rate // nominal panel charge rate at 1 AU * norm_solar_flux // normalized flux at panel distance from sun * cosine_factor; // cosine factor of panel orientation // produce EC ec.Produce(output * Kerbalism.elapsed_s); // update ui field_visibility = info.sunlight * 100.0; field_atmosphere = info.atmo_factor * 100.0; field_exposure = cosine_factor * 100.0; field_output = output; Fields["field_visibility"].guiActive = analytical_sunlight; Fields["field_atmosphere"].guiActive = info.atmo_factor < 1.0; Fields["field_exposure"].guiActive = true; Fields["field_output"].guiActive = true; } // if panel is disabled, retracted, or in shadow else { // hide ui Fields["field_visibility"].guiActive = false; Fields["field_atmosphere"].guiActive = false; Fields["field_exposure"].guiActive = false; Fields["field_output"].guiActive = false; } // update status ui field_status = analytical_sunlight ? "<color=#ffff22>Integrated over the orbit</color>" : locally_occluded ? "<color=#ff2222>Occluded by vessel</color>" : info.sunlight < 1.0 ? "<color=#ff2222>Occluded by celestial body</color>" : string.Empty; Fields["field_status"].guiActive = field_status.Length > 0; }
// implement greenhouse mechanics for unloaded vessels public static void BackgroundUpdate(Vessel vessel, ProtoPartSnapshot p, ProtoPartModuleSnapshot m, Greenhouse greenhouse, vessel_info info, vessel_resources resources, double elapsed_s) { // get protomodule data bool door_opened = Lib.Proto.GetBool(m, "door_opened"); double growth = Lib.Proto.GetDouble(m, "growth"); float lamps = Lib.Proto.GetFloat(m, "lamps"); double lighting = Lib.Proto.GetDouble(m, "lighting"); // if lamp is on if (lamps > float.Epsilon) { // get resource handler resource_info ec = resources.Info(vessel, "ElectricCharge"); // consume ec ec.Consume(greenhouse.ec_rate * lamps * elapsed_s * Reliability.Penalty(p, "Greenhouse", 2.0)); // shut down the light if there isn't enough ec // note: comparing against amount at previous simulation step if (ec.amount <= double.Epsilon) lamps = 0.0f; } // determine lighting conditions // note: we ignore sun direction for gameplay reasons: else the user must reorient the greenhouse as the planets dance over time // - natural light depend on: distance from sun, direct sunlight, door status // - artificial light depend on: lamps tweakable and ec available, door status lighting = info.solar_flux / Sim.SolarFluxAtHome() * (door_opened ? 1.0 : 0.0) + lamps; // if can use waste, and there is some lighting double waste_perc = 0.0; if (greenhouse.waste_name.Length > 0 && lighting > double.Epsilon) { // get resource handler resource_info waste = resources.Info(vessel, greenhouse.waste_name); // consume waste waste.Consume(greenhouse.waste_rate * elapsed_s); // determine waste bonus // note: comparing against amount from previous simulation step waste_perc = Math.Min(waste.amount / greenhouse.waste_rate, 1.0); } // determine growth bonus double growth_bonus = greenhouse.soil_bonus * (info.landed ? 1.0 : 0.0) + greenhouse.waste_bonus * waste_perc; // grow the crop double growing = greenhouse.growth_rate * (1.0 + growth_bonus) * lighting; growth += elapsed_s * growing; // if it is harvest time if (growth >= 1.0) { // reset growth growth = 0.0; // produce food resources.Produce(vessel, greenhouse.resource_name, greenhouse.harvest_size); // show a message to the user Message.Post(Lib.BuildString("On <color=FFFFFF>", vessel.vesselName, "</color> the crop harvest produced <color=FFFFFF>", greenhouse.harvest_size.ToString("F0"), " ", greenhouse.resource_name, "</color>")); // record first space harvest if (!info.landed && DB.Ready()) DB.Landmarks().space_harvest = 1; } // store data Lib.Proto.Set(m, "growth", growth); Lib.Proto.Set(m, "lamps", lamps); Lib.Proto.Set(m, "lighting", lighting); Lib.Proto.Set(m, "growth_diff", growing); }
// implement greenhouse mechanics public void FixedUpdate() { // set emissive intensity from lamp tweakable if (emissive_object.Length > 0) { foreach(var rdr in part.GetComponentsInChildren<UnityEngine.Renderer>()) { if (rdr.name == emissive_object) { rdr.material.SetColor("_EmissiveColor", new Color(lamps, lamps, lamps, 1.0f)); break; } } } // do nothing else in the editor if (HighLogic.LoadedSceneIsEditor) return; // get vessel info from the cache vessel_info vi = Cache.VesselInfo(vessel); // do nothing if vessel is invalid if (!vi.is_valid) return; // get resource cache vessel_resources resources = ResourceCache.Get(vessel); // get elapsed time double elapsed_s = Kerbalism.elapsed_s * vi.time_dilation; // when the greenhouse is assembled using KIS, the growth field is set to NaN // at that point it remain NaN forever, so we fix it // also, a report indicated -infinity in growth if (double.IsNaN(growth) || double.IsInfinity(growth)) growth = 0.0; // if lamp is on if (lamps > float.Epsilon) { // get resource handler resource_info ec = resources.Info(vessel, "ElectricCharge"); // consume ec ec.Consume(ec_rate * lamps * elapsed_s); // shut down the light if there isn't enough ec // note: comparing against amount at previous simulation step if (ec.amount <= double.Epsilon) lamps = 0.0f; } // determine lighting conditions // note: we ignore sun direction for gameplay reasons: else the user must reorient the greenhouse as the planets dance over time lighting = vi.solar_flux / Sim.SolarFluxAtHome() * (door_opened ? 1.0 : 0.0) + lamps; // if can use waste, and there is some lighting double waste_perc = 0.0; if (waste_name.Length > 0 && waste_rate > double.Epsilon && lighting > double.Epsilon) { // get resource handler resource_info waste = resources.Info(vessel, waste_name); // consume waste waste.Consume(waste_rate * elapsed_s); // determine waste bonus // note: comparing against amount from previous simulation step waste_perc = Math.Min(waste.amount / waste_rate, 1.0); } // determine growth bonus double growth_bonus = soil_bonus * (vi.landed ? 1.0 : 0.0) + waste_bonus * waste_perc; // grow the crop growing = growth_rate * (1.0 + growth_bonus) * lighting; growth += elapsed_s * growing; // if it is harvest time if (growth >= 1.0) { // reset growth growth = 0.0; // produce food resources.Produce(vessel, resource_name, harvest_size); // show a message to the user Message.Post(Lib.BuildString("On <color=FFFFFF>", vessel.vesselName, "</color> the crop harvest produced <color=FFFFFF>", harvest_size.ToString("F0"), " ", resource_name, "</color>")); // record first space harvest if (!vi.landed && DB.Ready()) DB.Landmarks().space_harvest = 1; } // set rmb ui status GrowthStatus = Lib.HumanReadablePerc(growth); LightStatus = Lib.HumanReadablePerc(lighting); WasteStatus = Lib.HumanReadablePerc(waste_perc); SoilStatus = vi.landed ? "yes" : "no"; TTAStatus = Lib.HumanReadableDuration(growing > double.Epsilon ? (1.0 - growth) / growing : 0.0); // enable/disable emergency harvest Events["EmergencyHarvest"].active = (growth >= 0.5); }
static double CurvedPanelOutput(Vessel vessel, ProtoPartSnapshot part, Part prefab, PartModule m, Vector3d sun_dir, double sun_dist, double atmo_factor) { // if, for whatever reason, sun_dist is zero (or negative), we do not return any output if (sun_dist <= double.Epsilon) return 0.0; // shortcuts Quaternion rot = part.rotation; // get values from part string transform_name = Lib.ReflectionValue<string>(m, "PanelTransformName"); float tot_rate = Lib.ReflectionValue<float>(m, "TotalEnergyRate"); // get components Transform[] components = prefab.FindModelTransforms(transform_name); if (components.Length == 0) return 0.0; // calculate solar flux double solar_flux = Sim.SolarFlux(sun_dist); // reduce solar flux inside atmosphere solar_flux *= atmo_factor; // for each one of the components the curved panel is composed of double output = 0.0; foreach(Transform t in components) { double cosine_factor = Math.Max(Vector3d.Dot(sun_dir, (vessel.transform.rotation * rot * t.forward.normalized).normalized), 0.0); output += (double)tot_rate / (double)components.Length * cosine_factor * solar_flux / Sim.SolarFluxAtHome(); } return output; }
// return solar panel EC output // note: we ignore temperature curve, and make sure it is not relavant in the MM patch static double PanelOutput(Vessel vessel, ProtoPartSnapshot part, ModuleDeployableSolarPanel panel, Vector3d sun_dir, double sun_dist, double atmo_factor) { // if, for whatever reason, sun_dist is zero (or negative), we do not return any output if (sun_dist <= double.Epsilon) return 0.0; // shortcuts Quaternion rot = part.rotation; Vector3d normal = panel.part.FindModelComponent<Transform>(panel.raycastTransformName).forward; // calculate cosine factor // note: for gameplay reasons, we ignore tracking panel pivots double cosine_factor = panel.sunTracking ? 1.0 : Math.Max(Vector3d.Dot(sun_dir, (vessel.transform.rotation * rot * normal).normalized), 0.0); // calculate solar flux double solar_flux = Sim.SolarFlux(sun_dist); // reduce solar flux inside atmosphere solar_flux *= atmo_factor; // finally, calculate output return panel.chargeRate * cosine_factor * (panel.useCurve ? panel.powerCurve.Evaluate((float)sun_dist) : solar_flux / Sim.SolarFluxAtHome()); }
public static ec_data analyze_ec(List<Part> parts, environment_data env, crew_data crew, food_data food, oxygen_data oxygen, signal_data signal) { // store data ec_data ec = new ec_data(); // calculate climate cost ec.consumed = (double)crew.count * env.temp_diff * Settings.ElectricChargePerSecond; // scan the parts foreach(Part p in parts) { // accumulate EC storage ec.storage += Lib.GetResourceAmount(p, "ElectricCharge"); // remember if we already considered a resource converter module // rationale: we assume only the first module in a converter is active bool first_converter = true; // for each module foreach(PartModule m in p.Modules) { // command if (m.moduleName == "ModuleCommand") { ModuleCommand mm = (ModuleCommand)m; foreach(ModuleResource res in mm.inputResources) { if (res.name == "ElectricCharge") { ec.consumed += res.rate; } } } // solar panel else if (m.moduleName == "ModuleDeployableSolarPanel") { ModuleDeployableSolarPanel mm = (ModuleDeployableSolarPanel)m; double solar_k = (mm.useCurve ? mm.powerCurve.Evaluate((float)env.sun_dist) : env.sun_flux / Sim.SolarFluxAtHome()); double generated = mm.chargeRate * solar_k * env.atmo_factor; ec.generated_sunlight += generated; ec.best_ec_generator = Math.Max(ec.best_ec_generator, generated); } // generator else if (m.moduleName == "ModuleGenerator") { // skip launch clamps, that include a generator if (p.partInfo.name == "launchClamp1") continue; ModuleGenerator mm = (ModuleGenerator)m; foreach(ModuleResource res in mm.inputList) { if (res.name == "ElectricCharge") { ec.consumed += res.rate; } } foreach(ModuleResource res in mm.outputList) { if (res.name == "ElectricCharge") { ec.generated_shadow += res.rate; ec.generated_sunlight += res.rate; ec.best_ec_generator = Math.Max(ec.best_ec_generator, res.rate); } } } // converter // note: only electric charge is considered for resource converters // note: we only consider the first resource converter in a part, and ignore the rest else if (m.moduleName == "ModuleResourceConverter" && first_converter) { ModuleResourceConverter mm = (ModuleResourceConverter)m; foreach(ResourceRatio rr in mm.inputList) { if (rr.ResourceName == "ElectricCharge") { ec.consumed += rr.Ratio; } } foreach(ResourceRatio rr in mm.outputList) { if (rr.ResourceName == "ElectricCharge") { ec.generated_shadow += rr.Ratio; ec.generated_sunlight += rr.Ratio; ec.best_ec_generator = Math.Max(ec.best_ec_generator, rr.Ratio); } } first_converter = false; } // harvester // note: only electric charge is considered for resource harvesters else if (m.moduleName == "ModuleResourceHarvester") { ModuleResourceHarvester mm = (ModuleResourceHarvester)m; foreach(ResourceRatio rr in mm.inputList) { if (rr.ResourceName == "ElectricCharge") { ec.consumed += rr.Ratio; } } } // active radiators else if (m.moduleName == "ModuleActiveRadiator") { ModuleActiveRadiator mm = (ModuleActiveRadiator)m; if (mm.IsCooling) { foreach(var rr in mm.inputResources) { if (rr.name == "ElectricCharge") { ec.consumed += rr.rate; } } } } // wheels else if (m.moduleName == "ModuleWheelMotor") { ModuleWheelMotor mm = (ModuleWheelMotor)m; if (mm.motorEnabled && mm.inputResource.name == "ElectricCharge") { ec.consumed += mm.inputResource.rate; } } else if (m.moduleName == "ModuleWheelMotorSteering") { ModuleWheelMotorSteering mm = (ModuleWheelMotorSteering)m; if (mm.motorEnabled && mm.inputResource.name == "ElectricCharge") { ec.consumed += mm.inputResource.rate; } } // SCANsat support else if (m.moduleName == "SCANsat" || m.moduleName == "ModuleSCANresourceScanner") { // include it in ec consumption, if deployed if (SCANsat.isDeployed(p, m)) ec.consumed += Lib.ReflectionValue<float>(m, "power"); } // NearFutureSolar support // note: assume half the components are in sunlight, and average inclination is half else if (m.moduleName == "ModuleCurvedSolarPanel") { // get total rate double tot_rate = Lib.ReflectionValue<float>(m, "TotalEnergyRate"); // get number of components int components = p.FindModelTransforms(Lib.ReflectionValue<string>(m, "PanelTransformName")).Length; // approximate output // 0.7071: average clamped cosine ec.generated_sunlight += 0.7071 * tot_rate; } } } // include cost from greenhouses artificial lighting ec.consumed += food.greenhouse_cost; // include cost from scrubbers ec.consumed += oxygen.scrubber_cost; // include relay cost for the best relay antenna ec.consumed += signal.relay_cost; // finally, calculate life expectancy of ec ec.life_expectancy_sunlight = ec.storage / Math.Max(ec.consumed - ec.generated_sunlight, 0.0); ec.life_expectancy_shadow = ec.storage / Math.Max(ec.consumed - ec.generated_shadow, 0.0); // return data return ec; }