Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LethalEcology v0.1.0
LethalEcology.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Unity.Netcode; using UnityEngine; using UnityEngine.AI; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("LethalEcology")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0")] [assembly: AssemblyProduct("LethalEcology")] [assembly: AssemblyTitle("LethalEcology")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace LethalEcology { public static class CultivationVisuals { private static GameObject _root; private static string _levelKey = ""; public static void Refresh(SelectableLevel level) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)level == (Object)null) { return; } MoonState orCreate = EcologyRuntime.GetOrCreate(level); if (orCreate == null) { return; } string text = EcologyRuntime.LevelKey(level); if ((Object)(object)_root != (Object)null && _levelKey != text) { Object.Destroy((Object)(object)_root); _root = null; } if ((Object)(object)_root == (Object)null) { _root = new GameObject("LethalEcology_Cultivation"); _levelKey = text; } foreach (Transform item in _root.transform) { Object.Destroy((Object)(object)((Component)item).gameObject); } StartOfRound instance = StartOfRound.Instance; Vector3 val = (((Object)(object)instance != (Object)null && (Object)(object)instance.shipLandingPosition != (Object)null) ? instance.shipLandingPosition.position : (((Object)(object)GameNetworkManager.Instance != (Object)null && (Object)(object)GameNetworkManager.Instance.localPlayerController != (Object)null) ? ((Component)GameNetworkManager.Instance.localPlayerController).transform.position : Vector3.zero)); int num = 0; foreach (CropState item2 in orCreate.Crops.Values.OrderBy((CropState x) => x.Name)) { for (int num2 = 0; num2 < Mathf.Clamp(item2.Amount, 1, 4); num2++) { float num3 = (float)num * 47f * (MathF.PI / 180f); float num4 = 7f + (float)(num % 3) * 2.2f; Vector3 position = val + new Vector3(Mathf.Cos(num3) * num4, 0.05f, Mathf.Sin(num3) * num4); CreateCrop(item2.Name, position, _root.transform, num); num++; } } } public static void Plant(SelectableLevel level, CropDef def) { Refresh(level); } public static void Clear() { if ((Object)(object)_root != (Object)null) { Object.Destroy((Object)(object)_root); } _root = null; _levelKey = ""; } private static void CreateCrop(string name, Vector3 position, Transform parent, int index) { //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_0192: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01c3: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_01fc: Unknown result type (might be due to invalid IL or missing references) //IL_0208: Unknown result type (might be due to invalid IL or missing references) //IL_020d: Unknown result type (might be due to invalid IL or missing references) //IL_0254: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_025e: Unknown result type (might be due to invalid IL or missing references) string text = EcoCatalog.Normalize(name); Color val = default(Color); ((Color)(ref val))..ctor(0.4f, 0.9f, 0.5f); float num = 0.6f; if (text == EcoCatalog.Normalize("Nectar Bloom")) { ((Color)(ref val))..ctor(1f, 0.55f, 0.2f); num = 0.8f; } else if (text == EcoCatalog.Normalize("Saproot")) { ((Color)(ref val))..ctor(0.35f, 0.7f, 0.3f); num = 1.8f; } else if (text == EcoCatalog.Normalize("Glowcap")) { ((Color)(ref val))..ctor(0.2f, 0.95f, 0.9f); num = 0.5f; } else if (text == EcoCatalog.Normalize("Ropevine")) { ((Color)(ref val))..ctor(0.25f, 0.65f, 0.25f); num = 2f; } else if (text == EcoCatalog.Normalize("Ironroot")) { ((Color)(ref val))..ctor(0.45f, 0.4f, 0.35f); num = 0.9f; } else if (text == EcoCatalog.Normalize("Carrion Bloom")) { ((Color)(ref val))..ctor(0.8f, 0.2f, 0.55f); num = 0.7f; } GameObject obj = GameObject.CreatePrimitive((PrimitiveType)2); ((Object)obj).name = "Crop_" + text + "_" + index; obj.transform.SetParent(parent, false); obj.transform.position = position + Vector3.up * (num * 0.5f); obj.transform.localScale = new Vector3(0.1f, num * 0.5f, 0.1f); Paint(obj, val * 0.6f); GameObject obj2 = GameObject.CreatePrimitive((PrimitiveType)((text == EcoCatalog.Normalize("Ironroot")) ? 3 : 0)); obj2.transform.SetParent(parent, false); obj2.transform.position = position + Vector3.up * (num + 0.15f); obj2.transform.localScale = ((text == EcoCatalog.Normalize("Ropevine")) ? new Vector3(0.18f, 0.5f, 0.18f) : new Vector3(0.55f, 0.4f, 0.55f)); Paint(obj2, val); } private static void Paint(GameObject obj, Color color) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) Collider component = obj.GetComponent<Collider>(); if ((Object)(object)component != (Object)null) { Object.Destroy((Object)(object)component); } Renderer component2 = obj.GetComponent<Renderer>(); if (!((Object)(object)component2 == (Object)null)) { Shader val = Shader.Find("Standard"); Material val2 = (Material)((!((Object)(object)val != (Object)null)) ? ((object)component2.material) : ((object)new Material(val))); val2.color = color; component2.material = val2; } } } public static class EcoData { private static bool _loaded; public static SpeciesDataFile Species { get; private set; } = new SpeciesDataFile(); public static CropDataFile Crops { get; private set; } = new CropDataFile(); public static MoonDataFile Moons { get; private set; } = new MoonDataFile(); public static MechanicsDataFile Mechanics { get; private set; } = new MechanicsDataFile(); public static void Load() { if (!_loaded) { _loaded = true; try { Species = ReadEmbedded<SpeciesDataFile>("LethalEcology.Data.species.json"); Crops = ReadEmbedded<CropDataFile>("LethalEcology.Data.crops.json"); Moons = ReadEmbedded<MoonDataFile>("LethalEcology.Data.moons.json"); Mechanics = ReadEmbedded<MechanicsDataFile>("LethalEcology.Data.mechanics.json"); } catch (Exception ex) { Debug.LogError((object)("[LethalEcology] Failed to load embedded data: " + ex)); } SpeciesDataFile speciesDataFile = LoadOverride<SpeciesDataFile>("species.json"); if (speciesDataFile != null) { Species = speciesDataFile; } CropDataFile cropDataFile = LoadOverride<CropDataFile>("crops.json"); if (cropDataFile != null) { Crops = cropDataFile; } MoonDataFile moonDataFile = LoadOverride<MoonDataFile>("moons.json"); if (moonDataFile != null) { Moons = moonDataFile; } MechanicsDataFile mechanicsDataFile = LoadOverride<MechanicsDataFile>("mechanics.json"); if (mechanicsDataFile != null) { Mechanics = mechanicsDataFile; } if (Species.Species == null) { Species.Species = new List<SpeciesDef>(); } if (Crops.Crops == null) { Crops.Crops = new List<CropDef>(); } if (Moons.Moons == null) { Moons.Moons = new List<MoonDef>(); } if (Mechanics.Mechanics == null) { Mechanics.Mechanics = new List<MechanicDef>(); } } } private static T ReadEmbedded<T>(string name) where T : new() { using Stream stream = typeof(EcoData).Assembly.GetManifestResourceStream(name); if (stream == null) { Debug.LogWarning((object)("[LethalEcology] Missing embedded resource " + name)); return new T(); } using StreamReader streamReader = new StreamReader(stream); T val = JsonConvert.DeserializeObject<T>(streamReader.ReadToEnd()); return (val != null) ? val : new T(); } private static T LoadOverride<T>(string fileName) where T : class { try { string text = Path.Combine(Path.Combine(Paths.PluginPath, "LethalEcology", "Data"), fileName); if (!File.Exists(text)) { return null; } T val = JsonConvert.DeserializeObject<T>(File.ReadAllText(text)); if (val != null) { Debug.Log((object)("[LethalEcology] Loaded override data sheet " + text)); } return val; } catch (Exception ex) { Debug.LogWarning((object)("[LethalEcology] Could not load override " + fileName + ": " + ex.Message)); return null; } } } public static class EcoCatalog { public static readonly Dictionary<string, SpeciesRuntime> Runtime = new Dictionary<string, SpeciesRuntime>(); public static readonly Dictionary<string, SpeciesDef> Authored = new Dictionary<string, SpeciesDef>(); private static bool _loaded; public static bool RuntimeBuilt { get; private set; } public static void Load() { if (_loaded) { return; } _loaded = true; EcoData.Load(); Authored.Clear(); foreach (SpeciesDef item in EcoData.Species.Species) { if (item != null && !string.IsNullOrWhiteSpace(item.Name)) { Authored[Normalize(item.Name)] = item; } } } public static string Normalize(string value) { if (string.IsNullOrWhiteSpace(value)) { return ""; } return Regex.Replace(value.ToLowerInvariant(), "[^a-z0-9]+", ""); } public static void EnsureRuntime() { if (RuntimeBuilt) { return; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.levels == null || instance.levels.Length == 0) { return; } RuntimeBuilt = true; SelectableLevel[] levels = instance.levels; foreach (SelectableLevel val in levels) { if (!((Object)(object)val == (Object)null)) { AddList(val.Enemies, outside: false, inside: true, daytime: false); AddList(val.OutsideEnemies, outside: true, inside: false, daytime: false); AddList(val.DaytimeEnemies, outside: false, inside: false, daytime: true); } } } private static void AddList(List<SpawnableEnemyWithRarity> list, bool outside, bool inside, bool daytime) { if (list == null) { return; } foreach (SpawnableEnemyWithRarity item in list) { if (item != null && !((Object)(object)item.enemyType == (Object)null)) { AddRuntime(item.enemyType, outside, inside, daytime); } } } public static SpeciesRuntime AddRuntime(EnemyType type, bool outside, bool inside, bool daytime) { string text = Normalize(type.enemyName); if (string.IsNullOrEmpty(text)) { text = Normalize(((Object)type).name); } if (Runtime.TryGetValue(text, out var value)) { value.Outside |= outside; value.Inside |= inside; value.Daytime |= daytime; return value; } SpeciesDef def = FindAuthored(type.enemyName) ?? MakeFallback(type, outside, inside, daytime); SpeciesRuntime speciesRuntime = new SpeciesRuntime { Key = text, Name = (string.IsNullOrWhiteSpace(type.enemyName) ? ((Object)type).name : type.enemyName), Def = def, Type = type, Outside = (outside || type.isOutsideEnemy), Inside = (inside || (!type.isOutsideEnemy && !type.isDaytimeEnemy)), Daytime = (daytime || type.isDaytimeEnemy), OriginalMaxCount = Mathf.Max(1, type.MaxCount), OriginalSpawningDisabled = type.spawningDisabled, Power = Mathf.Max(0f, type.PowerLevel) }; Runtime[text] = speciesRuntime; return speciesRuntime; } public static SpeciesDef FindAuthored(string value) { if (string.IsNullOrWhiteSpace(value)) { return null; } Load(); string text = Normalize(value); if (Authored.TryGetValue(text, out var value2)) { return value2; } foreach (SpeciesDef item in EcoData.Species.Species) { if (item == null) { continue; } foreach (string item2 in item.Aliases ?? new List<string>()) { if (Normalize(item2) == text) { return item; } } } SpeciesDef result = null; int num = 0; foreach (SpeciesDef item3 in EcoData.Species.Species) { if (item3 == null) { continue; } List<string> list = new List<string>(); list.Add(item3.Name); list.AddRange(item3.Aliases ?? new List<string>()); foreach (string item4 in list) { string text2 = Normalize(item4); if (text2.Length >= 4) { int num2 = 0; if (text2.StartsWith(text) || text.StartsWith(text2)) { num2 = Mathf.Min(text2.Length, text.Length) + 2; } else if (text2.Contains(text) || text.Contains(text2)) { num2 = Mathf.Min(text2.Length, text.Length); } if (num2 > num) { num = num2; result = item3; } } } } if (num < 4) { return null; } return result; } private static SpeciesDef MakeFallback(EnemyType type, bool outside, bool inside, bool daytime) { string text = (string.IsNullOrWhiteSpace(type.enemyName) ? ((Object)type).name : type.enemyName); string text2 = Normalize(text); float powerLevel = type.PowerLevel; string tier = ((powerLevel >= 8f) ? "Apex" : ((powerLevel >= 3f) ? "Secondary" : ((powerLevel >= 1f) ? "Primary" : "Producer"))); if (text2.Contains("coil") || text2.Contains("jester") || text2.Contains("ghost") || text2.Contains("barber")) { tier = "Anomaly"; } string text3 = (outside ? "Outside" : (daytime ? "Daytime" : "Indoor")); bool flag = !text2.Contains("leviathan") && !text2.Contains("coil") && !text2.Contains("jester") && !text2.Contains("ghost") && !text2.Contains("barber") && type.canBeStunned && powerLevel <= 8f; SpeciesDef speciesDef = new SpeciesDef { Name = text, Tier = tier, Habitat = text3, Role = "Field-classified " + text3.ToLowerInvariant() + " species", Diet = "Unclassified. Recorded from the installed game assembly.", Capturable = flag, PenCost = (flag ? Mathf.Clamp(Mathf.CeilToInt(powerLevel) + 1, 1, 4) : 0), Power = powerLevel, Notes = "This species is present in the installed build but has no hand-authored sheet. Its profile is generated from live enemy data." }; speciesDef.Traits.Add("Runtime-discovered species"); if (outside) { speciesDef.Traits.Add("Outside spawn pool"); } if (inside) { speciesDef.Traits.Add("Indoor spawn pool"); } if (daytime) { speciesDef.Traits.Add("Daytime spawn pool"); } speciesDef.Controls.Add("Use the population and budget tools"); speciesDef.Controls.Add("Capture only if the terminal says it is capturable"); speciesDef.Canon.Add("Generated profile from the installed build"); speciesDef.Mod.Add("Fallback food-web role; add a data-sheet entry to refine it."); return speciesDef; } public static List<SpeciesRuntime> SpeciesForLevel(SelectableLevel level) { EnsureRuntime(); List<SpeciesRuntime> result = new List<SpeciesRuntime>(); HashSet<string> seen = new HashSet<string>(); if ((Object)(object)level == (Object)null) { return result; } AddLevelList(level.Enemies, result, seen); AddLevelList(level.OutsideEnemies, result, seen); AddLevelList(level.DaytimeEnemies, result, seen); return result; } private static void AddLevelList(List<SpawnableEnemyWithRarity> list, List<SpeciesRuntime> result, HashSet<string> seen) { if (list == null) { return; } foreach (SpawnableEnemyWithRarity item in list) { if (item != null && !((Object)(object)item.enemyType == (Object)null)) { string text = Normalize(item.enemyType.enemyName); if (Runtime.TryGetValue(text, out var value) && seen.Add(text)) { result.Add(value); } } } } public static bool HasSpecies(SelectableLevel level, string speciesName) { string key = Normalize(speciesName); return SpeciesForLevel(level).Any((SpeciesRuntime x) => x.Key == key); } public static List<SpeciesRuntime> AllRuntimeOrdered() { EnsureRuntime(); return (from x in Runtime.Values orderby TierOrder(x.Def.Tier), x.Name select x).ToList(); } public static int TierOrder(string tier) { return (tier ?? "").ToLowerInvariant() switch { "producer" => 0, "primary" => 1, "secondary" => 2, "apex" => 3, "parasite" => 4, "anomaly" => 5, "scavenger" => 6, _ => 7, }; } public static List<SpeciesRuntime> CurrentMoonSpecies() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.currentLevel == (Object)null) { return new List<SpeciesRuntime>(); } return SpeciesForLevel(instance.currentLevel); } public static SpeciesRuntime GetRuntime(string keyOrName) { EnsureRuntime(); string key = Normalize(keyOrName); if (Runtime.TryGetValue(key, out var value)) { return value; } foreach (SpeciesRuntime value2 in Runtime.Values) { if (Normalize(value2.Name) == key) { return value2; } if ((value2.Def.Aliases ?? new List<string>()).Any((string a) => Normalize(a) == key)) { return value2; } } return null; } public static MoonDef FindMoon(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return null; } return FindMoon(level.PlanetName) ?? FindMoon("level " + level.levelID); } public static MoonDef FindMoon(string value) { Load(); if (string.IsNullOrWhiteSpace(value)) { return null; } string text = Normalize(value); foreach (MoonDef moon in EcoData.Moons.Moons) { if (moon != null && Normalize(moon.Name) == text) { return moon; } } foreach (MoonDef moon2 in EcoData.Moons.Moons) { if (moon2 == null) { continue; } foreach (string item in moon2.Aliases ?? new List<string>()) { if (Normalize(item) == text) { return moon2; } } } MoonDef result = null; int num = 0; foreach (MoonDef moon3 in EcoData.Moons.Moons) { if (moon3 == null) { continue; } List<string> list = new List<string>(); list.Add(moon3.Name); list.AddRange(moon3.Aliases ?? new List<string>()); foreach (string item2 in list) { string text2 = Normalize(item2); if (text2.Length >= 3) { int num2 = 0; if (text2.StartsWith(text) || text.StartsWith(text2)) { num2 = Mathf.Min(text2.Length, text.Length) + 2; } else if (text2.Contains(text) || text.Contains(text2)) { num2 = Mathf.Min(text2.Length, text.Length); } if (num2 > num) { num = num2; result = moon3; } } } } if (num < 3) { return null; } return result; } public static MoonDef MoonForLevelOrFallback(SelectableLevel level) { MoonDef moonDef = FindMoon(level); if (moonDef != null) { return moonDef; } if ((Object)(object)level == (Object)null) { return new MoonDef(); } return new MoonDef { Name = (string.IsNullOrWhiteSpace(level.PlanetName) ? ("Level " + level.levelID) : level.PlanetName), Identity = "Runtime-discovered moon", Biome = "Unclassified", StartingFood = 55f, StartingStability = 2, Notes = "No hand-authored moon identity exists for this level. The mod uses its actual enemy pool and vanilla power bases.", Project = "Stabilize the food web and record what the moon supports." }; } public static CropDef FindCrop(string value) { Load(); if (string.IsNullOrWhiteSpace(value)) { return null; } string key = Normalize(value); foreach (CropDef crop in EcoData.Crops.Crops) { if (crop == null) { continue; } if (Normalize(crop.Name) == key) { return crop; } foreach (string item in crop.Aliases ?? new List<string>()) { if (Normalize(item) == key) { return crop; } } } return EcoData.Crops.Crops.FirstOrDefault((CropDef c) => c != null && (Normalize(c.Name).Contains(key) || key.Contains(Normalize(c.Name)))); } public static MechanicDef FindMechanic(string value) { Load(); if (string.IsNullOrWhiteSpace(value)) { return null; } string key = Normalize(value); foreach (MechanicDef mechanic in EcoData.Mechanics.Mechanics) { if (mechanic == null) { continue; } if (Normalize(mechanic.Name) == key) { return mechanic; } foreach (string item in mechanic.Aliases ?? new List<string>()) { if (Normalize(item) == key) { return mechanic; } } } return EcoData.Mechanics.Mechanics.FirstOrDefault((MechanicDef c) => c != null && (Normalize(c.Name).Contains(key) || key.Contains(Normalize(c.Name)))); } public static List<SelectableLevel> AllLevels() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.levels == null) { return new List<SelectableLevel>(); } return (from x in instance.levels where (Object)(object)x != (Object)null orderby x.levelID select x).ToList(); } public static List<CropDef> CropsForMoon(SelectableLevel level) { List<string> list = MoonForLevelOrFallback(level).Crops ?? new List<string>(); if (list.Count == 0) { return EcoData.Crops.Crops; } List<CropDef> list2 = new List<CropDef>(); foreach (string item in list) { CropDef cropDef = FindCrop(item); if (cropDef != null && !list2.Contains(cropDef)) { list2.Add(cropDef); } } return list2; } public static bool TierIs(string tier, string expected) { return string.Equals(tier ?? "", expected, StringComparison.OrdinalIgnoreCase); } } public static class EcologyRuntime { private static readonly Dictionary<int, LevelBase> LevelBases = new Dictionary<int, LevelBase>(); private static readonly Dictionary<SpawnableEnemyWithRarity, int> RarityBases = new Dictionary<SpawnableEnemyWithRarity, int>(); private static readonly HashSet<SpawnableEnemyWithRarity> IntroducedItems = new HashSet<SpawnableEnemyWithRarity>(); private static readonly HashSet<int> CountedDeaths = new HashSet<int>(); public static EcologySave Data { get; private set; } public static bool Loaded { get; private set; } public static void Initialize() { EcoCatalog.Load(); EnsureLoaded(); } public static void EnsureLoaded() { string text = EcologyStore.ResolveSaveName(); if (!Loaded || EcologyStore.CurrentSaveName != text) { Data = EcologyStore.Load(text); Loaded = true; EcoCatalog.EnsureRuntime(); } if (Data == null) { Data = new EcologySave(); } if (Data.Moons == null) { Data.Moons = new Dictionary<string, MoonState>(); } if (Data.Hold == null) { Data.Hold = new HoldState(); } if (Data.Hold.Specimens == null) { Data.Hold.Specimens = new List<SpecimenState>(); } if (Plugin.HoldPens != null) { Data.Hold.Capacity = Mathf.Clamp(Plugin.HoldPens.Value, 1, 8); } } public static void Save() { EnsureLoaded(); EcologyStore.Save(Data); } public static void ResetAll() { string text = EcologyStore.CurrentSaveName; if (string.IsNullOrWhiteSpace(text)) { text = EcologyStore.ResolveSaveName(); } Data = new EcologySave { SaveName = text }; Loaded = true; CountedDeaths.Clear(); SpecimenHold.Clear(); CultivationVisuals.Clear(); TerritoryAnchors.Clear(); EcologyStore.Save(Data); } public static string LevelKey(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return "unknown"; } return level.levelID + "|" + EcoCatalog.Normalize(level.PlanetName); } public static MoonState GetOrCreate(SelectableLevel level) { EnsureLoaded(); if ((Object)(object)level == (Object)null) { return null; } string key = LevelKey(level); if (!Data.Moons.TryGetValue(key, out var value)) { value = new MoonState { LevelId = level.levelID, LevelName = level.PlanetName }; Data.Moons[key] = value; } if (value.Species == null) { value.Species = new Dictionary<string, SpeciesState>(); } if (value.Territories == null) { value.Territories = new Dictionary<string, TerritoryState>(); } if (value.Crops == null) { value.Crops = new Dictionary<string, CropState>(); } if (value.Log == null) { value.Log = new List<string>(); } if (!value.Initialized) { EcoCatalog.EnsureRuntime(); List<SpeciesRuntime> list = EcoCatalog.SpeciesForLevel(level); if (list.Count > 0 || EcoCatalog.Runtime.Count > 0) { InitializeMoon(value, level, list); } } return value; } public static MoonState CurrentMoon() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.currentLevel == (Object)null) { return null; } return GetOrCreate(instance.currentLevel); } private static void InitializeMoon(MoonState state, SelectableLevel level, List<SpeciesRuntime> pool) { MoonDef moonDef = EcoCatalog.MoonForLevelOrFallback(level); state.LevelName = level.PlanetName; state.LevelId = level.levelID; state.Food = Mathf.Clamp(moonDef.StartingFood, 0f, 100f); state.Stability = Mathf.Clamp(moonDef.StartingStability, 0, 3); state.Seed = Math.Abs((level.PlanetName ?? "moon").GetHashCode()) + level.levelID * 7919; state.Cycle = 0; state.Visits = 0; state.Species.Clear(); state.Territories.Clear(); state.Crops.Clear(); state.Log.Clear(); foreach (SpeciesRuntime item in pool) { state.Species[item.Key] = new SpeciesState { Key = item.Key, Name = item.Name, Population = InitialPopulation(item), Present = true, Introduced = false, Health = 100f }; } bool flag = pool.Any((SpeciesRuntime x) => x.Key == EcoCatalog.Normalize("Old Bird")); bool flag2 = pool.Any((SpeciesRuntime x) => x.Key == EcoCatalog.Normalize("Earth Leviathan")); int num = moonDef.Relays; int num2 = moonDef.Burrows; if (flag && num <= 0) { num = 1; } if (flag2 && num2 <= 0) { num2 = 1; } for (int num3 = 0; num3 < num; num3++) { string text = "relay" + (num3 + 1); state.Territories[text] = new TerritoryState { Id = text, Type = "Relay", ResidentKey = (flag ? EcoCatalog.Normalize("Old Bird") : ""), ResidentName = (flag ? "Old Bird" : ""), Occupied = flag, PowerCost = ((Plugin.ApexPower != null) ? Plugin.ApexPower.Value : 12f), ReclaimIn = 0 }; } for (int num4 = 0; num4 < num2; num4++) { string text2 = "burrow" + (num4 + 1); state.Territories[text2] = new TerritoryState { Id = text2, Type = "Burrow", ResidentKey = (flag2 ? EcoCatalog.Normalize("Earth Leviathan") : ""), ResidentName = (flag2 ? "Earth Leviathan" : ""), Occupied = flag2, PowerCost = ((Plugin.ApexPower != null) ? Plugin.ApexPower.Value : 12f), ReclaimIn = 0 }; } state.Initialized = true; AddLog(state, "Ecology survey initialized."); Save(); } private static float InitialPopulation(SpeciesRuntime rt) { float num = (rt.Def.Tier ?? "").ToLowerInvariant() switch { "apex" => 34f, "secondary" => 42f, "parasite" => 24f, "anomaly" => 18f, "producer" => 58f, _ => 48f, }; if (rt.Key == EcoCatalog.Normalize("Old Bird") || rt.Key == EcoCatalog.Normalize("Earth Leviathan")) { num = 40f; } if (rt.Daytime) { num += 5f; } return Mathf.Clamp(num, 8f, 80f); } public static void OnArriveAtLevel() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || !((NetworkBehaviour)instance).IsServer || (Object)(object)instance.currentLevel == (Object)null) { return; } EnsureLoaded(); MoonState orCreate = GetOrCreate(instance.currentLevel); if (orCreate != null) { orCreate.Visits++; orCreate.Cycle++; if (orCreate.Visits > 1) { EcoSim.Advance(orCreate, instance.currentLevel); AddLog(orCreate, "Cycle " + orCreate.Cycle + ": field survey updated."); } else { AddLog(orCreate, "First survey of " + orCreate.LevelName + "."); } ApplyToLevel(instance.currentLevel); Save(); } } public static void OnLevelChanged() { StartOfRound instance = StartOfRound.Instance; if (!((Object)(object)instance == (Object)null) && ((NetworkBehaviour)instance).IsServer && !((Object)(object)instance.currentLevel == (Object)null)) { EnsureLoaded(); if (GetOrCreate(instance.currentLevel) != null) { ApplyToLevel(instance.currentLevel); Save(); } } } public static LevelBase GetLevelBase(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return new LevelBase(); } int instanceID = ((Object)level).GetInstanceID(); if (!LevelBases.TryGetValue(instanceID, out var value)) { value = new LevelBase { MaxInside = level.maxEnemyPowerCount, MaxOutside = level.maxOutsideEnemyPowerCount, MaxDaytime = level.maxDaytimeEnemyPowerCount }; LevelBases[instanceID] = value; } return value; } public static BudgetResult ComputeBudget(SelectableLevel level, MoonState state) { LevelBase levelBase = GetLevelBase(level); if (state == null) { state = new MoonState(); } BudgetResult budgetResult = new BudgetResult { OutsideBase = levelBase.MaxOutside, InsideBase = levelBase.MaxInside, DaytimeBase = levelBase.MaxDaytime, Food = Mathf.RoundToInt(state.Food), Stability = state.Stability }; budgetResult.FoodModifier = Mathf.Clamp(Mathf.FloorToInt((state.Food - 50f) / 10f), -5, 10); budgetResult.StabilityModifier = StabilityModifier(state.Stability); if (state.Territories != null) { budgetResult.ApexPower = Mathf.RoundToInt(state.Territories.Values.Where((TerritoryState t) => !t.Destroyed && t.Occupied).Sum((TerritoryState t) => Mathf.Max(0f, t.PowerCost))); } budgetResult.OutsideBudget = Mathf.Max(0, budgetResult.OutsideBase + budgetResult.FoodModifier + budgetResult.StabilityModifier - budgetResult.ApexPower); budgetResult.InsideBudget = Mathf.Max(0, budgetResult.InsideBase + budgetResult.FoodModifier + budgetResult.StabilityModifier); budgetResult.DaytimeBudget = Mathf.Max(0, budgetResult.DaytimeBase + budgetResult.FoodModifier + budgetResult.StabilityModifier); return budgetResult; } public static BudgetResult CurrentBudget() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.currentLevel == (Object)null) { return new BudgetResult { Food = 55, Stability = 2 }; } return ComputeBudget(instance.currentLevel, GetOrCreate(instance.currentLevel)); } public static BudgetResult BudgetFor(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return new BudgetResult { Food = 55, Stability = 2 }; } return ComputeBudget(level, GetOrCreate(level)); } public static int StabilityModifier(int stability) { return stability switch { 0 => -4, 1 => -2, 2 => 0, _ => 2, }; } public static string StabilityName(int stability) { return stability switch { 0 => "Collapsing", 1 => "Stressed", 2 => "Stable", _ => "Thriving", }; } public static int SpawnCap(MoonState state, SpeciesRuntime rt) { if (state == null || rt == null) { return 0; } if (!state.Species.TryGetValue(rt.Key, out var value)) { return 0; } if (!value.Present || value.Population < 10f) { return 0; } string territoryType = TerritoryType(rt.Key); if (territoryType != null) { int num = state.Territories.Values.Count((TerritoryState t) => !t.Destroyed && t.Occupied && t.Type == territoryType && string.Equals(t.ResidentKey, rt.Key, StringComparison.OrdinalIgnoreCase)); return Mathf.Max(0, num); } return Mathf.Clamp(Mathf.CeilToInt(value.Population / 20f), 0, Mathf.Max(1, rt.OriginalMaxCount)); } public static string TerritoryType(string speciesKey) { if (speciesKey == EcoCatalog.Normalize("Old Bird")) { return "Relay"; } if (speciesKey == EcoCatalog.Normalize("Earth Leviathan")) { return "Burrow"; } return null; } public static void ApplyToLevel(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return; } EcoCatalog.EnsureRuntime(); MoonState orCreate = GetOrCreate(level); if (orCreate == null) { return; } EnsureStateForPool(level, orCreate); EnsureIntroducedPools(level, orCreate); LevelBase levelBase = GetLevelBase(level); BudgetResult budgetResult = ComputeBudget(level, orCreate); if (Plugin.Profile == null || Plugin.Profile.Value != EcoProfile.FullEcology) { level.maxEnemyPowerCount = levelBase.MaxInside; level.maxOutsideEnemyPowerCount = levelBase.MaxOutside; level.maxDaytimeEnemyPowerCount = levelBase.MaxDaytime; RestoreCanonLists(level); return; } level.maxEnemyPowerCount = budgetResult.InsideBudget; level.maxOutsideEnemyPowerCount = budgetResult.OutsideBudget; level.maxDaytimeEnemyPowerCount = budgetResult.DaytimeBudget; ApplyList(level.Enemies, orCreate); ApplyList(level.OutsideEnemies, orCreate); ApplyList(level.DaytimeEnemies, orCreate); ApplyTypeFields(level, orCreate); RoundManager instance = RoundManager.Instance; if ((Object)(object)instance != (Object)null && ((NetworkBehaviour)instance).IsServer) { instance.currentMaxInsidePower = budgetResult.InsideBudget; instance.currentMaxOutsidePower = budgetResult.OutsideBudget; } } private static void EnsureStateForPool(SelectableLevel level, MoonState state) { foreach (SpeciesRuntime item in EcoCatalog.SpeciesForLevel(level)) { if (!state.Species.ContainsKey(item.Key)) { state.Species[item.Key] = new SpeciesState { Key = item.Key, Name = item.Name, Population = InitialPopulation(item), Present = true }; } } } private static void EnsureIntroducedPools(SelectableLevel level, MoonState state) { foreach (SpeciesState value in state.Species.Values) { if (value.Introduced && value.Present && !(value.Population <= 0f)) { SpeciesRuntime runtime = EcoCatalog.GetRuntime(value.Key); if (runtime != null && !IsInLevel(level, value.Key)) { AddToPool(level, runtime); } } } } private static bool IsInLevel(SelectableLevel level, string key) { return EcoCatalog.SpeciesForLevel(level).Any((SpeciesRuntime x) => x.Key == key); } private static void AddToPool(SelectableLevel level, SpeciesRuntime rt) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected O, but got Unknown SpawnableEnemyWithRarity item = new SpawnableEnemyWithRarity(rt.Type, 1); if (rt.Outside) { if (level.OutsideEnemies == null) { level.OutsideEnemies = new List<SpawnableEnemyWithRarity>(); } level.OutsideEnemies.Add(item); } else if (rt.Daytime) { if (level.DaytimeEnemies == null) { level.DaytimeEnemies = new List<SpawnableEnemyWithRarity>(); } level.DaytimeEnemies.Add(item); } else { if (level.Enemies == null) { level.Enemies = new List<SpawnableEnemyWithRarity>(); } level.Enemies.Add(item); } IntroducedItems.Add(item); } private static void ApplyList(List<SpawnableEnemyWithRarity> list, MoonState state) { if (list == null) { return; } foreach (SpawnableEnemyWithRarity item in list) { if (item == null || (Object)(object)item.enemyType == (Object)null) { continue; } string text = EcoCatalog.Normalize(item.enemyType.enemyName); SpeciesRuntime runtime = EcoCatalog.GetRuntime(text); if (runtime != null) { if (!state.Species.TryGetValue(text, out var value)) { value = new SpeciesState { Key = text, Name = runtime.Name, Population = 20f, Present = false }; state.Species[text] = value; } if (!RarityBases.ContainsKey(item)) { RarityBases[item] = item.rarity; } int num = (value.LastCap = SpawnCap(state, runtime)); bool flag = value.Present && value.Population > 0f && num > 0; item.rarity = (flag ? RarityFor(value, runtime, num) : 0); } } } private static void ApplyTypeFields(SelectableLevel level, MoonState state) { HashSet<EnemyType> hashSet = new HashSet<EnemyType>(); foreach (SpeciesRuntime item in EcoCatalog.SpeciesForLevel(level)) { if (!((Object)(object)item.Type == (Object)null) && hashSet.Add(item.Type)) { if (!state.Species.TryGetValue(item.Key, out var value)) { value = new SpeciesState { Key = item.Key, Name = item.Name, Population = 0f, Present = false }; state.Species[item.Key] = value; } int num = SpawnCap(state, item); if (!value.Present || !(value.Population > 0f) || num <= 0) { item.Type.MaxCount = 0; item.Type.spawningDisabled = true; } else { int maxCount = ((TerritoryType(item.Key) != null) ? Mathf.Clamp(num, 1, 3) : Mathf.Clamp(num, 1, Mathf.Max(1, item.OriginalMaxCount))); item.Type.MaxCount = maxCount; item.Type.spawningDisabled = false; } } } } private static int RarityFor(SpeciesState species, SpeciesRuntime rt, int cap) { if (cap <= 0) { return 0; } if (TerritoryType(rt.Key) != null) { return 200; } return Mathf.Clamp(Mathf.RoundToInt(species.Population * 1.2f), 1, 240); } private static void RestoreCanonLists(SelectableLevel level) { RemoveIntroduced(level.Enemies); RemoveIntroduced(level.OutsideEnemies); RemoveIntroduced(level.DaytimeEnemies); RestoreList(level.Enemies); RestoreList(level.OutsideEnemies); RestoreList(level.DaytimeEnemies); } private static void RemoveIntroduced(List<SpawnableEnemyWithRarity> list) { list?.RemoveAll((SpawnableEnemyWithRarity item) => item != null && IntroducedItems.Contains(item)); } private static void RestoreList(List<SpawnableEnemyWithRarity> list) { if (list == null) { return; } foreach (SpawnableEnemyWithRarity item in list) { if (item == null) { continue; } if (!RarityBases.ContainsKey(item)) { RarityBases[item] = item.rarity; } item.rarity = RarityBases[item]; if ((Object)(object)item.enemyType != (Object)null) { SpeciesRuntime runtime = EcoCatalog.GetRuntime(item.enemyType.enemyName); if (runtime != null) { item.enemyType.MaxCount = runtime.OriginalMaxCount; item.enemyType.spawningDisabled = runtime.OriginalSpawningDisabled; } } } } public static void RefreshRuntimeCaps() { StartOfRound instance = StartOfRound.Instance; if (!((Object)(object)instance == (Object)null) && ((NetworkBehaviour)instance).IsServer && !((Object)(object)instance.currentLevel == (Object)null) && Plugin.Profile != null && Plugin.Profile.Value == EcoProfile.FullEcology) { BudgetResult budgetResult = BudgetFor(instance.currentLevel); RoundManager instance2 = RoundManager.Instance; if ((Object)(object)instance2 != (Object)null) { instance2.currentMaxInsidePower = budgetResult.InsideBudget; instance2.currentMaxOutsidePower = budgetResult.OutsideBudget; } } } public static SpeciesState GetSpeciesState(MoonState state, string keyOrName) { if (state == null) { return null; } string text = EcoCatalog.Normalize(keyOrName); if (state.Species.TryGetValue(text, out var value)) { return value; } foreach (SpeciesState value2 in state.Species.Values) { if (EcoCatalog.Normalize(value2.Name) == text) { return value2; } if (EcoCatalog.Normalize(value2.Key) == text) { return value2; } } return null; } public static void AddLog(MoonState state, string text) { if (state != null) { if (state.Log == null) { state.Log = new List<string>(); } state.Log.Add("[" + DateTime.Now.ToString("HH:mm") + "] " + text); while (state.Log.Count > 24) { state.Log.RemoveAt(0); } } } public static void OnEnemyKilled(EnemyAI enemy) { if ((Object)(object)enemy == (Object)null || (Object)(object)enemy.enemyType == (Object)null || !((NetworkBehaviour)enemy).IsServer) { return; } int instanceID = ((Object)enemy).GetInstanceID(); if (!CountedDeaths.Add(instanceID)) { return; } EnsureLoaded(); SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); if ((Object)(object)val == (Object)null) { return; } MoonState orCreate = GetOrCreate(val); SpeciesRuntime rt = EcoCatalog.GetRuntime(enemy.enemyType.enemyName); if (rt == null || !orCreate.Species.TryGetValue(rt.Key, out var value)) { return; } string territoryType = TerritoryType(rt.Key); if (territoryType != null) { TerritoryState territoryState = orCreate.Territories.Values.FirstOrDefault((TerritoryState t) => !t.Destroyed && t.Occupied && t.Type == territoryType && string.Equals(t.ResidentKey, rt.Key, StringComparison.OrdinalIgnoreCase)); if (territoryState != null) { territoryState.Occupied = false; territoryState.ResidentKey = ""; territoryState.ResidentName = ""; territoryState.ReclaimIn = 3; } } float num = Mathf.Clamp(100f / (float)Mathf.Max(1, rt.OriginalMaxCount), 6f, 25f); value.Population = Mathf.Max(0f, value.Population - num); value.Killed++; AddLog(orCreate, rt.Name + " killed. Population -" + Mathf.RoundToInt(num) + "."); Save(); } public static bool TryCapture(EnemyAI enemy, out string message) { message = ""; if ((Object)(object)enemy == (Object)null || (Object)(object)enemy.enemyType == (Object)null) { message = "No creature in range."; return false; } EnsureLoaded(); SpeciesRuntime rt = EcoCatalog.GetRuntime(enemy.enemyType.enemyName); if (rt == null) { message = "Unknown species."; return false; } if (!rt.Def.Capturable || rt.Def.PenCost <= 0) { message = rt.Name + " cannot be captured alive."; return false; } HoldState hold = Data.Hold; hold.Capacity = ((Plugin.HoldPens != null) ? Mathf.Clamp(Plugin.HoldPens.Value, 1, 8) : 4); int num = hold.Specimens.Sum((SpecimenState s) => s.PenCost); if (num + rt.Def.PenCost > hold.Capacity) { message = "Not enough pen space. " + rt.Name + " needs " + rt.Def.PenCost + " pen(s); " + (hold.Capacity - num) + " free."; return false; } SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); MoonState moonState = (((Object)(object)val != (Object)null) ? GetOrCreate(val) : null); if (moonState != null && moonState.Species.TryGetValue(rt.Key, out var value)) { string territoryType = TerritoryType(rt.Key); if (territoryType != null) { TerritoryState territoryState = moonState.Territories.Values.FirstOrDefault((TerritoryState t) => !t.Destroyed && t.Occupied && t.Type == territoryType && string.Equals(t.ResidentKey, rt.Key, StringComparison.OrdinalIgnoreCase)); if (territoryState != null) { territoryState.Occupied = false; territoryState.ResidentKey = ""; territoryState.ResidentName = ""; territoryState.ReclaimIn = 3; } } value.Population = Mathf.Max(0f, value.Population - Mathf.Clamp(100f / (float)Mathf.Max(1, rt.OriginalMaxCount), 5f, 20f)); value.Captured++; } SpecimenState specimenState = new SpecimenState { Id = "spec_" + Guid.NewGuid().ToString("N").Substring(0, 8), Key = rt.Key, Name = rt.Name, PenCost = rt.Def.PenCost, Health = 100f, Stress = 12f, CapturedCycle = (moonState?.Cycle ?? Data.Cycle), SourceMoon = (((Object)(object)val != (Object)null) ? val.PlanetName : "unknown"), Juvenile = (rt.Key == EcoCatalog.Normalize("Giant Sapsucker") && Random.value < 0.35f), Growth = 0f, Hive = (rt.Key == EcoCatalog.Normalize("Circuit Bee")), Pen = NextPen(hold, rt.Def.PenCost) }; hold.Specimens.Add(specimenState); if (moonState != null) { AddLog(moonState, rt.Name + " captured and stored in pen " + specimenState.Pen + "."); } Save(); message = rt.Name + " captured. Pen " + specimenState.Pen + "."; return true; } private static int NextPen(HoldState hold, int cost) { HashSet<int> hashSet = new HashSet<int>(); foreach (SpecimenState specimen in hold.Specimens) { for (int i = 0; i < Mathf.Max(1, specimen.PenCost); i++) { hashSet.Add(specimen.Pen + i); } } cost = Mathf.Max(1, cost); for (int j = 1; j + cost - 1 <= hold.Capacity; j++) { bool flag = true; for (int k = 0; k < cost; k++) { if (hashSet.Contains(j + k)) { flag = false; break; } } if (flag) { return j; } } return Mathf.Max(1, hold.Capacity); } public static bool Release(int pen, out string message) { message = ""; EnsureLoaded(); HoldState hold = Data.Hold; SpecimenState specimenState = hold.Specimens.FirstOrDefault((SpecimenState s) => s.Pen == pen); if (specimenState == null) { specimenState = hold.Specimens.FirstOrDefault((SpecimenState s) => s.Id == pen.ToString()); } if (specimenState == null) { message = "No specimen in pen " + pen + "."; return false; } SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); if ((Object)(object)val == (Object)null) { message = "You must be on a moon to release a specimen."; return false; } SpeciesRuntime runtime = EcoCatalog.GetRuntime(specimenState.Key); if (runtime == null || (Object)(object)runtime.Type == (Object)null) { message = "The species record is missing; release aborted."; return false; } MoonState orCreate = GetOrCreate(val); bool flag = IsInLevel(val, specimenState.Key); if (!orCreate.Species.TryGetValue(specimenState.Key, out var value)) { value = new SpeciesState { Key = specimenState.Key, Name = specimenState.Name, Population = 0f, Present = true, Introduced = !flag }; orCreate.Species[specimenState.Key] = value; } string territoryType = TerritoryType(specimenState.Key); TerritoryState territoryState = null; if (territoryType != null) { territoryState = orCreate.Territories.Values.FirstOrDefault((TerritoryState t) => !t.Destroyed && !t.Occupied && t.Type == territoryType); if (territoryState == null) { message = "No empty " + ((territoryType == "Relay") ? "Relay Spire" : "Burrow") + " is available on " + val.PlanetName + ". Build or clear one first."; return false; } } if (!flag) { AddToPool(val, runtime); } bool flag2 = orCreate.Food < 25f && Random.value < 0.55f; value.Present = true; if (!flag2) { value.Population = Mathf.Clamp(value.Population + ((territoryState != null) ? 35f : 18f), 0f, 100f); if (!flag) { value.Introduced = true; } if (territoryState != null) { territoryState.Occupied = true; territoryState.ResidentKey = specimenState.Key; territoryState.ResidentName = specimenState.Name; territoryState.ReclaimIn = 0; } } else { value.Population = Mathf.Max(0f, value.Population - 5f); if (territoryState != null) { territoryState.Occupied = false; territoryState.ResidentKey = ""; territoryState.ResidentName = ""; territoryState.ReclaimIn = 0; } } hold.Specimens.Remove(specimenState); AddLog(orCreate, specimenState.Name + (flag2 ? " release failed; the habitat rejected it." : (" released from pen " + pen + "."))); ApplyToLevel(val); Save(); message = (flag2 ? (specimenState.Name + " released, but the habitat rejected it. Food is too low.") : (specimenState.Name + " released into " + val.PlanetName + ".")); return true; } } public static class EcoSim { public static void Advance(MoonState state, SelectableLevel level) { if (state == null) { return; } Random random = new Random(state.Seed + state.Cycle * 7919 + state.Visits * 104729); EcoCatalog.MoonForLevelOrFallback(level); float num = 0f; float num2 = 0f; float num3 = 0f; bool pollinated = HasPollinator(state); foreach (CropState item in state.Crops.Values.ToList()) { if (item.Amount <= 0) { state.Crops.Remove(item.Key); continue; } CropDef cropDef = EcoCatalog.FindCrop(item.Name); if (cropDef == null) { continue; } if (EcoCatalog.Normalize(cropDef.Name) == EcoCatalog.Normalize("Nectar Bloom")) { item.Pollinated = pollinated; } float num4 = ((state.Stability >= 2) ? 2f : (-2f)); if (item.Pollinated) { num4 += 1f; } if (HasSpecies(state, "Spore Lizard", 10f) && EcoCatalog.Normalize(cropDef.Name) == EcoCatalog.Normalize("Glowcap")) { num4 += 2f; } item.Health = Mathf.Clamp(item.Health + num4, 0f, 100f); float num5 = 0f; if (EcoCatalog.Normalize(cropDef.Name) == EcoCatalog.Normalize("Carrion Bloom")) { num5 += 3.5f; } if (EcoCatalog.Normalize(cropDef.Name) == EcoCatalog.Normalize("Nectar Bloom")) { num5 += 0.4f; } if (HasSpecies(state, "Vain Shroud", 10f)) { num5 += 1.5f; } if (HasSpecies(state, "Cadaver Bloom", 10f)) { num5 += 1f; } if (EcoCatalog.Normalize(cropDef.Name) == EcoCatalog.Normalize("Glowcap")) { num5 -= 2f; } if (HasSpecies(state, "Spore Lizard", 10f)) { num5 -= 1f; } if (state.Stability <= 1) { num5 += 1f; } item.Blight = Mathf.Clamp(item.Blight + num5 + (float)(random.NextDouble() * 0.8 - 0.4), 0f, 100f); if (item.Blight > 60f) { item.Health = Mathf.Max(0f, item.Health - 4f); } if (item.Blight > 85f) { item.Amount = 0; } if (item.Amount <= 0) { state.Crops.Remove(item.Key); continue; } if (state.Cycle > item.PlantedCycle && item.Amount < 3 && item.Health >= 70f && item.Blight < 40f && random.NextDouble() < 0.35) { item.Amount++; EcologyRuntime.AddLog(state, item.Name + " spread to patch " + item.Amount + "."); } float num6 = cropDef.FoodPerCycle * (float)item.Amount * (item.Pollinated ? 1f : 0.5f) * (item.Health / 100f); num2 += num6; num3 += cropDef.StabilityPerCycle * (item.Health / 100f); num += item.Blight; } float num7 = 0f; float num8 = 2.6f; foreach (SpeciesState value2 in state.Species.Values) { if (value2.Present && !(value2.Population <= 0f)) { string tier = TierOf(state, value2.Key); if (EcoCatalog.TierIs(tier, "Producer")) { num8 += value2.Population * 0.026f; } else if (EcoCatalog.TierIs(tier, "Apex")) { num7 += value2.Population * 0.004f; } else if (!EcoCatalog.TierIs(tier, "Anomaly")) { num7 += value2.Population * 0.009f; } } } state.Food = Mathf.Clamp(state.Food + num8 + num2 - num7 + (float)(random.NextDouble() * 1.6 - 0.8), 0f, 100f); foreach (SpeciesState item2 in state.Species.Values.ToList()) { SpeciesRuntime rt = EcoCatalog.GetRuntime(item2.Key); if (rt == null) { continue; } string text = (rt.Def.Tier ?? "").ToLowerInvariant(); float num9 = AveragePopulation(state, rt.Def.Preys); float num10 = AveragePopulation(state, rt.Def.Predators); float num11 = AverageHostPopulation(state, rt.Def.Hosts); float num12 = (state.Food - 48f) / 18f; float num13 = 0f; num13 = text switch { "producer" => num12 * 0.9f + 0.2f, "primary" => num12 * 0.8f - num10 / 60f + 0.1f, "secondary" => (num9 - 40f) / 22f - num10 / 70f, "apex" => (num9 - 45f) / 24f + 0.05f, "parasite" => (num11 - 35f) / 24f, "scavenger" => num12 * 0.5f, "anomaly" => 0f, _ => num12 * 0.4f, }; if (rt.Outside && state.Food < 20f) { num13 -= 2f; } if (rt.Daytime && state.Food > 65f) { num13 += 0.5f; } num13 += (float)(random.NextDouble() * 2.4 - 1.2); string territoryType = EcologyRuntime.TerritoryType(rt.Key); if (territoryType != null) { int num14 = state.Territories.Values.Count((TerritoryState t) => !t.Destroyed && t.Occupied && t.Type == territoryType && string.Equals(t.ResidentKey, rt.Key, StringComparison.OrdinalIgnoreCase)); if (num14 <= 0) { item2.Population = Mathf.Max(0f, item2.Population - 4f); } else { item2.Population = Mathf.Clamp((float)num14 * 32f + (float)random.NextDouble() * 8f, 20f, 100f); } } else { item2.Population = Mathf.Clamp(item2.Population + num13, 0f, 100f); } if (item2.Population < 1f) { item2.Population = 0f; if (state.Food > 35f && !EcoCatalog.TierIs(text, "anomaly") && random.NextDouble() < 0.07) { item2.Population = 5f; EcologyRuntime.AddLog(state, item2.Name + " immigrated back into the moon."); } } item2.Health = Mathf.Clamp(100f - Mathf.Abs(state.Food - 55f) * 0.4f, 10f, 100f); } foreach (TerritoryState territory in state.Territories.Values) { if (territory.Destroyed) { continue; } if (territory.Occupied) { SpeciesState speciesState = state.Species.Values.FirstOrDefault((SpeciesState s) => s.Key == territory.ResidentKey); if (speciesState == null || speciesState.Population <= 0f) { territory.Occupied = false; territory.ResidentKey = ""; territory.ResidentName = ""; territory.ReclaimIn = 3; } } else if (territory.ReclaimIn > 0) { territory.ReclaimIn--; } else if (state.Food >= 30f && state.Stability >= 1 && random.NextDouble() < 0.45) { string text2 = ((territory.Type == "Relay") ? "Old Bird" : "Earth Leviathan"); string text3 = EcoCatalog.Normalize(text2); if (state.Species.TryGetValue(text3, out var value) && value.Present) { territory.Occupied = true; territory.ResidentKey = text3; territory.ResidentName = text2; EcologyRuntime.AddLog(state, text2 + " reclaimed " + TerritoryLabel(territory) + "."); } } } int num15 = state.Species.Values.Count((SpeciesState s) => s.Present && s.Population >= 10f); float num16 = state.Species.Values.Where((SpeciesState s) => s.Present && s.Population >= 10f).Sum(delegate(SpeciesState s) { SpeciesRuntime runtime = EcoCatalog.GetRuntime(s.Key); return (runtime == null || !EcoCatalog.TierIs(runtime.Def.Tier, "Apex")) ? 0f : Mathf.Clamp(runtime.Power, 1f, 12f); }); float num17 = ((state.Crops.Count > 0) ? (num / (float)state.Crops.Count) : 0f); float num18 = state.Food * 0.55f + (float)num15 * 6f + num3 * 2f - num16 * 1.2f; if (num17 > 55f) { num18 -= 12f; } if (state.Food < 20f) { num18 = Mathf.Min(num18, 24f); } else if (state.Food < 40f) { num18 = Mathf.Min(num18, 49f); } int num19 = ((!(num18 < 30f)) ? ((num18 < 50f) ? 1 : ((!(num18 < 78f)) ? 3 : 2)) : 0); if (num19 != state.Stability) { EcologyRuntime.AddLog(state, "Stability changed from " + EcologyRuntime.StabilityName(state.Stability) + " to " + EcologyRuntime.StabilityName(num19) + "."); state.Stability = num19; } UpdateHold(state, random); if (state.Log.Count > 24) { state.Log.RemoveRange(0, state.Log.Count - 24); } } private static void UpdateHold(MoonState state, Random rng) { HoldState hold = EcologyRuntime.Data.Hold; if (hold == null || hold.Specimens == null) { return; } bool flag = hold.Specimens.Sum((SpecimenState s) => s.PenCost) > hold.Capacity; foreach (SpecimenState specimen in hold.Specimens) { float num = (flag ? 4f : (-1.5f)); if (state.Food < 30f) { num += 2f; } specimen.Stress = Mathf.Clamp(specimen.Stress + num, 0f, 100f); if (specimen.Stress > 80f) { specimen.Health = Mathf.Max(0f, specimen.Health - 4f); } else { specimen.Health = Mathf.Min(100f, specimen.Health + 1f); } if (specimen.Juvenile) { SpeciesRuntime runtime = EcoCatalog.GetRuntime(specimen.Key); bool num2 = runtime != null && EcoCatalog.Normalize(runtime.Name) == EcoCatalog.Normalize("Giant Sapsucker"); bool flag2 = state.Crops.Values.Any((CropState c) => EcoCatalog.Normalize(c.Name) == EcoCatalog.Normalize("Saproot")); if (!num2 || flag2 || state.Food >= 30f) { specimen.Growth = Mathf.Clamp(specimen.Growth + 10f, 0f, 100f); } if (specimen.Growth >= 100f) { specimen.Juvenile = false; specimen.Name = "Giant Sapsucker"; specimen.PenCost = 3; EcologyRuntime.AddLog(state, "A juvenile sapsucker matured in the Specimen Hold."); } } } hold.Specimens.RemoveAll((SpecimenState s) => s.Health <= 0f); } private static bool HasPollinator(MoonState state) { if (!HasSpecies(state, "Circuit Bee", 10f) && !HasSpecies(state, "Roaming Locust", 10f)) { return EcologyRuntime.Data.Hold.Specimens.Any((SpecimenState s) => s.Hive); } return true; } private static bool HasSpecies(MoonState state, string name, float minimum) { if (state == null) { return false; } string key = EcoCatalog.Normalize(name); if (state.Species.TryGetValue(key, out var value) && value.Present) { return value.Population >= minimum; } return false; } private static string TierOf(MoonState state, string key) { SpeciesRuntime runtime = EcoCatalog.GetRuntime(key); if (runtime == null) { return "Primary"; } return runtime.Def.Tier; } private static float AveragePopulation(MoonState state, List<string> names) { if (names == null || names.Count == 0) { return state.Food * 0.55f; } float num = 0f; int num2 = 0; foreach (string name in names) { string key = EcoCatalog.Normalize(name); if (state.Species.TryGetValue(key, out var value) && value.Present) { num += value.Population; num2++; } } if (num2 != 0) { return num / (float)num2; } return state.Food * 0.5f; } private static float AverageHostPopulation(MoonState state, List<string> hosts) { if (hosts == null || hosts.Count == 0) { return 45f; } float num = 0f; int num2 = 0; foreach (string host in hosts) { string text = EcoCatalog.Normalize(host); SpeciesState value; if (text == EcoCatalog.Normalize("Players")) { num += 45f; num2++; } else if (state.Species.TryGetValue(text, out value)) { num += value.Population; num2++; } } if (num2 != 0) { return num / (float)num2; } return 40f; } public static string TerritoryLabel(TerritoryState territory) { if (territory == null) { return "territory"; } if (territory.Type == "Relay") { string text = territory.Id.Replace("relay", ""); return "Relay Spire " + text; } string text2 = territory.Id.Replace("burrow", ""); return "Burrow " + text2; } } public static class EcologyStore { private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { Formatting = (Formatting)1, NullValueHandling = (NullValueHandling)0 }; public static string CurrentSaveName { get; private set; } = ""; public static string CurrentPath { get; private set; } = ""; public static string ResolveSaveName() { try { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance != (Object)null && !string.IsNullOrWhiteSpace(instance.currentSaveFileName)) { return instance.currentSaveFileName; } } catch { } return "LCSaveFile1"; } public static EcologySave Load(string saveName = null) { if (string.IsNullOrWhiteSpace(saveName)) { saveName = ResolveSaveName(); } saveName = SafeName(saveName); string text = Path.Combine(Paths.ConfigPath, "LethalEcology", "saves"); Directory.CreateDirectory(text); string text2 = Path.Combine(text, saveName + ".json"); CurrentSaveName = saveName; CurrentPath = text2; if (!File.Exists(text2)) { EcologySave obj = new EcologySave { SaveName = saveName }; Save(obj); return obj; } try { EcologySave ecologySave = JsonConvert.DeserializeObject<EcologySave>(File.ReadAllText(text2), JsonSettings); if (ecologySave == null) { ecologySave = new EcologySave { SaveName = saveName }; } ecologySave.SaveName = saveName; if (ecologySave.Moons == null) { ecologySave.Moons = new Dictionary<string, MoonState>(); } if (ecologySave.Hold == null) { ecologySave.Hold = new HoldState(); } if (ecologySave.Hold.Specimens == null) { ecologySave.Hold.Specimens = new List<SpecimenState>(); } return ecologySave; } catch (Exception ex) { Debug.LogError((object)("[LethalEcology] Could not read ecology save, starting a fresh sidecar: " + ex)); string destFileName = text2 + ".broken-" + DateTime.UtcNow.ToString("yyyyMMddHHmmss"); try { File.Copy(text2, destFileName, overwrite: true); } catch { } EcologySave obj3 = new EcologySave { SaveName = saveName }; Save(obj3); return obj3; } } public static void Save(EcologySave save) { if (save == null) { return; } if (string.IsNullOrWhiteSpace(CurrentPath)) { CurrentSaveName = SafeName(ResolveSaveName()); string text = Path.Combine(Paths.ConfigPath, "LethalEcology", "saves"); Directory.CreateDirectory(text); CurrentPath = Path.Combine(text, CurrentSaveName + ".json"); } try { save.UpdatedUtc = DateTime.UtcNow.ToString("o"); string contents = JsonConvert.SerializeObject((object)save, JsonSettings); string text2 = CurrentPath + ".tmp"; File.WriteAllText(text2, contents); if (File.Exists(CurrentPath)) { File.Delete(CurrentPath); } File.Move(text2, CurrentPath); } catch (Exception ex) { Debug.LogError((object)("[LethalEcology] Could not write ecology save: " + ex)); } } public static bool Reset() { try { if (File.Exists(CurrentPath)) { File.Delete(CurrentPath); } return true; } catch (Exception ex) { Debug.LogError((object)("[LethalEcology] Could not delete ecology save: " + ex)); return false; } } private static string SafeName(string value) { char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { value = value.Replace(oldChar, '_'); } if (!string.IsNullOrWhiteSpace(value)) { return value; } return "LCSaveFile1"; } } public static class EcologyText { private const int Width = 62; public static string Help() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "LETHAL ECOLOGY // COMMAND INDEX"); stringBuilder.AppendLine("All commands are host-authoritative."); stringBuilder.AppendLine("Use ecoindex for every topic, ecofind for search."); stringBuilder.AppendLine(); Command(stringBuilder, "ecohelp", "this command list"); Command(stringBuilder, "ecoindex", "every mechanic, species, crop, and moon"); Command(stringBuilder, "ecostatus", "current moon dashboard"); Command(stringBuilder, "ecomoons", "all moons and their ecology"); Command(stringBuilder, "ecomoon <moon>", "moon identity, budget, crops, apex"); Command(stringBuilder, "ecospecies [name]", "species index or full species sheet"); Command(stringBuilder, "ecoweb", "current moon food web"); Command(stringBuilder, "ecobudget", "exact power budget math"); Command(stringBuilder, "ecocensus", "populations, spawn caps, power"); Command(stringBuilder, "ecoterritory", "Relay Spires and Burrows"); Command(stringBuilder, "ecocultivation", "crop list and current crops"); Command(stringBuilder, "ecocrop <name>", "crop requirements and steps"); Command(stringBuilder, "ecoplant <name>", "buy and plant a crop on this moon"); Command(stringBuilder, "ecocare <name>", "spend credits to treat blight"); Command(stringBuilder, "ecoitems", "every mod tool, charge, and kit"); Command(stringBuilder, "ecodestroy <anchor>", "buy a demolition charge and destroy an anchor"); Command(stringBuilder, "ecohold", "Specimen Hold inventory"); Command(stringBuilder, "ecohold enter", "enter the containment deck"); Command(stringBuilder, "ecohold exit", "return to the ship"); Command(stringBuilder, "ecorelease <pen>", "release a held specimen"); Command(stringBuilder, "ecocapture", "capture instructions and keybind"); Command(stringBuilder, "ecogear", "capture gear and pen costs"); Command(stringBuilder, "ecomechanic <name>", "full mechanic explanation"); Command(stringBuilder, "ecomanual", "quick-start manual"); Command(stringBuilder, "ecocompat", "profiles, multiplayer, and data sheets"); Command(stringBuilder, "ecolog [n]", "recent field log"); Command(stringBuilder, "ecosave", "force a sidecar save"); Command(stringBuilder, "ecoreset confirm", "wipe ecology data for this save"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Species, crops, and moons also answer to aliases."); stringBuilder.AppendLine("Example: ecospecies forest keeper"); return stringBuilder.ToString(); } public static string Index() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "LETHAL ECOLOGY // COMPLETE INDEX"); stringBuilder.AppendLine("COMMANDS"); stringBuilder.AppendLine(" ecohelp, ecoindex, ecofind <text>, ecostatus"); stringBuilder.AppendLine(" ecomoons, ecomoon <moon>, ecospecies [name], ecoweb"); stringBuilder.AppendLine(" ecobudget, ecocensus, ecoterritory, ecolog [n]"); stringBuilder.AppendLine(" ecocultivation, ecocrop <name>, ecoplant <name>, ecocare <name>"); stringBuilder.AppendLine(" ecoitems, ecoterritory, ecodestroy <anchor>"); stringBuilder.AppendLine(" ecohold [enter|exit], ecorelease <pen>, ecocapture, ecogear"); stringBuilder.AppendLine(" ecomechanic <name>, ecomanual, ecocompat, ecosave"); stringBuilder.AppendLine(); stringBuilder.AppendLine("MECHANICS"); foreach (MechanicDef mechanic in EcoData.Mechanics.Mechanics) { stringBuilder.AppendLine(" " + mechanic.Name + " - " + mechanic.Summary); } stringBuilder.AppendLine(); stringBuilder.AppendLine("SPECIES (" + EcoCatalog.AllRuntimeOrdered().Count + " discovered, " + EcoData.Species.Species.Count + " authored)"); foreach (IGrouping<string, SpeciesRuntime> item in from x in EcoCatalog.AllRuntimeOrdered() group x by x.Def.Tier) { stringBuilder.AppendLine(" " + item.Key + ": " + string.Join(", ", item.Select((SpeciesRuntime x) => x.Name).ToArray())); } stringBuilder.AppendLine(); stringBuilder.AppendLine("CROPS"); foreach (CropDef crop in EcoData.Crops.Crops) { stringBuilder.AppendLine(" " + crop.Name + " - " + crop.Role); } stringBuilder.AppendLine(); stringBuilder.AppendLine("MOONS"); foreach (SelectableLevel item2 in EcoCatalog.AllLevels()) { stringBuilder.AppendLine(" " + item2.PlanetName + " (level " + item2.levelID + ")"); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Use ecofind <text> to search every sheet."); return stringBuilder.ToString(); } public static string Status() { StartOfRound instance = StartOfRound.Instance; StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "LETHAL ECOLOGY // CURRENT MOON"); if ((Object)(object)instance == (Object)null || (Object)(object)instance.currentLevel == (Object)null) { stringBuilder.AppendLine("No moon selected. Start or load a game first."); return stringBuilder.ToString(); } SelectableLevel currentLevel = instance.currentLevel; MoonState orCreate = EcologyRuntime.GetOrCreate(currentLevel); MoonDef moonDef = EcoCatalog.MoonForLevelOrFallback(currentLevel); BudgetResult budgetResult = EcologyRuntime.ComputeBudget(currentLevel, orCreate); string text = ((Plugin.Profile != null) ? Plugin.Profile.Value.ToString() : "FullEcology"); stringBuilder.AppendLine(currentLevel.PlanetName + " // risk " + Safe(currentLevel.riskLevel, "?") + " // level " + currentLevel.levelID); stringBuilder.AppendLine(moonDef.Identity + " // " + moonDef.Biome); stringBuilder.AppendLine("Profile: " + text); stringBuilder.AppendLine(); stringBuilder.AppendLine("FOOD " + Mathf.RoundToInt(orCreate.Food).ToString().PadLeft(3) + " / 100 " + Bar(orCreate.Food, 100f, 22)); stringBuilder.AppendLine("STABILITY " + EcologyRuntime.StabilityName(orCreate.Stability) + " (" + Signed(EcologyRuntime.StabilityModifier(orCreate.Stability)) + ")"); stringBuilder.AppendLine("BUDGET Outside " + budgetResult.OutsideBudget + " | Inside " + budgetResult.InsideBudget + " | Day " + budgetResult.DaytimeBudget); stringBuilder.AppendLine(" base " + budgetResult.OutsideBase + " + food " + Signed(budgetResult.FoodModifier) + " + stability " + Signed(budgetResult.StabilityModifier) + " - apex " + budgetResult.ApexPower); stringBuilder.AppendLine(); int num = orCreate.Species.Values.Count((SpeciesState s) => s.Present && !s.Introduced); int num2 = orCreate.Species.Values.Count((SpeciesState s) => s.Present && s.Introduced); stringBuilder.AppendLine("SPECIES " + num + " native / " + num2 + " introduced"); stringBuilder.AppendLine("CROPS " + orCreate.Crops.Count + " active"); stringBuilder.AppendLine("HOLD " + UsedPens() + " / " + HoldCapacity() + " pens"); stringBuilder.AppendLine("TERRITORY " + TerritorySummary(orCreate)); stringBuilder.AppendLine("CYCLE " + orCreate.Cycle + " // visits " + orCreate.Visits); stringBuilder.AppendLine(); List<SpeciesState> list = (from s in orCreate.Species.Values where s.Present && s.Population > 0f orderby PopulationPower(s) descending select s).Take(6).ToList(); if (list.Count > 0) { stringBuilder.AppendLine("DOMINANT SPECIES"); foreach (SpeciesState item in list) { SpeciesRuntime runtime = EcoCatalog.GetRuntime(item.Key); int num3 = ((runtime != null) ? EcologyRuntime.SpawnCap(orCreate, runtime) : 0); stringBuilder.AppendLine(" " + Pad(item.Name, 22) + " pop " + Mathf.RoundToInt(item.Population).ToString().PadLeft(3) + " cap " + num3 + " " + ((runtime != null) ? ("p" + runtime.Power.ToString("0.0")) : "")); } } if (orCreate.Log.Count > 0) { stringBuilder.AppendLine(); stringBuilder.AppendLine("LAST LOG"); stringBuilder.AppendLine(" " + orCreate.Log[orCreate.Log.Count - 1]); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Try ecobudget, ecocensus, ecoweb, or ecoterritory."); return stringBuilder.ToString(); } public static string Moons() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "MOON SURVEY"); stringBuilder.AppendLine("Name Food Stab Out In Apex"); stringBuilder.AppendLine("----------------------------------------------------------------"); foreach (SelectableLevel item in EcoCatalog.AllLevels()) { MoonState orCreate = EcologyRuntime.GetOrCreate(item); BudgetResult budgetResult = EcologyRuntime.ComputeBudget(item, orCreate); MoonDef moonDef = EcoCatalog.MoonForLevelOrFallback(item); string text = ((moonDef.Apex != null && moonDef.Apex.Count > 0) ? string.Join(", ", moonDef.Apex.ToArray()) : "-"); stringBuilder.AppendLine(Pad(item.PlanetName, 28) + " " + Mathf.RoundToInt(orCreate.Food).ToString().PadLeft(3) + " " + Pad(EcologyRuntime.StabilityName(orCreate.Stability), 8) + " " + budgetResult.OutsideBudget.ToString().PadLeft(4) + " " + budgetResult.InsideBudget.ToString().PadLeft(3) + " " + text); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Use ecomoon <name>. Food drives both outside and inside budgets."); return stringBuilder.ToString(); } public static string MoonDetail(string query) { StringBuilder stringBuilder = new StringBuilder(); MoonDef moon = EcoCatalog.FindMoon(query); SelectableLevel val = FindLevel(query, moon); if ((Object)(object)val == (Object)null) { Header(stringBuilder, "MOON NOT FOUND"); stringBuilder.AppendLine("No moon matches '" + query + "'."); stringBuilder.AppendLine("Use ecomoons for the full list."); return stringBuilder.ToString(); } moon = EcoCatalog.MoonForLevelOrFallback(val); MoonState orCreate = EcologyRuntime.GetOrCreate(val); BudgetResult budgetResult = EcologyRuntime.ComputeBudget(val, orCreate); Header(stringBuilder, val.PlanetName.ToUpperInvariant() + " // " + moon.Identity.ToUpperInvariant()); stringBuilder.AppendLine("Biome: " + moon.Biome); stringBuilder.AppendLine("Risk: " + Safe(val.riskLevel, "?") + " Level ID: " + val.levelID); stringBuilder.AppendLine(); stringBuilder.AppendLine("Food " + Mathf.RoundToInt(orCreate.Food) + " / 100 " + Bar(orCreate.Food, 100f, 24)); stringBuilder.AppendLine("Stability " + EcologyRuntime.StabilityName(orCreate.Stability) + " (" + Signed(EcologyRuntime.StabilityModifier(orCreate.Stability)) + ")"); stringBuilder.AppendLine("Budget Outside " + budgetResult.OutsideBudget + " | Inside " + budgetResult.InsideBudget + " | Day " + budgetResult.DaytimeBudget); stringBuilder.AppendLine("Bases " + budgetResult.OutsideBase + " outside | " + budgetResult.InsideBase + " inside | " + budgetResult.DaytimeBase + " day"); stringBuilder.AppendLine("Modifiers food " + Signed(budgetResult.FoodModifier) + " | stability " + Signed(budgetResult.StabilityModifier) + " | apex -" + budgetResult.ApexPower); stringBuilder.AppendLine(); stringBuilder.AppendLine("APEX"); stringBuilder.AppendLine(" " + ((moon.Apex != null && moon.Apex.Count > 0) ? string.Join(", ", moon.Apex.ToArray()) : "No named apex.")); stringBuilder.AppendLine(" " + TerritorySummary(orCreate)); stringBuilder.AppendLine(); stringBuilder.AppendLine("NATIVE POOL"); List<SpeciesRuntime> list = EcoCatalog.SpeciesForLevel(val); foreach (SpeciesRuntime item in from x in list orderby EcoCatalog.TierOrder(x.Def.Tier), x.Name select x) { SpeciesState speciesState = EcologyRuntime.GetSpeciesState(orCreate, item.Key); stringBuilder.AppendLine(" " + Pad(item.Name, 22) + " " + Pad(item.Def.Tier, 10) + " pop " + Mathf.RoundToInt(speciesState?.Population ?? 0f).ToString().PadLeft(3)); } if (list.Count == 0) { stringBuilder.AppendLine(" Runtime pool not available yet."); } stringBuilder.AppendLine(); stringBuilder.AppendLine("CROPS"); stringBuilder.AppendLine(" " + ((moon.Crops != null && moon.Crops.Count > 0) ? string.Join(", ", moon.Crops.ToArray()) : "None listed.")); stringBuilder.AppendLine("HAZARDS"); foreach (string item2 in moon.Hazards ?? new List<string>()) { stringBuilder.AppendLine(" - " + item2); } stringBuilder.AppendLine(); AppendWrapped(stringBuilder, moon.Notes, " "); if (!string.IsNullOrWhiteSpace(moon.Project)) { stringBuilder.AppendLine(); stringBuilder.AppendLine("PROJECT"); AppendWrapped(stringBuilder, moon.Project, " "); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Use ecocensus, ecoweb, or ecocultivation for live detail."); return stringBuilder.ToString(); } public static string SpeciesIndex() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "SPECIES INDEX"); MoonState moonState = EcologyRuntime.CurrentMoon(); List<SpeciesRuntime> list = EcoCatalog.AllRuntimeOrdered(); if (list.Count == 0) { foreach (SpeciesDef item in from x in EcoData.Species.Species orderby EcoCatalog.TierOrder(x.Tier), x.Name select x) { stringBuilder.AppendLine(" " + Pad(item.Name, 24) + " " + Pad(item.Tier, 10) + " p" + item.Power.ToString("0.0") + " [dormant]"); } stringBuilder.AppendLine(); stringBuilder.AppendLine("The game assembly has not loaded a level pool yet."); return stringBuilder.ToString(); } foreach (IGrouping<string, SpeciesRuntime> item2 in from x in list group x by x.Def.Tier into g orderby EcoCatalog.TierOrder(g.Key) select g) { stringBuilder.AppendLine(item2.Key.ToUpperInvariant()); foreach (SpeciesRuntime item3 in item2.OrderBy((SpeciesRuntime x) => x.Name)) { SpeciesState speciesState = ((moonState != null) ? EcologyRuntime.GetSpeciesState(moonState, item3.Key) : null); string text = ((speciesState != null && speciesState.Present) ? "*" : " "); string text2 = ((speciesState != null) ? Mathf.RoundToInt(speciesState.Population).ToString().PadLeft(3) : " -"); string text3 = ((speciesState != null) ? EcologyRuntime.SpawnCap(moonState, item3).ToString() : "-"); stringBuilder.AppendLine(" " + text + " " + Pad(item3.Name, 22) + " p" + item3.Power.ToString("0.0").PadLeft(4) + " pop " + text2 + " cap " + text3 + " " + item3.Habitat); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("* = present on current moon. Use ecospecies <name>."); return stringBuilder.ToString(); } public static string SpeciesDetail(string query) { StringBuilder stringBuilder = new StringBuilder(); SpeciesRuntime runtime = EcoCatalog.GetRuntime(query); SpeciesDef speciesDef = ((runtime == null) ? EcoCatalog.FindAuthored(query) : runtime.Def); if (speciesDef == null) { Header(stringBuilder, "SPECIES NOT FOUND"); stringBuilder.AppendLine("No species matches '" + query + "'."); stringBuilder.AppendLine("Use ecospecies for the index."); return stringBuilder.ToString(); } Header(stringBuilder, speciesDef.Name.ToUpperInvariant()); stringBuilder.AppendLine("Tier: " + speciesDef.Tier + " Habitat: " + ((runtime != null) ? runtime.Habitat : speciesDef.Habitat)); stringBuilder.AppendLine("Power: " + ((runtime != null) ? runtime.Power.ToString("0.0") : speciesDef.Power.ToString("0.0")) + " Capturable: " + (speciesDef.Capturable ? ("yes (" + speciesDef.PenCost + " pen" + ((speciesDef.PenCost == 1) ? "" : "s") + ")") : "no")); if (runtime != null) { stringBuilder.AppendLine("Spawn cap limit: " + runtime.OriginalMaxCount + " Runtime: live"); } else { stringBuilder.AppendLine("Runtime: dormant on this install"); } MoonState moonState = EcologyRuntime.CurrentMoon(); if (moonState != null) { SpeciesState speciesState = EcologyRuntime.GetSpeciesState(moonState, speciesDef.Name); if (speciesState != null) { stringBuilder.AppendLine("Current moon: " + moonState.LevelName); stringBuilder.AppendLine(" Population " + Mathf.RoundToInt(speciesState.Population) + " / 100 Spawn cap " + ((runtime != null) ? EcologyRuntime.SpawnCap(moonState, runtime) : 0) + " Killed " + speciesState.Killed + " Captured " + speciesState.Captured); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("ROLE"); AppendWrapped(stringBuilder, speciesDef.Role, " "); stringBuilder.AppendLine(); stringBuilder.AppendLine("DIET"); AppendWrapped(stringBuilder, speciesDef.Diet, " "); stringBuilder.AppendLine(); stringBuilder.AppendLine("PREYS"); AppendList(stringBuilder, speciesDef.Preys); stringBuilder.AppendLine("PREDATORS"); AppendList(stringBuilder, speciesDef.Predators); stringBuilder.AppendLine("HOSTS"); AppendList(stringBuilder, speciesDef.Hosts); stringBuilder.AppendLine(); stringBuilder.AppendLine("TRAITS (C = canon, D = mod)"); foreach (string item in speciesDef.Traits ?? new List<string>()) { stringBuilder.AppendLine(" - " + item); } stringBuilder.AppendLine(); stringBuilder.AppendLine("PLAYER CONTROLS"); foreach (string item2 in speciesDef.Controls ?? new List<string>()) { stringBuilder.AppendLine(" - " + item2); } stringBuilder.AppendLine(); stringBuilder.AppendLine("CANON NOTES"); foreach (string item3 in speciesDef.Canon ?? new List<string>()) { stringBuilder.AppendLine(" C " + item3); } stringBuilder.AppendLine("MOD NOTES"); foreach (string item4 in speciesDef.Mod ?? new List<string>()) { stringBuilder.AppendLine(" D " + item4); } stringBuilder.AppendLine(); AppendWrapped(stringBuilder, speciesDef.Notes, " "); return stringBuilder.ToString(); } public static string Web() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "FOOD WEB // CURRENT MOON"); SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); MoonState moonState = EcologyRuntime.CurrentMoon(); if ((Object)(object)val == (Object)null || moonState == null) { stringBuilder.AppendLine("No current moon."); return stringBuilder.ToString(); } stringBuilder.AppendLine(val.PlanetName + " Food " + Mathf.RoundToInt(moonState.Food) + " " + EcologyRuntime.StabilityName(moonState.Stability)); stringBuilder.AppendLine(); string[] array = new string[6] { "Producer", "Primary", "Secondary", "Apex", "Parasite", "Anomaly" }; foreach (string tier in array) { var list = (from s in moonState.Species.Values where s.Present && s.Population > 0f select new { State = s, Runtime = EcoCatalog.GetRuntime(s.Key) } into x where x.Runtime != null && EcoCatalog.TierIs(x.Runtime.Def.Tier, tier) orderby x.State.Population descending select x).ToList(); if (list.Count == 0) { continue; } stringBuilder.AppendLine(tier.ToUpperInvariant()); foreach (var item in list) { stringBuilder.AppendLine(" " + Pad(item.State.Name, 24) + " " + Mathf.RoundToInt(item.State.Population).ToString().PadLeft(3) + " " + Bar(item.State.Population, 100f, 14)); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("KEY LINKS"); int num = 0; foreach (SpeciesState item2 in from x in moonState.Species.Values where x.Present && x.Population >= 10f orderby x.Population descending select x) { SpeciesRuntime runtime = EcoCatalog.GetRuntime(item2.Key); if (runtime == null) { continue; } foreach (string item3 in runtime.Def.Preys ?? new List<string>()) { string key = EcoCatalog.Normalize(item3); if (moonState.Species.TryGetValue(key, out var value) && value.Present) { stringBuilder.AppendLine(" " + item2.Name + " <- " + value.Name); num++; if (num >= 10) { break; } } } if (num >= 10) { break; } } if (num == 0) { stringBuilder.AppendLine(" No direct links are recorded yet."); } stringBuilder.AppendLine(); stringBuilder.AppendLine("Arrow points from food to eater. Use ecospecies for a full sheet."); return stringBuilder.ToString(); } public static string Budget() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "POWER BUDGET"); SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); if ((Object)(object)val == (Object)null) { stringBuilder.AppendLine("No current moon. The formula is global:"); stringBuilder.AppendLine(" Outside = Base + FoodModifier + StabilityModifier - ApexResidents"); stringBuilder.AppendLine(" Inside = Base + FoodModifier + StabilityModifier"); stringBuilder.AppendLine(" FoodModifier = floor((Food - 50) / 10), clamp -5..+10"); stringBuilder.AppendLine(" Stability: Thriving +2, Stable 0, Stressed -2, Collapsing -4"); stringBuilder.AppendLine(" Each occupied Relay Spire or Burrow subtracts its resident power from Outside."); return stringBuilder.ToString(); } MoonState orCreate = EcologyRuntime.GetOrCreate(val); BudgetResult budgetResult = EcologyRuntime.ComputeBudget(val, orCreate); stringBuilder.AppendLine(val.PlanetName); stringBuilder.AppendLine(); stringBuilder.AppendLine("OUTSIDE"); stringBuilder.AppendLine(" Base " + budgetResult.OutsideBase); stringBuilder.AppendLine(" Food " + Mathf.RoundToInt(orCreate.Food) + " " + Signed(budgetResult.FoodModifier)); stringBuilder.AppendLine(" Stability " + Signed(budgetResult.StabilityModifier) + " (" + EcologyRuntime.StabilityName(orCreate.Stability) + ")"); stringBuilder.AppendLine(" Apex residents -" + budgetResult.ApexPower); stringBuilder.AppendLine(" = Budget " + budgetResult.OutsideBudget); stringBuilder.AppendLine(); stringBuilder.AppendLine("INSIDE"); stringBuilder.AppendLine(" Base " + budgetResult.InsideBase); stringBuilder.AppendLine(" Food + Stability " + Signed(budgetResult.FoodModifier + budgetResult.StabilityModifier)); stringBuilder.AppendLine(" = Budget " + budgetResult.InsideBudget); stringBuilder.AppendLine(); stringBuilder.AppendLine("DAYTIME"); stringBuilder.AppendLine(" Base " + budgetResult.DaytimeBase); stringBuilder.AppendLine(" = Budget " + budgetResult.DaytimeBudget); stringBuilder.AppendLine(); stringBuilder.AppendLine("FOOD MODIFIER TABLE"); stringBuilder.AppendLine(" Food 0-9: -5 10-19: -4 20-29: -3 30-39: -2"); stringBuilder.AppendLine(" 40-49: -1 50-59: 0 60-69: +1 70-79: +2"); stringBuilder.AppendLine(" 80-89: +3 90-99: +4 100: +5 (cap +10 is available to special effects)"); stringBuilder.AppendLine(); stringBuilder.AppendLine("SPAWN CAP"); stringBuilder.AppendLine(" pop < 10 -> 0 spawns"); stringBuilder.AppendLine(" pop >= 10 -> ceil(pop / 20), maximum 5"); stringBuilder.AppendLine(" Apex territory -> one spawn slot per occupied anchor"); stringBuilder.AppendLine(); List<SpeciesRuntime> source = EcoCatalog.SpeciesForLevel(val); stringBuilder.AppendLine("CURRENT CAPS"); foreach (SpeciesRuntime item in from x in source orderby x.Power descending, x.Name select x) { SpeciesState speciesState = EcologyRuntime.GetSpeciesState(orCreate, item.Key); if (speciesState != null) { stringBuilder.AppendLine(" " + Pad(item.Name, 24) + " pop " + Mathf.RoundToInt(speciesState.Population).ToString().PadLeft(3) + " cap " + EcologyRuntime.SpawnCap(orCreate, item) + " cost " + item.Power.ToString("0.0")); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("Apex residents cost " + ((Plugin.ApexPower != null) ? Plugin.ApexPower.Value.ToString("0.0") : "12") + " power each."); return stringBuilder.ToString(); } public static string Census() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "SPECIES CENSUS"); SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); MoonState moonState = EcologyRuntime.CurrentMoon(); if ((Object)(object)val == (Object)null || moonState == null) { stringBuilder.AppendLine("No current moon."); return stringBuilder.ToString(); } stringBuilder.AppendLine(val.PlanetName + " Food " + Mathf.RoundToInt(moonState.Food) + " " + EcologyRuntime.StabilityName(moonState.Stability)); stringBuilder.AppendLine(); string[] array = new string[5] { "Outside", "Inside", "Daytime", "Outside / Inside", "Any" }; foreach (string habitat in array) { var list = (from s in moonState.Species.Values select new { State = s, Runtime = EcoCatalog.GetRuntime(s.Key) } into x where x.Runtime != null && (x.Runtime.Habitat == habitat || (habitat == "Any" && x.Runtime.Habitat == "Any")) orderby x.State.Population descending select x).ToList(); if (list.Count == 0) { continue; } stringBuilder.AppendLine(habitat.ToUpperInvariant()); foreach (var item in list) { int num = EcologyRuntime.SpawnCap(moonState, item.Runtime); string text = ((!item.State.Present) ? "EXTINCT" : (item.State.Introduced ? "INTRO" : "")); stringBuilder.AppendLine(" " + Pad(item.State.Name, 22) + " " + Mathf.RoundToInt(item.State.Population).ToString().PadLeft(3) + " cap " + num + " p" + item.Runtime.Power.ToString("0.0").PadLeft(4) + " " + text); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("Species below 10 population cannot spawn. Protect producers first."); return stringBuilder.ToString(); } public static string Territories() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "APEX TERRITORIES"); SelectableLevel obj = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); MoonState moonState = EcologyRuntime.CurrentMoon(); if ((Object)(object)obj == (Object)null || moonState == null) { stringBuilder.AppendLine("No current moon."); return stringBuilder.ToString(); } stringBuilder.AppendLine("Old Birds claim Relay Spires. Earth Leviathans claim Burrows."); stringBuilder.AppendLine("One resident per intact anchor. Each resident costs " + ((Plugin.ApexPower != null) ? Plugin.ApexPower.Value.ToString("0.0") : "12") + " power."); stringBuilder.AppendLine(); if (moonState.Territories.Count == 0) { stringBuilder.AppendLine("No apex anchors on this moon."); return stringBuilder.ToString(); } foreach (TerritoryState item in from x in moonState.Territories.Values orderby x.Type, x.Id select x) { string value = EcoSim.TerritoryLabel(item); string value2 = (item.Destroyed ? "DESTROYED" : (item.Occupied ? "OCCUPIED" : "EMPTY")); stringBuilder.AppendLine(Pad(value, 20) + " " + Pad(value2, 10) + " " + Safe(item.ResidentName, "-")); if (!item.Destroyed) { stringBuilder.AppendLine(" ID: " + item.Id + " demolition: ecodestroy " + item.Id); } if (!item.Occupied && !item.Destroyed) { stringBuilder.AppendLine(" Reclaim in " + item.ReclaimIn + " cycle(s) if Food and Stability hold."); } } stringBuilder.AppendLine(); stringBuilder.AppendLine("REDUCTION"); stringBuilder.AppendLine(" ecodestroy <id> buys a demolition charge and permanently removes the anchor."); stringBuilder.AppendLine(" Relay charge $150; Burrow charge $200. Both are host-only."); stringBuilder.AppendLine(" Kill the resident with a Nutcracker shotgun (Old Bird) or explosives (Leviathan)."); stringBuilder.AppendLine(" Drop the moon budget below 12 to suppress new apex spawns."); stringBuilder.AppendLine(" Ironroot and stable soil prevent new Burrows."); return stringBuilder.ToString(); } public static string Cultivation() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "CULTIVATION"); SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); MoonState moonState = EcologyRuntime.CurrentMoon(); if ((Object)(object)val == (Object)null || moonState == null) { stringBuilder.AppendLine("No current moon."); return stringBuilder.ToString(); } stringBuilder.AppendLine(val.PlanetName + " Food " + Mathf.RoundToInt(moonState.Food) + " " + EcologyRuntime.StabilityName(moonState.Stability)); stringBuilder.AppendLine(); if (moonState.Crops.Count == 0) { stringBuilder.AppendLine("No active crops."); } else { stringBuilder.AppendLine("ACTIVE CROPS"); foreach (CropState item in moonState.Crops.Values.OrderBy((CropState x) => x.Name)) { stringBuilder.AppendLine(" " + Pad(item.Name, 18) + " x" + item.Amount + " hp " + Mathf.RoundToInt(item.Health) + " blight " + Mathf.RoundToInt(item.Blight) + (item.Pollinated ? " pollinated" : "")); } stringBuilder.AppendLine(); } stringBuilder.AppendLine("AVAILABLE CROPS"); foreach (CropDef item2 in EcoCatalog.CropsForMoon(val)) { stringBuilder.AppendLine(" " + Pad(item2.Name, 18) + " $" + item2.Cost + " food +" + item2.FoodPerCycle.ToString("0.0") + " stab " + Signed(Mathf.RoundToInt(item2.StabilityPerCycle)) + " blight " + item2.BlightRisk); } stringBuilder.AppendLine(); stringBuilder.AppendLine("ecoplant <name> buys and plants a crop on this moon."); stringBuilder.AppendLine("ecocare <name> spends credits to reduce blight."); stringBuilder.AppendLine("ecocrop <name> shows requirements and exact steps."); return stringBuilder.ToString(); } public static string CropDetail(string query) { StringBuilder stringBuilder = new StringBuilder(); CropDef cropDef = EcoCatalog.FindCrop(query); if (cropDef == null) { Header(stringBuilder, "CROP NOT FOUND"); stringBuilder.AppendLine("No crop matches '" + query + "'."); return stringBuilder.ToString(); } Header(stringBuilder, cropDef.Name.ToUpperInvariant()); stringBuilder.AppendLine(cropDef.Kind + " // " + cropDef.Role); stringBuilder.AppendLine("Cost $" + cropDef.Cost + " Food +" + cropDef.FoodPerCycle.ToString("0.0") + " Stability " + Signed(Mathf.RoundToInt(cropDef.StabilityPerCycle)) + " Blight " + cropDef.BlightRisk); stringBuilder.AppendLine("Pollinator: " + cropDef.Pollinator); stringBuilder.AppendLine("Habitat: " + cropDef.Habitat); stringBuilder.AppendLine(); stringBuilder.AppendLine("EFFECT"); AppendWrapped(stringBuilder, cropDef.Effect, " "); stringBuilder.AppendLine(); stringBuilder.AppendLine("REQUIREMENTS"); foreach (string item in cropDef.Requirements ?? new List<string>()) { stringBuilder.AppendLine(" - " + item); } stringBuilder.AppendLine(); stringBuilder.AppendLine("STEPS"); int num = 1; foreach (string item2 in cropDef.Steps ?? new List<string>()) { stringBuilder.AppendLine(" " + num++ + ". " + item2); } if (!string.IsNullOrWhiteSpace(cropDef.Warning)) { stringBuilder.AppendLine(); stringBuilder.AppendLine("WARNING"); AppendWrapped(stringBuilder, cropDef.Warning, " "); } return stringBuilder.ToString(); } public static string Hold() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "SPECIMEN HOLD"); HoldState hold = EcologyRuntime.Data.Hold; int num = HoldCapacity(); stringBuilder.AppendLine("Pens: " + UsedPens() + " / " + num + " used"); stringBuilder.AppendLine("Live specimens stay with the ship between moons."); stringBuilder.AppendLine(); if (hold.Specimens.Count == 0) { stringBuilder.AppendLine("The hold is empty."); stringBuilder.AppendLine(); stringBuilder.AppendLine("Stun a capturable creature, stand within 5 m, and press " + ((Plugin.CaptureKeyName != null) ? Plugin.CaptureKeyName.Value : "F8") + "."); return stringBuilder.ToString(); } stringBuilder.AppendLine("PEN SPECIMEN COST HEALTH STRESS NOTE"); stringBuilder.AppendLine("---------------------------------------------------------------"); foreach (SpecimenState item in hold.Specimens.OrderBy((SpecimenState x) => x.Pen)) { string text = (item.Juvenile ? ("juvenile " + Mathf.RoundToInt(item.Growth) + "%") : (item.Hive ? "live hive" : "adult")); stringBuilder.AppendLine(Pad(item.Pen.ToString(), 4) + " " + Pad(item.Name, 22) + " " + Pad(item.PenCost.ToString(), 5) + " " + Pad(Mathf.RoundToInt(item.Health).ToString(), 7) + " " + Pad(Mathf.RoundToInt(item.Stress).ToString(), 7) + " " + text); } stringBuilder.AppendLine(); stringBuilder.AppendLine("ecohold enter visit the containment deck"); stringBuilder.AppendLine("ecohold exit return to the ship"); stringBuilder.AppendLine("ecorelease <pen> release on the current moon"); return stringBuilder.ToString(); } public static string Gear() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "CAPTURE GEAR"); stringBuilder.AppendLine("Capture key: " + ((Plugin.CaptureKeyName != null) ? Plugin.CaptureKeyName.Value : "F8")); stringBuilder.AppendLine("Hold deck key: F9"); stringBuilder.AppendLine("Requirement: the creature must be stunned, not dead."); stringBuilder.AppendLine("Range: 5 m from the host player."); stringBuilder.AppendLine(); stringBuilder.AppendLine("PEN COSTS"); stringBuilder.AppendLine(" Small (1) Manticoil, Locust, Hoarding Bug, Snare Flea"); stringBuilder.AppendLine(" Medium (2) Baboon Hawk, Bunker Spider, Thumper, Butler, Hive"); stringBuilder.AppendLine(" Large (3) Nutcracker, Eyeless Dog, Adult Sapsucker"); stringBuilder.AppendLine(" Huge (4) Forest Keeper, Old Bird, Bracken, Cave Dweller"); stringBuilder.AppendLine(); stringBuilder.AppendLine("NOT CAPTURABLE"); stringBuilder.AppendLine(" Earth Leviathan, Coil-Head, Jester, Ghost Girl, Barber, Clay Surgeon."); stringBuilder.AppendLine(" Cadaver Growths, Cadaver Bloom, and Vain Shroud are sample-only."); stringBuilder.AppendLine(); stringBuilder.AppendLine("Capture removes the creature from the moon and lowers its population."); return stringBuilder.ToString(); } public static string Items() { StringBuilder stringBuilder = new StringBuilder(); Header(stringBuilder, "ECOLOGY TOOLS AND ITEMS"); stringBuilder.AppendLine("This version deliberately adds no inventory items. Every tool is a"); stringBuilder.AppendLine("terminal action or a keybind, so vanilla clients need no mod item IDs."); stringBuilder.AppendLine(); stringBuilder.AppendLine("FIELD CAPTURE KIT"); stringBuilder.AppendLine(" Unlock: always available."); stringBuilder.AppendLine(" Use: stun a creature, stand within 5 m, press " + ((Plugin.CaptureKeyName != null) ? Plugin.CaptureKeyName.Value : "F8") + "."); stringBuilder.AppendLine(" Cost: free. Requires an empty pen in the Specimen Hold."); stringBuilder.AppendLine(); stringBuilder.AppendLine("SPECIMEN HOLD"); stringBuilder.AppendLine(" Unlock: always available on the host ship."); stringBuilder.AppendLine(" Use