diff --git a/EdBPrepareCarefully.csproj b/EdBPrepareCarefully.csproj index 83bd2bc..bbd81f4 100644 --- a/EdBPrepareCarefully.csproj +++ b/EdBPrepareCarefully.csproj @@ -78,6 +78,7 @@ + diff --git a/Resources/CHANGELOG.txt b/Resources/CHANGELOG.txt index aeb0e3d..ae321df 100644 --- a/Resources/CHANGELOG.txt +++ b/Resources/CHANGELOG.txt @@ -5,6 +5,8 @@ - Pawn generation changes to avoid apparel generation issues and to work around problems in other mods when generating a pawn with no faction. + - Reworked how scenario parts are applied when starting a new game to work + around issues with definitions that are removed by other mods. _____________________________________________________________________________ diff --git a/Source/Controller.cs b/Source/Controller.cs index 8154055..9c1f6bc 100644 --- a/Source/Controller.cs +++ b/Source/Controller.cs @@ -108,25 +108,15 @@ public void PrepareGame() { PrepareColonists(); PrepareWorldPawns(); - // This needs some explaining. We need custom scenario parts to handle animal spawning - // and scattered things. However, we don't want the scenario that gets saved with a game - // to include any Prepare Carefully-specific parts (because the save would become bound to - // the mod). We work around this by creating two copies of the actual scenario. The first - // copy includes the customized scenario parts needed to do the spawning. The second contains - // vanilla versions of those parts that can safely be saved without forcing a dependency on - // the mod. The GenStep_RemovePrepareCarefullyScenario class is responsible for switching out - // the actual scenario with the vanilla-friendly version at the end of the map generation process. - Scenario originalScenario = Find.Scenario; - Scenario actualScenario = UtilityCopy.CopyExposable(originalScenario); - Scenario vanillaFriendlyScenario = UtilityCopy.CopyExposable(originalScenario); - Current.Game.Scenario = actualScenario; - PrepareCarefully.OriginalScenario = vanillaFriendlyScenario; - - // Remove equipment scenario parts. - ReplaceScenarioParts(originalScenario, actualScenario, vanillaFriendlyScenario); - - //Logger.Debug(actualScenario.GetFullInformationText()); - //Logger.Debug(vanillaFriendlyScenario.GetFullInformationText()); + // When replacing the scenario parts, we'll get a list of parts that will be added to the scenario to spawn the new game. + // We also get back a set of vanilla-friendly parts that match the actual parts used. Once we've finishing spawning the + // scenario parts into the map, we'll replace the actual parts with the vanilla-friendly ones instead. We do this because + // the actual parts may have Prepare Carefully-specific scene parts. We want to make sure that the scenario that's saved with + // the game does not have anything specific to Prepare Carefully so that saves are not dependent on the mod. + (var actualParts, var vanillaFriendlyParts) = ReplaceScenarioParts(Find.Scenario); + Scenario scenario = Current.Game.Scenario; + scenario.SetPrivateField("parts", actualParts); + PrepareCarefully.OriginalScenarioParts = vanillaFriendlyParts; } protected void PrepareColonists() { @@ -282,22 +272,11 @@ protected void MakePawnIntoFactionLeader(CustomPawn pawn) { pawn.Faction.Faction.leader = pawn.Pawn; } - // The three arguments are: - // - originalScenario: the original, unmodified scenario - // - actualScenario: this is the scenario that will be used to spawn into the map. It contains Prepare Carefully-specific scenario part that should not be saved into a save file. - // - vanillaFriendlyScenario: this is the scenario that will be saved into the game save. It will be a copy of actualScenario, but with all of the Prepare Carefully-specific - // parts replaced by vanilla parts. - protected void ReplaceScenarioParts(Scenario originalScenario, Scenario actualScenario, Scenario vanillaFriendlyScenario) { - - // Create lists to hold the new scenario parts. - List actualScenarioParts = new List(); - List vanillaFriendlyScenarioParts = new List(); - - // Get the list of parts from the original scenario. We do this using reflection because the "AllParts" property - // will include an extra "player faction" part that we don't want. - FieldInfo partsField = typeof(Scenario).GetField("parts", BindingFlags.NonPublic | BindingFlags.Instance); - List originalParts = (List)partsField.GetValue(originalScenario); - List copiedParts = (List)partsField.GetValue(actualScenario); + protected Tuple, List> ReplaceScenarioParts(Scenario originalScenario) { + // Get a list of all of the original scenario parts + List originalParts = ReflectionUtil.GetFieldValue>(Find.Scenario, "parts"); + List actualParts = new List(); + List vanillaFriendlyParts = new List(); // Fill in the part lists with the scenario parts that we're not going to replace. We won't need to modify any of these. int index = -1; @@ -305,27 +284,22 @@ protected void ReplaceScenarioParts(Scenario originalScenario, Scenario actualSc index++; bool partReplaced = PrepareCarefully.Instance.ReplacedScenarioParts.Contains(part); if (!partReplaced) { - actualScenarioParts.Add(copiedParts[index]); - vanillaFriendlyScenarioParts.Add(copiedParts[index]); + actualParts.Add(originalParts[index]); + vanillaFriendlyParts.Add(originalParts[index]); } //Logger.Debug(String.Format("[{0}] Replaced? {1}: {2} {3}", index, partReplaced, part.Label, String.Join(", ", part.GetSummaryListEntries("PlayerStartsWith")))); } // Replace the pawn count in the configure pawns scenario parts to reflect the number of // pawns that were selected in Prepare Carefully. - foreach (var part in actualScenarioParts) { - if (!(part is ScenPart_ConfigPage_ConfigureStartingPawns configurePawnPart)) { - continue; - } - configurePawnPart.pawnCount = PrepareCarefully.Instance.ColonyPawns.Count; - configurePawnPart.pawnChoiceCount = configurePawnPart.pawnCount; - } - foreach (var part in vanillaFriendlyScenarioParts) { - if (!(part is ScenPart_ConfigPage_ConfigureStartingPawns configurePawnPart)) { - continue; - } - configurePawnPart.pawnCount = PrepareCarefully.Instance.ColonyPawns.Count; - configurePawnPart.pawnChoiceCount = configurePawnPart.pawnCount; + ScenPart_ConfigPage_ConfigureStartingPawns originalStartingPawnsPart = originalParts.FirstOrDefault(p => p is ScenPart_ConfigPage_ConfigureStartingPawns) as ScenPart_ConfigPage_ConfigureStartingPawns; + if (originalStartingPawnsPart != null) { + ScenPart_ConfigPage_ConfigureStartingPawns actualStartingPawnsPart = UtilityCopy.CopyExposable(originalStartingPawnsPart); + int pawnCount = PrepareCarefully.Instance.ColonyPawns.Count; + actualStartingPawnsPart.pawnCount = pawnCount; + actualStartingPawnsPart.pawnChoiceCount = pawnCount; + actualParts.Add(actualStartingPawnsPart); + vanillaFriendlyParts.Add(actualStartingPawnsPart); } // Sort the equipment from highest count to lowest so that gear is less likely to get blocked @@ -339,7 +313,8 @@ protected void ReplaceScenarioParts(Scenario originalScenario, Scenario actualSc // Create all of the scatter things scenario parts that we need. Make note of the maximum number of stacks // that could be created. We must use a custom scatter scenario part because we need to customize the spawn // radius when there are large numbers of resources. - List scatterParts = new List(); + //List scatterParts = new List(); + List scatterParts = new List(); int scatterStackCount = 0; foreach (var e in PrepareCarefully.Instance.Equipment) { if (e.record.animal) { @@ -348,27 +323,16 @@ protected void ReplaceScenarioParts(Scenario originalScenario, Scenario actualSc if (!PlayerStartsWith(e)) { int stacks = Mathf.CeilToInt((float)e.Count / (float)e.ThingDef.stackLimit); scatterStackCount += stacks; - ScenPart_CustomScatterThingsNearPlayerStart part = new ScenPart_CustomScatterThingsNearPlayerStart(); - part.ThingDef = e.ThingDef; - part.StuffDef = e.StuffDef; - part.Count = e.Count; - scatterParts.Add(part); - - ScenPart_ScatterThingsNearPlayerStart vanillaPart = new ScenPart_ScatterThingsNearPlayerStart(); - vanillaPart.def = ScenPartDefOf.ScatterThingsNearPlayerStart; - vanillaPart.SetPrivateField("thingDef", e.ThingDef); - vanillaPart.SetPrivateField("stuff", e.StuffDef); - vanillaPart.SetPrivateField("count", e.Count); - vanillaFriendlyScenarioParts.Add(vanillaPart); + ScenPart_ScatterThingsNearPlayerStart part = new ScenPart_ScatterThingsNearPlayerStart(); + part.def = ScenPartDefOf.ScatterThingsNearPlayerStart; + part.SetPrivateField("thingDef", e.ThingDef); + part.SetPrivateField("stuff", e.StuffDef); + part.SetPrivateField("count", e.Count); + actualParts.Add(part); + vanillaFriendlyParts.Add(part); } } - // Get the non-public fields that we'll need to set on the new starting thing scenario parts - // that we're going to add. - FieldInfo thingDefField = typeof(ScenPart_StartingThing_Defined).GetField("thingDef", BindingFlags.Instance | BindingFlags.NonPublic); - FieldInfo stuffField = typeof(ScenPart_StartingThing_Defined).GetField("stuff", BindingFlags.Instance | BindingFlags.NonPublic); - FieldInfo countField = typeof(ScenPart_StartingThing_Defined).GetField("count", BindingFlags.Instance | BindingFlags.NonPublic); - // Go through all of the equipment that's meant to spawn as a starting thing. We'll try to add // a starting thing scenario part for each, but we're keeping track of the overall density of // things that will be spawned into the area (based on stack count and spawn area radius). If @@ -402,26 +366,21 @@ protected void ReplaceScenarioParts(Scenario originalScenario, Scenario actualSc // cause null pointer exceptions when trying to sort the scenario parts when creating the // description to display in the "Scenario Summary." part.def = ScenPartDefOf.StartingThing_Defined; - thingDefField.SetValue(part, e.ThingDef); - stuffField.SetValue(part, e.StuffDef); - countField.SetValue(part, nearCount); - actualScenarioParts.Add(part); - vanillaFriendlyScenarioParts.Add(part); + part.SetPrivateField("thingDef", e.ThingDef); + part.SetPrivateField("stuff", e.StuffDef); + part.SetPrivateField("count", nearCount); + actualParts.Add(part); + vanillaFriendlyParts.Add(part); } if (scatterCount > 0) { scatterCount += Mathf.CeilToInt((float)scatterCount / (float)e.ThingDef.stackLimit); - ScenPart_CustomScatterThingsNearPlayerStart part = new ScenPart_CustomScatterThingsNearPlayerStart(); - part.ThingDef = e.ThingDef; - part.StuffDef = e.StuffDef; - part.Count = scatterCount; - scatterParts.Add(part); - - ScenPart_ScatterThingsNearPlayerStart vanillaPart = new ScenPart_ScatterThingsNearPlayerStart(); - vanillaPart.def = ScenPartDefOf.ScatterThingsNearPlayerStart; - vanillaPart.SetPrivateField("thingDef", e.ThingDef); - vanillaPart.SetPrivateField("stuff", e.StuffDef); - vanillaPart.SetPrivateField("count", scatterCount); - vanillaFriendlyScenarioParts.Add(vanillaPart); + ScenPart_ScatterThingsNearPlayerStart part = new ScenPart_ScatterThingsNearPlayerStart(); + part.def = ScenPartDefOf.ScatterThingsNearPlayerStart; + part.SetPrivateField("thingDef", e.ThingDef); + part.SetPrivateField("stuff", e.StuffDef); + part.SetPrivateField("count", scatterCount); + actualParts.Add(part); + vanillaFriendlyParts.Add(part); } } } @@ -432,11 +391,12 @@ protected void ReplaceScenarioParts(Scenario originalScenario, Scenario actualSc foreach (var e in PrepareCarefully.Instance.Equipment) { if (e.record.animal) { PawnKindDef animalKindDef = (from td in DefDatabase.AllDefs where td.race == e.ThingDef select td).FirstOrDefault(); - ScenPart_CustomAnimal part = new ScenPart_CustomAnimal(); - part.Count = e.count; - part.Gender = e.Gender; - part.KindDef = animalKindDef; - actualScenarioParts.Add(part); + ScenPart_CustomAnimal part = new ScenPart_CustomAnimal() { + Count = e.count, + Gender = e.Gender, + KindDef = animalKindDef + }; + actualParts.Add(part); if (animalKindCounts.ContainsKey(animalKindDef)) { int count = animalKindCounts[animalKindDef]; @@ -447,44 +407,19 @@ protected void ReplaceScenarioParts(Scenario originalScenario, Scenario actualSc } } } - + // The vanilla starting animal part does not distinguish between genders, so we combine // the custom parts into a single vanilla part for each animal kind. foreach (var animalKindDef in animalKindCounts.Keys) { - ScenPart_StartingAnimal vanillaPart = new ScenPart_StartingAnimal(); - vanillaPart.def = ScenPartDefOf.StartingAnimal; + ScenPart_StartingAnimal vanillaPart = new ScenPart_StartingAnimal() { + def = ScenPartDefOf.StartingAnimal + }; vanillaPart.SetPrivateField("animalKind", animalKindDef); vanillaPart.SetPrivateField("count", animalKindCounts[animalKindDef]); - vanillaFriendlyScenarioParts.Add(vanillaPart); - } - - // We figure out how dense the spawn area will be after spawning all of the scattered things. - // We'll target a maximum density and increase the spawn radius if we're over that density. - stackCount += scatterStackCount; - float originalRadius = 12f; - radius = originalRadius; - maxDensity = 0.35f; - bool evaluate = true; - while (evaluate) { - float density = GetSpawnAreaDensity(radius, stackCount); - if (density > maxDensity) { - radius += 1f; - } - else { - evaluate = false; - } - } - int addedRadius = (int)(radius - originalRadius); - - // For each scatter part, we set our custom radius before adding the part to the scenario. - foreach (var part in scatterParts) { - part.Radius = addedRadius; - actualScenarioParts.Add(part); + vanillaFriendlyParts.Add(vanillaPart); } - // Set the new part lists on the two scenarios. - actualScenario.SetPrivateField("parts", actualScenarioParts); - vanillaFriendlyScenario.SetPrivateField("parts", vanillaFriendlyScenarioParts); + return Tuple.Create(actualParts, vanillaFriendlyParts); } protected float GetSpawnAreaDensity(float radius, float stackCount) { diff --git a/Source/ExtensionsEnumerable.cs b/Source/ExtensionsEnumerable.cs new file mode 100644 index 0000000..2fff2f0 --- /dev/null +++ b/Source/ExtensionsEnumerable.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace EdB.PrepareCarefully { + public static class ExtensionsEnumerable { + public static string CommaDelimitedList(this IEnumerable e) { + if (e == null) { + return "null"; + } + return string.Join(", ", e); + } + } +} diff --git a/Source/HarmonyPatches.cs b/Source/HarmonyPatches.cs index e2b90e4..15d0f92 100644 --- a/Source/HarmonyPatches.cs +++ b/Source/HarmonyPatches.cs @@ -50,8 +50,8 @@ static void Postfix() { class ReplaceScenarioPatch { [HarmonyPostfix] static void Postfix() { - if (PrepareCarefully.OriginalScenario != null) { - Current.Game.Scenario = PrepareCarefully.OriginalScenario; + if (PrepareCarefully.OriginalScenarioParts != null) { + Current.Game.Scenario.SetPrivateField("parts", PrepareCarefully.OriginalScenarioParts); PrepareCarefully.ClearOriginalScenario(); } } diff --git a/Source/PrepareCarefully.cs b/Source/PrepareCarefully.cs index d1ba3c3..56448cd 100644 --- a/Source/PrepareCarefully.cs +++ b/Source/PrepareCarefully.cs @@ -67,12 +67,12 @@ public State State { } } - public static Scenario OriginalScenario { + public static List OriginalScenarioParts { get; set; } public static void ClearOriginalScenario() { - OriginalScenario = null; + OriginalScenarioParts = null; } public Providers Providers { @@ -123,7 +123,7 @@ public void DoNextInBasePage() { } public void Clear() { - OriginalScenario = null; + ClearOriginalScenario(); this.Active = false; this.Providers = new Providers(); this.equipmentDatabase = new EquipmentDatabase(); @@ -208,8 +208,11 @@ protected void InitializeDefaultEquipment() { foreach (ScenPart part in Verse.Find.Scenario.AllParts) { index++; - ScenPart_ScatterThingsNearPlayerStart nearPlayerStart = part as ScenPart_ScatterThingsNearPlayerStart; - if (nearPlayerStart != null) { + if (part is ScenPart_ConfigPage_ConfigureStartingPawns) { + ReplacedScenarioParts.Add(part); + } + + if (part is ScenPart_ScatterThingsNearPlayerStart nearPlayerStart) { FieldInfo thingDefField = typeof(ScenPart_ScatterThingsNearPlayerStart).GetField("thingDef", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo stuffDefField = typeof(ScenPart_ScatterThingsNearPlayerStart).GetField("stuff", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo countField = typeof(ScenPart_ScatterThingsNearPlayerStart).GetField("count", BindingFlags.Instance | BindingFlags.NonPublic); @@ -232,8 +235,7 @@ record = AddNonStandardScenarioEquipmentEntry(key); // Go through all of the scenario steps that place starting equipment with the colonists and // add them to the resource/equipment list. - ScenPart_StartingThing_Defined startingThing = part as ScenPart_StartingThing_Defined; - if (startingThing != null) { + if (part is ScenPart_StartingThing_Defined startingThing) { FieldInfo thingDefField = typeof(ScenPart_StartingThing_Defined).GetField("thingDef", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo stuffDefField = typeof(ScenPart_StartingThing_Defined).GetField("stuff", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo countField = typeof(ScenPart_StartingThing_Defined).GetField("count", BindingFlags.Instance | BindingFlags.NonPublic); @@ -259,8 +261,7 @@ record = AddNonStandardScenarioEquipmentEntry(key); // Go through all of the scenario steps that spawn a pet and add the pet to the equipment/resource // list. - ScenPart_StartingAnimal animal = part as ScenPart_StartingAnimal; - if (animal != null) { + if (part is ScenPart_StartingAnimal animal) { FieldInfo animalCountField = typeof(ScenPart_StartingAnimal).GetField("count", BindingFlags.Instance | BindingFlags.NonPublic); int count = (int)animalCountField.GetValue(animal); for (int i = 0; i < count; i++) {