void Indicator_EC(Panel p, Vessel v, Vessel_Info vi) { Resource_Info ec = ResourceCache.Info(v, "ElectricCharge"); Supply supply = Profile.supplies.Find(k => k.resource == "ElectricCharge"); double low_threshold = supply != null ? supply.low_threshold : 0.15; double depletion = ec.Depletion(vi.crew_count); string tooltip = Lib.BuildString ( "<align=left /><b>name\tlevel\tduration</b>\n", ec.level <= 0.005 ? "<color=#ff0000>" : ec.level <= low_threshold ? "<color=#ffff00>" : "<color=#cccccc>", "EC\t", Lib.HumanReadablePerc(ec.level), "\t", depletion <= double.Epsilon ? "depleted" : Lib.HumanReadableDuration(depletion), "</color>" ); Texture image = ec.level <= 0.005 ? Icons.battery_red : ec.level <= low_threshold ? Icons.battery_yellow : Icons.battery_white; p.SetIcon(image, tooltip); }
public void Update() { // in flight if (Lib.IsFlight()) { // get info from cache Vessel_Info vi = Cache.VesselInfo(vessel); // do nothing if vessel is invalid if (!vi.is_valid) { return; } // update status Status = Telemetry_Content(vessel, vi, type); // if there is a pin animation if (pin.Length > 0) { // still-play pin animation pin_anim.Still(Telemetry_Pin(vessel, vi, type)); } } }
// return true if body is relevant to the player // - body: reference body of the planetary system static bool Body_is_Relevant(CelestialBody body) { // [disabled] // special case: home system is always relevant // note: we deal with the case of a planet mod setting homebody as a moon //if (body == Lib.PlanetarySystem(FlightGlobals.GetHomeBody())) return true; // for each vessel foreach (Vessel v in FlightGlobals.Vessels) { // if inside the system if (Lib.PlanetarySystem(v.mainBody) == body) { // get info from the cache Vessel_Info vi = Cache.VesselInfo(v); // skip invalid vessels if (!vi.is_valid) { continue; } // obey message config if (!DB.Vessel(v).cfg_storm) { continue; } // body is relevant return(true); } } return(false); }
public static void FileMan(this Panel p, Vessel v) { // avoid corner-case when this is called in a lambda after scene changes v = FlightGlobals.FindVessel(v.id); // if vessel doesn't exist anymore, leave the panel empty if (v == null) { return; } // get info from the cache Vessel_Info vi = Cache.VesselInfo(v); // if not a valid vessel, leave the panel empty if (!vi.is_valid) { return; } // set metadata p.Title(Lib.BuildString(Lib.Ellipsis(v.vesselName, 20), " <color=#cccccc>FILE MANAGER</color>")); p.Width(320.0f); // time-out simulation if (p.Timeout(vi)) { return; } // get vessel drive Drive drive = DB.Vessel(v).drive; // draw data section p.SetSection("DATA"); foreach (var pair in drive.files) { string filename = pair.Key; File file = pair.Value; Render_File(p, filename, file, drive); } if (drive.files.Count == 0) { p.SetContent("<i>no files</i>", string.Empty); } // draw samples section p.SetSection("SAMPLES"); foreach (var pair in drive.samples) { string filename = pair.Key; Sample sample = pair.Value; Render_Sample(p, filename, sample, drive); } if (drive.samples.Count == 0) { p.SetContent("<i>no samples</i>", string.Empty); } }
public static double Evaluate(Vessel v, Vessel_Info vi, Vessel_Resources resources, List <string> modifiers) { double k = 1.0; foreach (string mod in modifiers) { switch (mod) { case "breathable": k *= vi.breathable ? 0.0 : 1.0; break; case "temperature": k *= vi.temp_diff; break; case "radiation": k *= vi.radiation; break; case "shielding": k *= 1.0 - vi.shielding; break; case "volume": k *= vi.volume; break; case "surface": k *= vi.surface; break; case "living_space": k /= vi.living_space; break; case "comfort": k /= vi.comforts.factor; break; case "pressure": k *= vi.pressure > Settings.PressureThreshold ? 1.0 : Settings.PressureFactor; break; case "poisoning": k *= vi.poisoning > Settings.PoisoningThreshold ? 1.0 : Settings.PoisoningFactor; break; case "per_capita": k /= (double)Math.Max(vi.crew_count, 1); break; default: k *= resources.Info(v, mod).amount; break; } } return(k); }
void Problem_Sunlight(Vessel_Info info, ref List <Texture> icons, ref List <string> tooltips) { if (info.sunlight <= double.Epsilon) { icons.Add(Icons.sun_black); tooltips.Add("In shadow"); } }
public static void Update(Vessel v, Vessel_Info vi, VesselData vd, double elapsed_s) { // do nothing if storms are disabled if (!Features.SpaceWeather) { return; } // only consider vessels in interplanetary space if (v.mainBody.flightGlobalsIndex != 0) { return; } // skip unmanned vessels if (vi.crew_count == 0) { return; } // generate storm time if necessary if (vd.storm_time <= double.Epsilon) { vd.storm_time = Settings.StormMinTime + (Settings.StormMaxTime - Settings.StormMinTime) * Lib.RandomDouble(); } // accumulate age vd.storm_age += elapsed_s * Storm_Frequency(vi.sun_dist); // if storm is over if (vd.storm_age > vd.storm_time && vd.storm_state == 2) { vd.storm_age = 0.0; vd.storm_time = 0.0; vd.storm_state = 0; // send message Message.Post(Severity.relax, Lib.BuildString("The solar storm around <b>", v.vesselName, "</b> is over")); } // if storm is in progress else if (vd.storm_age > vd.storm_time - Settings.StormDuration && vd.storm_state == 1) { vd.storm_state = 2; // send message Message.Post(Severity.danger, Lib.BuildString("The coronal mass ejection hit <b>", v.vesselName, "</b>"), Lib.BuildString("Storm duration: ", Lib.HumanReadableDuration(TimeLeftCME(vd.storm_time, vd.storm_age)))); } // if storm is incoming else if (vd.storm_age > vd.storm_time - Settings.StormDuration - Time_to_Impact(vi.sun_dist) && vd.storm_state == 0) { vd.storm_state = 1; // send message Message.Post(Severity.warning, Lib.BuildString("Our observatories report a coronal mass ejection directed toward <b>", v.vesselName, "</b>"), Lib.BuildString("Time to impact: ", Lib.HumanReadableDuration(TimeBeforeCME(vd.storm_time, vd.storm_age)))); } }
public static bool Timeout(this Panel p, Vessel_Info vi) { if (!vi.connection.linked && vi.crew_count == 0) { p.SetHeader(msg[((int)Time.realtimeSinceStartup) % msg.Length]); return(true); } return(false); }
// Monitoring all indicator to support life public static void Telemetry_Life(this Panel p, Vessel v) { // avoid corner-case when this is called in a lambda after scene changes v = FlightGlobals.FindVessel(v.id); // if vessel doesn't exist anymore, leave the panel empty if (v == null) { return; } // get info from the cache Vessel_Info vi = Cache.VesselInfo(v); // if not a valid vessel, leave the panel empty if (!vi.is_valid) { return; } // set metadata p.Title(Lib.BuildString(Lib.Ellipsis(v.vesselName, 20), " <color=#cccccc>TELEMETRY</color>")); // time-out simulation if (p.Timeout(vi)) { return; } // get vessel data VesselData vd = DB.Vessel(v); // get resources Vessel_Resources resources = ResourceCache.Get(v); // get crew var crew = Lib.CrewList(v); // draw the content Render_Crew(p, crew); Render_Greenhouse(p, vi); Render_Supplies(p, v, vi, resources); Render_Habitat(p, v, vi); Render_Environment(p, v, vi); // collapse eva kerbal sections into one if (v.isEVA) { p.Collapse("EVA SUIT"); } }
// get readings value public static double Telemetry_Value(Vessel v, Vessel_Info vi, string type) { switch (type) { case "temperature": return(vi.temperature); case "radiation": return(vi.radiation); case "pressure": return(v.mainBody.GetPressure(v.altitude));; case "gravioli": return(vi.gravioli); } return(0.0); }
// get readings value in [0,1] range, for pin animation public static double Telemetry_Pin(Vessel v, Vessel_Info vi, string type) { switch (type) { case "temperature": return(Math.Min(vi.temperature / 11000.0, 1.0)); case "radiation": return(Math.Min(vi.radiation * 3600.0 / 11.0, 1.0)); case "pressure": return(Math.Min(v.mainBody.GetPressure(v.altitude) / Sim.PressureAtSeaLevel() / 11.0, 1.0)); case "gravioli": return(Math.Min(vi.gravioli, 1.0)); } return(0.0); }
// get readings short text info public static string Telemetry_Content(Vessel v, Vessel_Info vi, string type) { switch (type) { case "temperature": return(Lib.HumanReadableTemp(vi.temperature)); case "radiation": return(Lib.HumanReadableRadiation(vi.radiation)); case "pressure": return(Lib.HumanReadablePressure(v.mainBody.GetPressure(v.altitude))); case "gravioli": return(vi.gravioli < 0.33 ? "nothing here" : vi.gravioli < 0.66 ? "almost one" : "WOW!"); } return(string.Empty); }
void Problem_Poisoning(Vessel_Info info, ref List <Texture> icons, ref List <string> tooltips) { string poisoning_str = Lib.BuildString("CO2 level in internal atmosphere: <b>", Lib.HumanReadablePerc(info.poisoning), "</b>"); if (info.poisoning >= 0.05) { icons.Add(Icons.recycle_red); tooltips.Add(poisoning_str); } else if (info.poisoning > 0.025) { icons.Add(Icons.recycle_yellow); tooltips.Add(poisoning_str); } }
public static void Update(Vessel v, Vessel_Info vi, VesselData vd, double elapsed_s) { // do nothing if signal mechanic is disabled if (!Features.Signal && !Features.KCommNet) { return; } // get connection info ConnectionInfo conn = vi.connection; // maintain and send messages // - do not send messages for vessels without an antenna // - do not send messages during/after solar storms // - do not send messages for EVA kerbals if (conn.status != LinkStatus.no_antenna && !v.isEVA && v.situation != Vessel.Situations.PRELAUNCH) { if (!vd.msg_signal && !conn.linked) { vd.msg_signal = true; if (vd.cfg_signal && conn.status != LinkStatus.blackout) { string subtext = "Data transmission disabled"; if (vi.crew_count == 0) { switch (Settings.UnlinkedControl) { case UnlinkedCtrl.none: subtext = "Remote control disabled"; break; case UnlinkedCtrl.limited: subtext = "Limited control available"; break; } } Message.Post(Severity.warning, Lib.BuildString("Signal lost with <b>", v.vesselName, "</b>"), subtext); } } else if (vd.msg_signal && conn.linked) { vd.msg_signal = false; if (vd.cfg_signal && !Storm.JustEnded(v, elapsed_s)) { var path = conn.path; Message.Post(Severity.relax, Lib.BuildString("<b>", v.vesselName, "</b> signal is back"), path.Count == 0 ? "We got a direct link with the space center" : Lib.BuildString("Relayed by <b>", path[path.Count - 1].vesselName, "</b>")); } } } }
// return vessel situation valid for specified experiment public static string Situation(Vessel v, string situations) { // shortcuts CelestialBody body = v.mainBody; Vessel_Info vi = Cache.VesselInfo(v); List <string> list = Lib.Tokenize(situations, ','); foreach (string sit in list) { bool b = false; switch (sit) { case "Surface": b = Lib.Landed(v); break; case "Atmosphere": b = body.atmosphere && v.altitude < body.atmosphereDepth; break; case "Ocean": b = body.ocean && v.altitude < 0.0; break; case "Space": b = body.flightGlobalsIndex != 0 && !Lib.Landed(v) && v.altitude > body.atmosphereDepth; break; case "AbsoluteZero": b = vi.temperature < 30.0; break; case "InnerBelt": b = vi.inner_belt; break; case "OuterBelt": b = vi.outer_belt; break; case "Magnetosphere": b = vi.magnetosphere; break; case "Thermosphere": b = vi.thermosphere; break; case "Exosphere": b = vi.exosphere; break; case "InterPlanetary": b = body.flightGlobalsIndex == 0 && !vi.interstellar; break; case "InterStellar": b = body.flightGlobalsIndex == 0 && vi.interstellar; break; } if (b) { return(sit); } } return(string.Empty); }
void Indicator_Supplies(Panel p, Vessel v, Vessel_Info vi) { List <string> tooltips = new List <string>(); uint max_severity = 0; if (vi.crew_count > 0) { foreach (Supply supply in Profile.supplies.FindAll(k => k.resource != "ElectricCharge")) { Resource_Info res = ResourceCache.Info(v, supply.resource); double depletion = res.Depletion(vi.crew_count); if (res.capacity > double.Epsilon) { if (tooltips.Count == 0) { tooltips.Add("<align=left /><b>name\t\tlevel\tduration</b>"); } tooltips.Add(Lib.BuildString ( res.level <= 0.005 ? "<color=#ff0000>" : res.level <= supply.low_threshold ? "<color=#ffff00>" : "<color=#cccccc>", supply.resource, supply.resource != "Ammonia" ? "\t\t" : "\t", //< hack: make ammonia fit damn it Lib.HumanReadablePerc(res.level), "\t", depletion <= double.Epsilon ? "depleted" : Lib.HumanReadableDuration(depletion), "</color>" )); uint severity = res.level <= 0.005 ? 2u : res.level <= supply.low_threshold ? 1u : 0; max_severity = Math.Max(max_severity, severity); } } } Texture image = max_severity == 2 ? Icons.box_red : max_severity == 1 ? Icons.box_yellow : Icons.box_white; p.SetIcon(image, string.Join("\n", tooltips.ToArray())); }
public static Vessel_Info VesselInfo(Vessel v) { // get vessel id UInt32 id = Lib.VesselID(v); // get the info from the cache, if it exist if (vessels.TryGetValue(id, out Vessel_Info info)) { return(info); } // compute vessel info info = new Vessel_Info(v, id, next_inc++); // store vessel info in the cache vessels.Add(id, info); return(info); }
void Problem_Radiation(Vessel_Info info, ref List <Texture> icons, ref List <string> tooltips) { string radiation_str = Lib.BuildString(" (<i>", (info.radiation * 60.0 * 60.0).ToString("F3"), " rad/h)</i>"); if (info.radiation > 1.0 / 3600.0) { icons.Add(Icons.radiation_red); tooltips.Add(Lib.BuildString("Exposed to extreme radiation", radiation_str)); } else if (info.radiation > 0.15 / 3600.0) { icons.Add(Icons.radiation_yellow); tooltips.Add(Lib.BuildString("Exposed to intense radiation", radiation_str)); } else if (info.radiation > 0.0195 / 3600.0) { icons.Add(Icons.radiation_yellow); tooltips.Add(Lib.BuildString("Exposed to moderate radiation", radiation_str)); } }
public static void Execute(Vessel v, Vessel_Info vi, VesselData vd, Vessel_Resources resources, double elapsed_s) { // execute all supplies foreach (Supply supply in supplies) { supply.Execute(v, vd, resources); } // execute all rules foreach (Rule rule in rules) { rule.Execute(v, vi, resources, elapsed_s); } // execute all processes foreach (Process process in processes) { process.Execute(v, vi, resources, elapsed_s); } }
public static void SetLocks(Vessel v, Vessel_Info vi) { // lock controls for EVA death if (EVA.IsDead(v)) { InputLockManager.SetControlLock(ControlTypes.EVA_INPUT, "eva_dead_lock"); } // lock controls for probes without signal if (vi.is_valid && !vi.connection.linked && vi.crew_count == 0 && Settings.UnlinkedControl != UnlinkedCtrl.full) { // choose no controls, or only full/zero throttle and staging ControlTypes ctrl = Settings.UnlinkedControl == UnlinkedCtrl.none ? ControlTypes.ALL_SHIP_CONTROLS : ControlTypes.PARTIAL_SHIP_CONTROLS; InputLockManager.SetControlLock(ctrl, "no_signal_lock"); FlightInputHandler.state.mainThrottle = 0.0f; } }
public void Execute(Vessel v, Vessel_Info vi, Vessel_Resources resources, double elapsed_s) { // evaluate modifiers double k = Modifiers.Evaluate(v, vi, resources, modifiers); // only execute processes if necessary if (k > double.Epsilon) { // prepare recipe Resource_Recipe recipe = new Resource_Recipe(); foreach (var p in inputs) { recipe.Input(p.Key, p.Value * k * elapsed_s); } foreach (var p in outputs) { recipe.Output(p.Key, p.Value * k * elapsed_s, dump.Check(p.Key)); } resources.Transform(recipe); } }
static void Render_Habitat(Panel p, Vessel v, Vessel_Info vi) { // if habitat feature is disabled, do not show the panel if (!Features.Habitat) { return; } // if vessel is unmanned, do not show the panel if (vi.crew_count == 0) { return; } // render panel, add some content based on enabled features p.SetSection("HABITAT"); if (Features.Poisoning) { p.SetContent("co2 level", Lib.Color(Lib.HumanReadablePerc(vi.poisoning, "F2"), vi.poisoning > Settings.PoisoningThreshold, "yellow")); } if (!v.isEVA) { if (Features.Pressure) { p.SetContent("pressure", Lib.HumanReadablePressure(vi.pressure * Sim.PressureAtSeaLevel())); } if (Features.Shielding) { p.SetContent("shielding", Lib.HumanReadableShielding(vi.shielding)); } if (Features.LivingSpace) { p.SetContent("living space", Habitat.Living_Space_to_String(vi.living_space)); } if (Features.Comfort) { p.SetContent("comfort", vi.comforts.Summary(), vi.comforts.Tooltip()); } } }
static void Render_Greenhouse(Panel p, Vessel_Info vi) { // do nothing without greenhouses if (vi.greenhouses.Count == 0) { return; } // panel section p.SetSection("GREENHOUSE"); // for each greenhouse for (int i = 0; i < vi.greenhouses.Count; ++i) { var greenhouse = vi.greenhouses[i]; // state string string state = greenhouse.issue.Length > 0 ? Lib.BuildString("<color=yellow>", greenhouse.issue, "</color>") : greenhouse.growth >= 0.99 ? "<color=green>ready to harvest</color>" : "growing"; // tooltip with summary string tooltip = greenhouse.growth < 0.99 ? Lib.BuildString ( "<align=left />", "time to harvest\t<b>", Lib.HumanReadableDuration(greenhouse.tta), "</b>\n", "growth\t\t<b>", Lib.HumanReadablePerc(greenhouse.growth), "</b>\n", "natural lighting\t<b>", Lib.HumanReadableFlux(greenhouse.natural), "</b>\n", "artificial lighting\t<b>", Lib.HumanReadableFlux(greenhouse.artificial), "</b>" ) : string.Empty; // render it p.SetContent(Lib.BuildString("crop #", (i + 1).ToString()), state, tooltip); // issues too, why not p.SetIcon(greenhouse.issue.Length == 0 ? Icons.plant_white : Icons.plant_yellow, tooltip); } }
static void Render_Supplies(Panel p, Vessel v, Vessel_Info vi, Vessel_Resources resources) { // for each supply int supplies = 0; foreach (Supply supply in Profile.supplies) { // get resource info Resource_Info res = resources.Info(v, supply.resource); // only show estimate if the resource is present if (res.amount <= double.Epsilon) { continue; } // render panel title, if not done already if (supplies == 0) { p.SetSection("SUPPLIES"); } // rate tooltip string rate_tooltip = Math.Abs(res.rate) >= 1e-10 ? Lib.BuildString ( res.rate > 0.0 ? "<color=#00ff00><b>" : "<color=#ff0000><b>", Lib.HumanReadableRate(Math.Abs(res.rate)), "</b></color>" ) : string.Empty; // determine label string label = supply.resource == "ElectricCharge" ? "battery" : Lib.SpacesOnCaps(supply.resource).ToLower(); // finally, render resource supply p.SetContent(label, Lib.HumanReadableDuration(res.Depletion(vi.crew_count)), rate_tooltip); ++supplies; } }
void Indicator_Problems(Panel p, Vessel v, Vessel_Info vi, List <ProtoCrewMember> crew) { // store problems icons & tooltips List <Texture> problem_icons = new List <Texture>(); List <string> problem_tooltips = new List <string>(); // detect problems Problem_Sunlight(vi, ref problem_icons, ref problem_tooltips); if (Features.SpaceWeather) { Problem_Storm(v, ref problem_icons, ref problem_tooltips); } if (crew.Count > 0 && Profile.rules.Count > 0) { Problem_Kerbals(crew, ref problem_icons, ref problem_tooltips); } if (crew.Count > 0 && Features.Radiation) { Problem_Radiation(vi, ref problem_icons, ref problem_tooltips); } Problem_Greenhouses(v, vi.greenhouses, ref problem_icons, ref problem_tooltips); if (Features.Poisoning) { Problem_Poisoning(vi, ref problem_icons, ref problem_tooltips); } // choose problem icon const UInt64 problem_icon_time = 3; Texture problem_icon = Icons.empty; if (problem_icons.Count > 0) { UInt64 problem_index = ((UInt64)Time.realtimeSinceStartup / problem_icon_time) % (UInt64)(problem_icons.Count); problem_icon = problem_icons[(int)problem_index]; } // generate problem icon p.SetIcon(problem_icon, String.Join("\n", problem_tooltips.ToArray())); }
void Indicator_Reliability(Panel p, Vessel v, Vessel_Info vi) { Texture image; string tooltip; if (!vi.malfunction) { image = Icons.wrench_white; tooltip = string.Empty; } else if (!vi.critical) { image = Icons.wrench_yellow; tooltip = "Malfunctions"; } else { image = Icons.wrench_red; tooltip = "Critical failures"; } p.SetIcon(image, tooltip); }
static void Render_Environment(Panel p, Vessel v, Vessel_Info vi) { // don't show env panel in eva kerbals if (v.isEVA) { return; } // get all sensor readings HashSet <string> readings = new HashSet <string>(); if (v.loaded) { foreach (var s in Lib.FindModules <Sensor>(v)) { readings.Add(s.type); } } else { foreach (ProtoPartModuleSnapshot m in Lib.FindModules(v.protoVessel, "Sensor")) { readings.Add(Lib.Proto.GetString(m, "type")); } } readings.Remove(string.Empty); p.SetSection("ENVIRONMENT"); foreach (string type in readings) { p.SetContent(type, Sensor.Telemetry_Content(v, vi, type), Sensor.Telemetry_Tooltip(v, vi, type)); } if (readings.Count == 0) { p.SetContent("<i>no sensors installed</i>"); } }
// get readings tooltip public static string Telemetry_Tooltip(Vessel v, Vessel_Info vi, string type) { switch (type) { case "temperature": return(Lib.BuildString ( "<align=left />", "solar flux\t<b>", Lib.HumanReadableFlux(vi.solar_flux), "</b>\n", "albedo flux\t<b>", Lib.HumanReadableFlux(vi.albedo_flux), "</b>\n", "body flux\t<b>", Lib.HumanReadableFlux(vi.body_flux), "</b>" )); case "radiation": return(string.Empty); case "pressure": return(vi.underwater ? "inside <b>ocean</b>" : vi.atmo_factor < 1.0 ? Lib.BuildString("inside <b>atmosphere</b> (", vi.breathable ? "breathable" : "not breathable", ")") : Sim.InsideThermosphere(v) ? "inside <b>thermosphere</b>" : Sim.InsideExosphere(v) ? "inside <b>exosphere</b>" : string.Empty); case "gravioli": return(Lib.BuildString ( "Gravioli detection events per-year: <b>", vi.gravioli.ToString("F2"), "</b>\n\n", "<i>The elusive negative gravioli particle\nseem to be much harder to detect\n", "than expected. On the other\nside there seems to be plenty\nof useless positive graviolis around.</i>" )); } return(string.Empty); }
// show warning message when a vessel cross a radiation belt public static void BeltWarnings(Vessel v, Vessel_Info vi, VesselData vd) { // if radiation is enabled if (Features.Radiation) { // we only show the warning for manned vessels, or for all vessels the first time its crossed bool must_warn = vi.crew_count > 0 || !DB.landmarks.belt_crossing; // are we inside a belt bool inside_belt = vi.inner_belt || vi.outer_belt; // show the message if (inside_belt && !vd.msg_belt && must_warn) { Message.Post(Lib.BuildString("<b>", v.vesselName, "</b> is crossing <i>", v.mainBody.bodyName, " radiation belt</i>"), "Exposed to extreme radiation"); vd.msg_belt = true; } else if (!inside_belt && vd.msg_belt) { // no message after crossing the belt vd.msg_belt = false; } // record first belt crossing if (inside_belt) { DB.landmarks.belt_crossing = true; } // record first heliopause crossing if (vi.interstellar) { DB.landmarks.heliopause_crossing = true; } } }
public static bool HasVesselInfo(Vessel v, out Vessel_Info vi) => vessels.TryGetValue(Lib.VesselID(v), out vi);