Ejemplo n.º 1
0
        public void CleanupHandFootSurgeryRecipes(List <RecipeDef> surgeryList)
        {
            Base XP = Base.Instance;

            // Try to clean up the more obvious hand/foot cross-connections on humanlikes
            foreach (RecipeDef surgery in surgeryList.Where(s => s.targetsBodyPart && s.appliedOnFixedBodyParts.Count > 0))
            {
                string surgeryLabelLower = surgery.label.ToLower();

                if (surgeryLabelLower.Contains(" foot ") || surgeryLabelLower.EndsWith(" foot"))
                {
                    surgery.appliedOnFixedBodyParts.RemoveAll(sbp => BodyPartMatcher.SimplifyBodyPartLabel(sbp) == "hand");
                }
                else if (surgeryLabelLower.Contains(" hand ") || surgeryLabelLower.EndsWith(" hand"))
                {
                    surgery.appliedOnFixedBodyParts.RemoveAll(sbp => BodyPartMatcher.SimplifyBodyPartLabel(sbp) == "foot");
                }

                // This shouldn't happen
                if (surgery.appliedOnFixedBodyParts.Count == 0)
                {
                    XP.ModLogger.Error("Cleaning up hand/foot surgeries for {0}, but ended up removing all the body parts!", surgery.LabelCap);
                }
            }
        }
Ejemplo n.º 2
0
        /* WARNING: Because of the sheer amount of combinations and loops we're dealing with, there is a LOT
         * of caching (both here and within Helpers), HashSets (for duplicate checks), and stopwatch timing.
         * Everything needs be optimized to the Nth degree to reduce as much overhead as possible.
         */

        public void InjectSurgeryRecipes(List <RecipeDef> surgeryList, List <ThingDef> pawnList)
        {
            Base XP = Base.Instance;

            Stopwatch stopwatch = Stopwatch.StartNew();

            /* Many mods like to use different def names for basic body parts.  This makes it harder to add the
             * surgery recipe to the alien.  We'll need to add in the similar body part to the
             * appliedOnFixedBodyParts list first.
             *
             * First, look through the list of common surgery recipes to infer the part type.  In other words, if
             * there's a surgery called "Install bionic arm" then that part is an arm that can accept other kinds of
             * arms.  Then, also look for body part matches by looking at the body part labels directly (basically
             * duck typing).
             *
             * These's all go into the part mapper for later injection.
             */
            var partToPartMapper = new Dictionary <string, HashSet <BodyPartDef> > {
            };

            // There are only a few pawn bio-types, so compile all of the pawn surgery lists outside of the
            // main surgery double-loop.
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }

            var pawnSurgeriesByBioType = new Dictionary <string, HashSet <RecipeDef> > {
            };

            foreach (ThingDef pawn in pawnList.Where(p => p.recipes != null))
            {
                string pawnBioType = Helpers.GetPawnBioType(pawn);
                pawnSurgeriesByBioType.SetOrAddNestedRange(pawnBioType, pawn.recipes);
            }

            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    PawnSurgeriesByBioType cache: took {0:F4}s; {1:N0}/{2:N0} keys/recipes",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    pawnSurgeriesByBioType.Keys.Count(), pawnSurgeriesByBioType.Values.Sum(h => h.Count())
                    );
                stopwatch.Reset();
            }

            // This list is used a few times.  Best to compose it outside the loops.  Distinct is important
            // because there's a lot of dupes.
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }

            List <BodyPartRecord> raceBodyParts =
                pawnList.
                Where(p => p.race?.body != null).        // no idea; NRE bulletproofing because of PawnMorpher?
                Select(p => p.race.body).Distinct().
                SelectMany(bd => bd.AllParts).Distinct().
                ToList()
            ;

            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    RaceBodyParts cache: took {0:F4}s; {1:N0} BPRs",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    raceBodyParts.Count()
                    );
                stopwatch.Reset();
            }

            // Both of these are useful in surgery->pawn body part matches
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }

            var doesPawnHaveSurgery  = new HashSet <string> {
            };
            var doesPawnHaveBodyPart = new HashSet <string> {
            };

            foreach (ThingDef pawn in pawnList)
            {
                if (pawn.recipes != null)
                {
                    doesPawnHaveSurgery.AddRange(
                        pawn.recipes.Select(
                            s => pawn.defName + "|" + s.label.ToLower()
                            )
                        );
                }
                if (pawn.race?.body != null)
                {
                    doesPawnHaveBodyPart.AddRange(
                        pawn.race.body.AllParts.Distinct().Select(
                            bpr => pawn.defName + "|" + bpr.def.defName
                            )
                        );
                }
            }

            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    DoesPawnHaveSurgery + BodyPart caches: took {0:F4}s; {1:N0} + {2:N0} strings",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    doesPawnHaveSurgery.Count(), doesPawnHaveBodyPart.Count()
                    );
                stopwatch.Reset();
            }

            /* Start with a hard-coded list, just in case any of these don't match.  This is especially helpful for
             * animals, since they don't always have obvious humanlike analogues.  This also works as a part group
             * separator to ensure parts don't get mixed into the wrong groups.
             */
            string staticPartSetString =
                // Basics
                "Arm Shoulder Hand Finger Foot Toe Eye Ear Nose Jaw Head Brain Torso Heart Lung Kidney Liver Stomach Neck" + ' ' +
                // Animal parts
                "Elytra Tail Horn Tusk Trunk" + ' ' +
                // Bones
                "Skull Ribcage Spine Clavicle Sternum Humerus Radius Pelvis Femur Tibia"
            ;
            Dictionary <string, string[]> staticPartGroups = staticPartSetString.Split(' ').ToDictionary(
                keySelector:     k => k,
                elementSelector: k => new[] { k.ToLower() }
                );

            var additionalStaticPartGroups = new Dictionary <string, string[]> {
                { "Arm", new[] { "flipper" } },
                { "Hand", new[] { "claw", "grasper", "pincer" } },
                { "Finger", new[] { "thumb", "pinky" } },
                { "Foot", new[] { "hoof", "paw" } },
                { "Eye", new[] { "sight", "seeing", "visual" } },
                { "Ear", new[] { "antenna", "hear", "hearing", "sound" } },
                { "Nose", new[] { "nostril", "smell", "smelling" } },
                { "Jaw", new[] { "beak", "mouth", "maw", "teeth", "mandible" } },
                { "Torso", new[] { "thorax", "body", "shell" } },
                { "Heart", new[] { "reactor" } },
                { "Neck", new[] { "pronotum" } },
                // Wing should really be the base name, but there is no vanilla Wing part (even for birds!)
                { "Elytra", new[] { "wing" } },
            };

            foreach (string vanillaPartName in additionalStaticPartGroups.Keys)
            {
                staticPartGroups.SetOrAddNestedRange(vanillaPartName, additionalStaticPartGroups[vanillaPartName]);
            }

            /* It's futile to try to separate the hand/foot connection, as animals have "hands" which also
             * sometimes double as feet.  We can try to clean this up later in CleanupHandFootSurgeryRecipes.
             *
             * We're still going to keep the bio-boundary below to keep out leg->hand connections.  That's still a
             * bit off.  And mechs, of course.
             */
            staticPartGroups["Hand"].AddRangeToArray(staticPartGroups["Foot"]);
            staticPartGroups["Foot"] = staticPartGroups["Hand"];

            // Initialize part mapper with the vanilla part
            foreach (string vanillaPartName in staticPartGroups.Keys)
            {
                partToPartMapper.Add(
                    vanillaPartName,
                    new HashSet <BodyPartDef> {
                    DefDatabase <BodyPartDef> .GetNamed(vanillaPartName)
                }
                    );
            }

            // Static part loop
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }
            foreach (BodyPartRecord raceBodyPart in raceBodyParts)
            {
                // Try really hard to only match one vanilla part group
                foreach (partMatchType matchType in Enum.GetValues(typeof(partMatchType)))
                {
                    var partGroupMatched = new Dictionary <string, bool> {
                    };
                    foreach (string vanillaPartName in staticPartGroups.Keys)
                    {
                        partGroupMatched.Add(
                            vanillaPartName,
                            staticPartGroups[vanillaPartName].Any(fuzzyPartName => fuzzyPartName == (
                                                                      matchType == partMatchType.BodyPartRecord ? BodyPartMatcher.SimplifyBodyPartLabel(raceBodyPart) :
                                                                      matchType == partMatchType.BodyPartDef    ? BodyPartMatcher.SimplifyBodyPartLabel(raceBodyPart.def) :
                                                                      matchType == partMatchType.DefName        ? BodyPartMatcher.SimplifyBodyPartLabel(raceBodyPart.def.defName) :
                                                                      matchType == partMatchType.LabelShort     ? BodyPartMatcher.SimplifyBodyPartLabel(raceBodyPart.LabelShort) :
                                                                      matchType == partMatchType.Label          ? BodyPartMatcher.SimplifyBodyPartLabel(raceBodyPart.Label) :
                                                                      "" // ??? Forgot to add a partMatchType?
                                                                      ))
                            );
                    }

                    // Only stop to add if there's a conclusive singular part matched
                    int partGroupMatches = staticPartGroups.Keys.Sum(k => partGroupMatched[k] ? 1 : 0);
                    if (partGroupMatches == 1)
                    {
                        string      vanillaPartName = partGroupMatched.Keys.First(k => partGroupMatched[k]);
                        BodyPartDef racePartDef     = raceBodyPart.def;

                        // Add to both sides
                        partToPartMapper[vanillaPartName].Add(racePartDef);
                        partToPartMapper.SetOrAddNested(
                            racePartDef.defName,
                            partToPartMapper[vanillaPartName].First(bpd => bpd.defName == vanillaPartName)
                            );
                        break;
                    }
                    else if (partGroupMatches == 0)
                    {
                        // It's never going to match on other loops, so just stop here
                        break;
                    }
                }
            }
            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    Static part loop: took {0:F4}s; {1:N0}/{2:N0} PartToPartMapper keys/BPDs",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    partToPartMapper.Keys.Count(), partToPartMapper.Values.Sum(h => h.Count())
                    );
                stopwatch.Reset();
            }

            // Part-to-part mapping
            // (This is actually fewer combinations than all of the duplicates within
            // surgeryList -> appliedOnFixedBodyParts.)
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }
            var simpleLabelToBPDMapping = new Dictionary <string, HashSet <BodyPartDef> > {
            };

            raceBodyParts.ForEach(bpr => simpleLabelToBPDMapping.SetOrAddNested(
                                      key:   BodyPartMatcher.SimplifyBodyPartLabel(bpr),
                                      value: bpr.def
                                      ));

            foreach (HashSet <BodyPartDef> similarBPDs in simpleLabelToBPDMapping.Values.Where(hs => hs.Count() >= 2))
            {
                similarBPDs.
                Select(bpd => bpd.defName).
                Do(defName => partToPartMapper.SetOrAddNestedRange(defName, similarBPDs))
                ;
            }

            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    Part-to-part mapping: took {0:F4}s; {1:N0}/{2:N0} PartToPartMapper keys/BPDs",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    partToPartMapper.Keys.Count(), partToPartMapper.Values.Sum(h => h.Count())
                    );
                stopwatch.Reset();
            }

            // Surgery-to-part mapping
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }

            foreach (RecipeDef surgery in surgeryList.Where(s => s.targetsBodyPart))
            {
                string surgeryBioType    = Helpers.GetSurgeryBioType(surgery);
                string surgeryLabelLower = surgery.label.ToLower();

                if (!pawnSurgeriesByBioType.ContainsKey(surgeryBioType))
                {
                    continue;
                }

                // Compose this list outside of the surgeryBodyPart loop
                HashSet <BodyPartDef> pawnSurgeryBodyParts =
                    // We can't cross the animal/humanlike boundary with these checks because animal surgery recipes tend to be a lot
                    // looser with limbs (ie: power claws on animal legs)
                    pawnSurgeriesByBioType[surgeryBioType].
                    Where(s => s.targetsBodyPart && s != surgery && s.defName != surgery.defName && s.label.ToLower() == surgeryLabelLower).
                    SelectMany(s => s.appliedOnFixedBodyParts).Distinct().
                    ToHashSet()
                ;
                if (pawnSurgeryBodyParts.Count == 0)
                {
                    continue;
                }

                /* If this list is crossing a bunch of our static part group boundaries, we should skip it.
                 * RoM's Druid Regrowth recipe is one such example that tends to pollute the whole bunch.
                 */
                int partGroupMatches = staticPartGroups.Keys.Sum(k =>
                                                                 partToPartMapper[k].Overlaps(pawnSurgeryBodyParts) || partToPartMapper[k].Overlaps(surgery.appliedOnFixedBodyParts) ? 1 : 0
                                                                 );
                if (partGroupMatches >= 2)
                {
                    continue;
                }

                // Look for matching surgery labels, and map them to similar body parts
                bool warnedAboutLargeSet = false;
                foreach (BodyPartDef surgeryBodyPart in surgery.appliedOnFixedBodyParts)
                {
                    string sbpDefName = surgeryBodyPart.defName;
                    partToPartMapper.NewIfNoKey(sbpDefName);

                    // Useful to warn when it's about to add a bunch of parts into a recipe at one time
                    HashSet <BodyPartDef> diff = pawnSurgeryBodyParts.Except(partToPartMapper[sbpDefName]).ToHashSet();
                    if (diff.Count() > 10 && !warnedAboutLargeSet)
                    {
                        XP.ModLogger.Warning(
                            "Mapping a large set of body parts from \"{0}\":\nSurgery parts: {1}\nCurrent mapper parts: {2} ==> {3}\nNew mapper parts: {4}",
                            surgery.LabelCap,
                            string.Join(", ", surgery.appliedOnFixedBodyParts.Select(bpd => bpd.defName)),
                            sbpDefName, string.Join(", ", partToPartMapper[sbpDefName].Select(bpd => bpd.defName)),
                            string.Join(", ", diff.Select(bpd => bpd.defName))
                            );
                        warnedAboutLargeSet = true;
                    }

                    partToPartMapper[sbpDefName].AddRange(
                        pawnSurgeryBodyParts.Where(bp => bp != surgeryBodyPart && bp.defName != sbpDefName)
                        );
                }
            }
            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    Surgery-to-part mapping: took {0:F4}s; {1:N0}/{2:N0} PartToPartMapper keys/BPDs",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    partToPartMapper.Keys.Count(), partToPartMapper.Values.Sum(h => h.Count())
                    );
                stopwatch.Reset();
            }

            // Clear out empty lists
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }

            foreach (string part in partToPartMapper.Keys)
            {
                if (partToPartMapper[part].Count < 1)
                {
                    partToPartMapper.Remove(part);
                }
            }

            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    Empty list cleanup: took {0:F4}s; {1:N0}/{2:N0} PartToPartMapper keys/BPDs",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    partToPartMapper.Keys.Count(), partToPartMapper.Values.Sum(h => h.Count())
                    );
                stopwatch.Reset();
            }

            // With the parts mapped, add new body parts to existing recipes
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }

            int newPartsAdded = 0;

            foreach (RecipeDef surgery in surgeryList.Where(s => s.targetsBodyPart))
            {
                var newPartSet = new HashSet <BodyPartDef> {
                };
                foreach (BodyPartDef surgeryBodyPart in surgery.appliedOnFixedBodyParts)
                {
                    if (partToPartMapper.ContainsKey(surgeryBodyPart.defName))
                    {
                        newPartSet.AddRange(partToPartMapper[surgeryBodyPart.defName]);
                    }
                }

                List <BodyPartDef> AOFBP = surgery.appliedOnFixedBodyParts;
                if (newPartSet.Count() >= 1 && !newPartSet.IsSubsetOf(AOFBP))
                {
                    newPartSet.ExceptWith(AOFBP);
                    AOFBP.AddRange(newPartSet);
                    newPartsAdded += newPartSet.Count();
                }
            }

            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    Add new body parts to surgeries: took {0:F4}s; {1:N0} additions",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    newPartsAdded
                    );
                stopwatch.Reset();
            }

            // Apply relevant missing surgery options to all pawn Defs
            if (Base.IsDebug)
            {
                stopwatch.Start();
            }

            int newSurgeriesAdded = 0;

            foreach (RecipeDef surgery in surgeryList)
            {
                string surgeryLabelLower = surgery.label.ToLower();

                foreach (ThingDef pawnDef in pawnList.Where(p =>
                                                            // If it already exists, don't add it
                                                            !doesPawnHaveSurgery.Contains(p.defName + "|" + surgeryLabelLower)
                                                            ))
                {
                    bool shouldAddSurgery = false;

                    // If it's an administer recipe, add it
                    if (!surgery.targetsBodyPart)
                    {
                        shouldAddSurgery = true;
                    }

                    // If it targets a body part, but nothing specific, add it
                    else if (surgery.targetsBodyPart && surgery.appliedOnFixedBodyParts.Count() == 0 && surgery.appliedOnFixedBodyPartGroups.Count() == 0)
                    {
                        shouldAddSurgery = true;
                    }

                    // XXX: Despite my best efforts, this step is still mapping hand/foot surgeries together...

                    // If it targets any body parts that exist within the pawn, add it
                    else if (surgery.targetsBodyPart && surgery.appliedOnFixedBodyParts.Count() >= 1 && surgery.appliedOnFixedBodyParts.Any(sbp =>
                                                                                                                                            doesPawnHaveBodyPart.Contains(pawnDef.defName + "|" + sbp.defName)
                                                                                                                                            ))
                    {
                        shouldAddSurgery = true;
                    }

                    if (shouldAddSurgery)
                    {
                        newSurgeriesAdded++;
                        if (pawnDef.recipes == null)
                        {
                            pawnDef.recipes = new List <RecipeDef> {
                                surgery
                            }
                        }
                        ; else
                        {
                            pawnDef.recipes.Add(surgery);
                        }
                        if (surgery.recipeUsers == null)
                        {
                            surgery.recipeUsers = new List <ThingDef>  {
                                pawnDef
                            }
                        }
                        ; else
                        {
                            surgery.recipeUsers.Add(pawnDef);
                        }
                    }
                }
            }
            if (Base.IsDebug)
            {
                stopwatch.Stop();
                XP.ModLogger.Message(
                    "    Add new surgeries to pawns: took {0:F4}s; {1:N0} additions",
                    stopwatch.ElapsedMilliseconds / 1000f,
                    newSurgeriesAdded
                    );
                stopwatch.Reset();
            }
        }