Example #1
0
  public static crew_data analyze_crew(List<Part> parts)
  {
    // store data
    crew_data crew = new crew_data();

    // get number of kerbals assigned to the vessel in the editor
    // note: crew manifest is not reset after root part is deleted
    var cad = KSP.UI.CrewAssignmentDialog.Instance;
    if (cad != null && cad.GetManifest() != null)
    {
      List<ProtoCrewMember> manifest = cad.GetManifest().GetAllCrew(false);
      crew.count = (uint)manifest.Count;
      crew.engineer = manifest.Find(k => k.trait == "Engineer") != null;
    }

    // scan the parts
    foreach(Part p in parts)
    {
      // accumulate crew capacity
      crew.capacity += (uint)p.CrewCapacity;
    }

    // return data
    return crew;
  }
Example #2
0
 void render_reliability(reliability_data reliability, crew_data crew)
 {
   render_title("RELIABILITY");
   render_content("malfunctions", Lib.ValueOrNone(reliability.failure_year, "/y"), "per-component average case estimate");
   render_content("redundancy", reliability.redundancy);
   render_content("quality", Malfunction.QualityToString(reliability.quality), "manufacturing quality");
   render_content("engineer", crew.engineer ? "yes" : "no");
   render_space();
 }
Example #3
0
  public static oxygen_data analyze_oxygen(List<Part> parts, environment_data env, crew_data crew)
  {
    // store data
    oxygen_data oxygen = new oxygen_data();

    // get scrubber efficiency
    oxygen.scrubber_efficiency = Scrubber.DeduceEfficiency();

    // calculate oxygen consumed
    oxygen.consumed = !env.breathable ? (double)crew.count * Settings.OxygenPerSecond : 0.0;

    // deduce co2 produced by the crew per-second
    double simulated_co2 = oxygen.consumed;

    // scan the parts
    foreach(Part p in parts)
    {
      // accumulate food storage
      oxygen.storage += Lib.GetResourceAmount(p, "Oxygen");

      // for each module
      foreach(PartModule m in p.Modules)
      {
        // scrubber
        if (m.moduleName == "Scrubber")
        {
          Scrubber mm = (Scrubber)m;

          // do nothing inside breathable atmosphere
          if (mm.is_enabled && !env.breathable)
          {
            double co2_scrubbed = Math.Min(simulated_co2, mm.co2_rate);
            if (co2_scrubbed > double.Epsilon)
            {
              oxygen.scrubber_cost += mm.ec_rate * (co2_scrubbed / mm.co2_rate);
              oxygen.recycled += co2_scrubbed * oxygen.scrubber_efficiency;
              simulated_co2 -= co2_scrubbed;
            }
          }
        }
      }
    }

    // calculate life expectancy
    oxygen.life_expectancy = !env.breathable ? oxygen.storage / Math.Max(oxygen.consumed - oxygen.recycled, 0.0) : double.NaN;

    // return data
    return oxygen;
  }
Example #4
0
  public static qol_data analyze_qol(List<Part> parts, environment_data env, crew_data crew, signal_data signal)
  {
    // store data
    qol_data qol = new qol_data();

    // scan the parts
    foreach(Part p in parts)
    {
      // for each module
      foreach(PartModule m in p.Modules)
      {
        // entertainment
        if (m.moduleName == "Entertainment")
        {
          Entertainment mm = (Entertainment)m;
          qol.entertainment *= mm.rate;
        }
      }
    }

    // calculate Quality-Of-Life bonus
    // note: ignore kerbal-specific variance
    if (crew.capacity > 0)
    {
      double bonus = QualityOfLife.Bonus(crew.count, crew.capacity, qol.entertainment, env.landed, signal.range > 0.0);
      qol.living_space = QualityOfLife.LivingSpace(crew.count, crew.capacity);
      qol.time_to_instability = bonus / Settings.StressedDegradationRate;
      List<string> factors = new List<string>();
      if (crew.count > 1) factors.Add("not-alone");
      if (signal.range > 0.0) factors.Add("call-home");
      if (env.landed) factors.Add("firm-ground");
      if (factors.Count == 0) factors.Add("none");
      qol.factors = String.Join(", ", factors.ToArray());
    }
    else
    {
      qol.living_space = 0.0;
      qol.time_to_instability = double.NaN;
      qol.factors = "none";
    }

    // return data
    return qol;
  }
Example #5
0
  void render_radiation(radiation_data radiation, environment_data env, crew_data crew)
  {
    string magnetosphere_str = Radiation.HasMagnetosphere(env.body) ? Lib.HumanReadableRange(Radiation.MagnAltitude(env.body)) : "none";
    string belt_strength_str = Radiation.HasBelt(env.body) ? " (" + (Radiation.Dynamo(env.body) * Settings.BeltRadiation * (60.0 * 60.0)).ToString("F0") + " rad/h)" : "";
    string belt_str = Radiation.HasBelt(env.body) ? Lib.HumanReadableRange(Radiation.BeltAltitude(env.body)) : "none";
    string shield_str = Radiation.ShieldingToString(radiation.shielding_amount, radiation.shielding_capacity);
    string shield_tooltip = radiation.shielding_capacity > 0 ? "average over the vessel" : "";
    string life_str = Lib.HumanReadableDuration(radiation.life_expectancy[0]) + "</b> / <b>" + Lib.HumanReadableDuration(radiation.life_expectancy[1]);
    string life_tooltip = "cosmic / storm";
    if (Radiation.HasBelt(env.body))
    {
      life_str += "</b> / <b>" + Lib.HumanReadableDuration(radiation.life_expectancy[2]);
      life_tooltip += " / belt";
    }

    render_title("RADIATION");
    render_content("magnetosphere", magnetosphere_str, "protect from cosmic radiation");
    render_content("radiation belt", belt_str, "abnormal radiation zone" + belt_strength_str);
    render_content("shielding", shield_str, shield_tooltip);
    render_content("life expectancy", crew.capacity > 0 ? life_str : "perpetual", crew.capacity > 0 ? life_tooltip : "");
    render_space();
  }
Example #6
0
  public void render()
  {
    // if there is something in the editor
    if (EditorLogic.RootPart != null)
    {
      // store situations and altitude multipliers
      string[] situations = {"Landed", "Low Orbit", "Orbit", "High Orbit"};
      double[] altitude_mults = {0.0, 0.33, 1.0, 3.0};

      // get body, situation and altitude multiplier
      CelestialBody body = FlightGlobals.Bodies[body_index];
      string situation = situations[situation_index];
      double altitude_mult = altitude_mults[situation_index];

      // get parts recursively
      List<Part> parts = Lib.GetPartsRecursively(EditorLogic.RootPart);

      // analyze
      environment_data env = analyze_environment(body, altitude_mult);
      crew_data crew = analyze_crew(parts);
      food_data food = analyze_food(parts, env, crew);
      oxygen_data oxygen = analyze_oxygen(parts, env, crew);
      signal_data signal = analyze_signal(parts);
      qol_data qol = analyze_qol(parts, env, crew, signal);
      radiation_data radiation = analyze_radiation(parts, env, crew);
      ec_data ec = analyze_ec(parts, env, crew, food, oxygen, signal);
      reliability_data reliability = analyze_reliability(parts, ec, signal);

      // render menu
      GUILayout.BeginHorizontal(row_style);
      if (GUILayout.Button(body.name, leftmenu_style)) { body_index = (body_index + 1) % FlightGlobals.Bodies.Count; if (body_index == 0) ++body_index; }
      if (GUILayout.Button("["+ (page + 1) + "/2]", midmenu_style)) { page = (page + 1) % 2; }
      if (GUILayout.Button(situation, rightmenu_style)) { situation_index = (situation_index + 1) % situations.Length; }
      GUILayout.EndHorizontal();

      // page 1/2
      if (page == 0)
      {
        // render
        render_ec(ec);
        render_food(food);
        render_oxygen(oxygen);
        render_qol(qol);
      }
      // page 2/2
      else
      {
        // render
        render_radiation(radiation, env, crew);
        render_reliability(reliability, crew);
        render_signal(signal, env, crew);
        render_environment(env);
      }
    }
    // if there is nothing in the editor
    else
    {
      // render quote
      GUILayout.FlexibleSpace();
      GUILayout.BeginHorizontal();
      GUILayout.Label("<i>In preparing for space, I have always found that\nplans are useless but planning is indispensable.\nWernher von Kerman</i>", quote_style);
      GUILayout.EndHorizontal();
      GUILayout.Space(10.0f);
    }
  }
Example #7
0
  void render_signal(signal_data signal, environment_data env, crew_data crew)
  {
    // approximate min/max distance between home and target body
    CelestialBody home = FlightGlobals.GetHomeBody();
    double home_dist_min = 0.0;
    double home_dist_max = 0.0;
    if (env.body == home)
    {
      home_dist_min = env.altitude;
      home_dist_max = env.altitude;
    }
    else if (env.body.referenceBody == home)
    {
      home_dist_min = Sim.Periapsis(env.body);
      home_dist_max = Sim.Apoapsis(env.body);
    }
    else
    {
      double home_p = Sim.Periapsis(Lib.PlanetarySystem(home));
      double home_a = Sim.Apoapsis(Lib.PlanetarySystem(home));
      double body_p = Sim.Periapsis(Lib.PlanetarySystem(env.body));
      double body_a = Sim.Apoapsis(Lib.PlanetarySystem(env.body));
      home_dist_min = Math.Min(Math.Abs(home_a - body_p), Math.Abs(home_p - body_a));
      home_dist_max = home_a + body_a;
    }

    // calculate if antenna is out of range from target body
    string range_tooltip = "";
    if (signal.range > double.Epsilon)
    {
      if (signal.range < home_dist_min) range_tooltip = "<color=#ff0000>out of range</color>";
      else if (signal.range < home_dist_max) range_tooltip = "<color=#ffff00>partially out of range</color>";
      else range_tooltip = "<color=#00ff00>in range</color>";
      if (home_dist_max > double.Epsilon) //< if not landed at home
      {
        range_tooltip += "\nbody distance (min): <b>" + Lib.HumanReadableRange(home_dist_min) + "</b>"
        + "\nbody distance (max): <b>" + Lib.HumanReadableRange(home_dist_max) + "</b>";
      }
    }
    else if (crew.capacity == 0) range_tooltip = "<color=#ff0000>no antenna on unmanned vessel</color>";

    // calculate transmission cost
    double cost = signal.range > double.Epsilon
      ? signal.transmission_cost_min + (signal.transmission_cost_max - signal.transmission_cost_min) * Math.Min(home_dist_max, signal.range) / signal.range
      : 0.0;
    string cost_str = signal.range > double.Epsilon ? cost.ToString("F1") + " EC/Mbit" : "none";

    // generate ecc table
    Func<double, double, double, string> deduce_color = (double range, double dist_min, double dist_max) =>
    {
      if (range < dist_min) return "<color=#ff0000>";
      else if (range < dist_max) return "<color=#ffff00>";
      else return "<color=#ffffff>";
    };
    double signal_100 = signal.range / signal.ecc;
    double signal_15 = signal_100 * 0.15;
    double signal_33 = signal_100 * 0.33;
    double signal_66 = signal_100 * 0.66;
    string ecc_tooltip = signal.range > double.Epsilon
      ? "<align=left /><b>ecc</b>\t<b>range</b>"
      + "\n15%\t"  + deduce_color(signal_15, home_dist_min, home_dist_max) + Lib.HumanReadableRange(signal_15) + "</color>"
      + "\n33%\t"  + deduce_color(signal_33, home_dist_min, home_dist_max) + Lib.HumanReadableRange(signal_33) + "</color>"
      + "\n66%\t"  + deduce_color(signal_66, home_dist_min, home_dist_max) + Lib.HumanReadableRange(signal_66) + "</color>"
      + "\n100%\t" + deduce_color(signal_100,home_dist_min, home_dist_max) + Lib.HumanReadableRange(signal_100) + "</color>"
      : "";


    render_title("SIGNAL");
    render_content("range", Lib.HumanReadableRange(signal.range), range_tooltip);
    render_content("relay", signal.relay_range <= double.Epsilon ? "none" : signal.relay_range < signal.range ? Lib.HumanReadableRange(signal.relay_range) : "yes");
    render_content("transmission", cost_str, "worst case data transmission cost\nfrom destination body");
    render_content("error correction", (signal.ecc * 100.0).ToString("F0") + "%", ecc_tooltip);
    render_space();
  }
Example #8
0
  public static radiation_data analyze_radiation(List<Part> parts, environment_data env, crew_data crew)
  {
    // store data
    radiation_data radiation = new radiation_data();

    // scan the parts
    foreach(Part p in parts)
    {
      // accumulate shielding amount and capacity
      radiation.shielding_amount += Lib.GetResourceAmount(p, "Shielding");
      radiation.shielding_capacity += Lib.GetResourceCapacity(p, "Shielding");
    }

    // calculate radiation data
    double shielding = Radiation.Shielding(radiation.shielding_amount, radiation.shielding_capacity);
    double belt_strength = Settings.BeltRadiation * Radiation.Dynamo(env.body) * 0.5; //< account for the 'ramp'
    if (crew.capacity > 0)
    {
      radiation.life_expectancy = new double[]
      {
        Settings.RadiationFatalThreshold / (Settings.CosmicRadiation * (1.0 - shielding)),
        Settings.RadiationFatalThreshold / (Settings.StormRadiation * (1.0 - shielding)),
        Radiation.HasBelt(env.body) ? Settings.RadiationFatalThreshold / (belt_strength * (1.0 - shielding)) : double.NaN
      };
    }
    else
    {
      radiation.life_expectancy = new double[]{double.NaN, double.NaN, double.NaN};
    }

    // return data
    return radiation;
  }
Example #9
0
  public static food_data analyze_food(List<Part> parts, environment_data env, crew_data crew)
  {
    // store data
    food_data food = new food_data();

    // calculate food consumed
    food.consumed = (double)crew.count * Settings.FoodPerMeal / Settings.MealFrequency;

    // deduce waste produced by the crew per-second
    double simulated_waste = food.consumed;

    // scan the parts
    foreach(Part p in parts)
    {
      // accumulate food storage
      food.storage += Lib.GetResourceAmount(p, "Food");

      // for each module
      foreach(PartModule m in p.Modules)
      {
        // greenhouse
        if (m.moduleName == "Greenhouse")
        {
          Greenhouse mm = (Greenhouse)m;

          // calculate natural lighting
          double natural_lighting = Greenhouse.NaturalLighting(env.sun_dist);

          // calculate ec consumed
          food.greenhouse_cost += mm.ec_rate * mm.lamps;

          // calculate lighting
          double lighting = natural_lighting * (mm.door_opened ? 1.0 : 0.0) + mm.lamps * (mm.door_opened ? 1.0 : 1.0 + Settings.GreenhouseDoorBonus);

          // calculate waste used
          double waste_used = Math.Min(simulated_waste, mm.waste_rate);
          double waste_perc = waste_used / mm.waste_rate;
          simulated_waste -= waste_used;

          // calculate growth bonus
          double growth_bonus = 0.0;
          growth_bonus += Settings.GreenhouseSoilBonus * (env.landed ? 1.0 : 0.0);
          growth_bonus += Settings.GreenhouseWasteBonus * waste_perc;

          // calculate growth factor
          double growth_factor = (mm.growth_rate * (1.0 + growth_bonus)) * lighting;

          // calculate food cultivated
          food.cultivated += mm.harvest_size * growth_factor;

          // calculate time-to-harvest
          if (growth_factor > double.Epsilon)
          {
            food.cultivated_tooltip += (food.cultivated_tooltip.Length > 0 ? "\n" : "")
              + "Time-to-harvest: <b>" + Lib.HumanReadableDuration(1.0 / growth_factor) + "</b>";
          }
        }
      }
    }

    // calculate life expectancy
    food.life_expectancy = food.storage / Math.Max(food.consumed - food.cultivated, 0.0);

    // add formatting to tooltip
    if (food.cultivated_tooltip.Length > 0) food.cultivated_tooltip = "<i>" + food.cultivated_tooltip + "</i>";

    // return data
    return food;
  }
Example #10
0
  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;
  }