using HarmonyLib; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using RimWorld; using Verse; using AlienRace; using Rimworld_Animations; using rjw; namespace Rimworld_Animations_Patch { [StaticConstructorOnStartup] public static class HarmonyPatch_Rimworld_Animations { static HarmonyPatch_Rimworld_Animations() { (new Harmony("Rimworld_Animations_Patch")).Patch(AccessTools.Method(AccessTools.TypeByName("Rimworld_Animations.AnimationUtility"), "GenitalCheckForPawn"), postfix: new HarmonyMethod(AccessTools.Method(typeof(HarmonyPatch_Rimworld_Animations), "PostFix_AnimationUtility_GenitalCheckForPawn"))); (new Harmony("Rimworld_Animations_Patch")).Patch(AccessTools.Method(AccessTools.TypeByName("Rimworld_Animations.CompBodyAnimator"), "setAnchor", new Type[] { typeof(Thing) }), prefix: new HarmonyMethod(AccessTools.Method(typeof(HarmonyPatch_Rimworld_Animations), "Prefix_HarmonyPatch_CompBodyAnimator_setAnchor"))); (new Harmony("Rimworld_Animations_Patch")).Patch(AccessTools.Method(AccessTools.TypeByName("Rimworld_Animations.HarmonyPatch_JobDriver_SexBaseInitiator_Start"), "RerollAnimations"), postfix: new HarmonyMethod(AccessTools.Method(typeof(HarmonyPatch_Rimworld_Animations), "Postfix_RerollAnimations"))); (new Harmony("Rimworld_Animations_Patch")).Patch(AccessTools.Method(AccessTools.TypeByName("Rimworld_Animations.HarmonyPatch_AlienRace"), "Prefix_AnimateHeadAddons"), prefix: new HarmonyMethod(AccessTools.Method(typeof(HarmonyPatch_Rimworld_Animations), "Prefix_DrawAddons"))); } // Extend the animation selector's body part check to include hands and whether the pawn is in bed or not public static void PostFix_AnimationUtility_GenitalCheckForPawn(ref bool __result, List requiredGenitals, Pawn pawn, ref string failReason) { int handCount = 0; bool pawnInBed = pawn.IsInBed(out Building bed); var hands = pawn.health.hediffSet.GetNotMissingParts().Where(x => x.def.defName == "Hand"); if (hands != null) { handCount = hands.Count(); } if (requiredGenitals.NullOrEmpty()) { return; } if (requiredGenitals.Contains("OneHand") && handCount < 1) { failReason = "missing hand"; __result = false; } if (requiredGenitals.Contains("TwoHands") && handCount < 2) { failReason = "missing hand(s)"; __result = false; } if (requiredGenitals.Contains("Bed") && pawnInBed == false) { failReason = "pawn is not in bed"; __result = false; } if (requiredGenitals.Contains("NoBed") && pawnInBed) { failReason = "pawn is in bed"; __result = false; } } // Override CompBodyAnimator's anchors public static bool Prefix_HarmonyPatch_CompBodyAnimator_setAnchor(CompBodyAnimator __instance, Thing thing) { __instance.anchor = AnimationPatchUtility.GetAnchorPosition(__instance.pawn, thing); return false; } // Adds functionality to determine which apparel each actor should discard based on the animation they are running public static void Postfix_RerollAnimations(Pawn pawn) { AnimationDef anim = pawn.GetAnimationData()?.animationDef; if (anim != null) { DebugMode.Message("Running animation: " + anim.defName); List pawnsToAnimate = pawn.GetAllSexParticipants(); Pawn Target = pawn.GetSexReceiver(); foreach (Pawn participant in pawnsToAnimate) { int actorID = (int)AccessTools.Field(typeof(CompBodyAnimator), "actor").GetValue(participant.TryGetComp()); DebugMode.Message("Participant " + actorID + ": " + participant.NameShortColored); } } } // Determine if a body addon is covered by apparel /*public static bool BodyAddonCoveredByApparel(Pawn pawn, AlienPartGenerator.BodyAddon bodyAddon) { foreach (ApparelGraphicRecord apparelGraphicRecord in pawn.Drawer.renderer.graphics.apparelGraphics) { Apparel apparel = apparelGraphicRecord.sourceApparel; if (apparel.def.apparel.bodyPartGroups.Any(x => bodyAddon.hiddenUnderApparelFor.Contains(x))) { return true; } } return false; }*/ public static bool BodyAddonCoveredByWornApparel(Pawn pawn, AlienPartGenerator.BodyAddon bodyAddon) { if (bodyAddon?.hiddenUnderApparelFor == null || bodyAddon?.hiddenUnderApparelTag == null) { return false; } foreach (Apparel apparel in pawn.apparel.WornApparel) { if (ApparelAnimationUtility.BodyAddonCoveredByApparel(apparel, bodyAddon)) { return true; } } return false; } // Determine if a body addon should be drawn public static bool CanDrawAddon(Pawn pawn, AlienPartGenerator.BodyAddon bodyAddon) { if (bodyAddon == null) { return false; } if (pawn.RaceProps.Animal) { return true; } Building_Bed building_Bed = pawn.CurrentBed(); if ((building_Bed == null || building_Bed.def.building.bed_showSleeperBody || bodyAddon.drawnInBed) && (bodyAddon.backstoryRequirement.NullOrEmpty() || pawn.story.AllBackstories.Any((Backstory x) => x.identifier == bodyAddon.backstoryRequirement))) { if (!bodyAddon.drawnDesiccated) { Corpse corpse = pawn.Corpse; if (corpse != null && corpse.GetRotStage() == RotStage.Dessicated) { return false; } } if (!bodyAddon.bodyPart.NullOrEmpty() && !pawn.health.hediffSet.GetNotMissingParts(BodyPartHeight.Undefined, BodyPartDepth.Undefined, null, null).Any((BodyPartRecord bpr) => bpr.untranslatedCustomLabel == bodyAddon.bodyPart || bpr.def.defName == bodyAddon.bodyPart)) { List list = bodyAddon.hediffGraphics; bool flag; if (list == null) { flag = false; } else { flag = list.Any((AlienPartGenerator.BodyAddonHediffGraphic bahg) => bahg.hediff == HediffDefOf.MissingBodyPart); } if (!flag) { return false; } } if ((pawn.gender == Gender.Female) ? bodyAddon.drawForFemale : bodyAddon.drawForMale) { if (bodyAddon.bodyTypeRequirement.NullOrEmpty() || pawn.story.bodyType.ToString() == bodyAddon.bodyTypeRequirement) { bool renderClothes = true; if (Find.WindowStack.currentlyDrawnWindow is Page_ConfigureStartingPawns) { renderClothes = (bool)AccessTools.Field(typeof(Page_ConfigureStartingPawns), "renderClothes").GetValue(Find.WindowStack.currentlyDrawnWindow); } else { renderClothes = pawn.Drawer.renderer.graphics.apparelGraphics.Count > 0; } bool conditionA = !BodyAddonCoveredByWornApparel(pawn, bodyAddon); bool conditionB = !renderClothes; bool conditionC = pawn.GetPosture() == PawnPosture.Standing; bool conditionD = (pawn.GetPosture() == PawnPosture.LayingOnGroundNormal || pawn.GetPosture() == PawnPosture.LayingOnGroundFaceUp) && bodyAddon.drawnOnGround; bool conditionE = pawn.GetPosture() == PawnPosture.LayingInBed && bodyAddon.drawnInBed; return (conditionA || conditionB) && (conditionC || conditionD || conditionE); } } } return false; } // Replacement patch for AlienRace to draw the body addons public static bool Prefix_DrawAddons(PawnRenderFlags renderFlags, Vector3 vector, Vector3 headOffset, Pawn pawn, Quaternion quat, Rot4 rotation) { if (!(pawn.def is ThingDef_AlienRace alienProps) || renderFlags.FlagSet(PawnRenderFlags.Invisible)) { return false; } // Try to draw apparel thrown on ground if (ApparelSettings.clothesThrownOnGround) { ApparelAnimationUtility.TryToDrawApparelOnFloor(pawn); } // Get components List bodyAddons = alienProps.alienRace.generalSettings.alienPartGenerator.bodyAddons.ToList(); AlienPartGenerator.AlienComp alienComp = pawn.GetComp(); CompBodyAnimator pawnAnimator = pawn.TryGetComp(); // Get available hands var hands = pawn.health.hediffSet.GetNotMissingParts().Where(x => x.def.defName == "Hand"); int handsAvailableCount = hands.Count(); // Sort addons by their layer offset, otherwise body parts will actualy be layered according to their position in the list // Note that sorting the addons directly seems to mess up relations between lists need by AlienRace var sortedBodyAddons = bodyAddons.Select((x, i) => new KeyValuePair(x, i)).OrderBy(x => x.Key.offsets.GetOffset(rotation).layerOffset).ToList(); List idxBodyAddons = sortedBodyAddons.Select(x => x.Value).ToList(); for (int idx = 0; idx < idxBodyAddons.Count; idx++) { int i = idxBodyAddons[idx]; AlienPartGenerator.BodyAddon bodyAddon = bodyAddons[i]; BodyPartRecord bodyPartRecord = AnimationPatchUtility.GetBodyPartRecord(pawn, bodyAddon.bodyPart); bool alignWithHead = bodyAddon.alignWithHead || (bodyPartRecord != null && bodyPartRecord.IsInGroup(BodyPartGroupDefOf.FullHead)); Graphic addonGraphic = alienComp.addonGraphics[i]; //DebugMode.Message(" Trying to draw " + addonGraphic.path); Rot4 apparentRotation = rotation; if (!renderFlags.FlagSet(PawnRenderFlags.Portrait) && pawnAnimator != null && pawnAnimator.isAnimating) { apparentRotation = alignWithHead ? pawnAnimator.headFacing : pawnAnimator.bodyFacing; } AlienPartGenerator.RotationOffset defaultOffsets = bodyAddon.defaultOffsets.GetOffset(apparentRotation); Vector3 bodyTypeOffset = (defaultOffsets != null) ? defaultOffsets.GetOffset(renderFlags.FlagSet(PawnRenderFlags.Portrait), pawn.story.bodyType, alienComp.crownType) : Vector3.zero; AlienPartGenerator.RotationOffset offsets = bodyAddon.offsets.GetOffset(apparentRotation); Vector3 vector2 = bodyTypeOffset + ((offsets != null) ? offsets.GetOffset(renderFlags.FlagSet(PawnRenderFlags.Portrait), pawn.story.bodyType, alienComp.crownType) : Vector3.zero); // Offset private parts so that they render over tattoos but under apparel (rendering under tatoos looks weird) if ((bodyPartRecord != null && (bodyPartRecord.IsInGroup(PatchBodyPartGroupDefOf.GenitalsBPG) || bodyPartRecord.IsInGroup(PatchBodyPartGroupDefOf.ChestBPG) || bodyPartRecord.IsInGroup(PatchBodyPartGroupDefOf.AnusBPG))) || addonGraphic.path.ToLower().Contains("belly")) { vector2.y = (vector2.y + 0.40f) / 1000f + 0.012f; // Erected penises should be drawn over apparel if (pawn.RaceProps.Humanlike && addonGraphic.path.ToLower().Contains("penis") && addonGraphic.path.ToLower().Contains("flaccid") == false && BodyAddonCoveredByWornApparel(pawn, bodyAddon) == false && apparentRotation == Rot4.South) { vector2.y += 0.010f; } } // Otherwise use the standard offsets else { vector2.y = 0.3f + vector2.y; } if (!bodyAddon.inFrontOfBody) { vector2.y *= -1f; } float bodyAddonAngle = bodyAddon.angle; if (apparentRotation == Rot4.North) { if (bodyAddon.layerInvert) { vector2.y = -vector2.y; } bodyAddonAngle = 0f; } if (apparentRotation == Rot4.East) { bodyAddonAngle = -bodyAddonAngle; vector2.x = -vector2.x; } Quaternion addonRotation = quat; Quaternion quatAdditional = Quaternion.identity; float finalAngle = 0; if (!renderFlags.FlagSet(PawnRenderFlags.Portrait) && pawnAnimator != null && pawnAnimator.isAnimating) { if (pawnAnimator.controlGenitalAngle && bodyAddon?.hediffGraphics != null && !bodyAddon.hediffGraphics.NullOrEmpty() && bodyAddon.hediffGraphics[0]?.path != null && (bodyAddon.hediffGraphics[0].path.Contains("Penis") || bodyAddon.hediffGraphics[0].path.Contains("penis"))) { float bodyAngle = pawnAnimator.bodyAngle; addonRotation = Quaternion.AngleAxis(angle: bodyAngle, axis: Vector3.up); float anglePenis = AnimationSettings.controlGenitalRotation ? pawnAnimator.genitalAngle : 0f; anglePenis = anglePenis < 0 ? 360 - (360 % anglePenis) : anglePenis; quatAdditional = Quaternion.AngleAxis(angle: anglePenis, axis: Vector3.up); finalAngle = bodyAngle + anglePenis; } else if (alignWithHead) { float headAngle = pawnAnimator.headAngle; headAngle = headAngle < 0 ? 360 - (360 % headAngle) : headAngle; addonRotation = Quaternion.AngleAxis(angle: headAngle, axis: Vector3.up); finalAngle = pawnAnimator.bodyAngle + headAngle; } else { float bodyAngle = pawnAnimator.bodyAngle; addonRotation = Quaternion.AngleAxis(angle: bodyAngle, axis: Vector3.up); finalAngle = bodyAngle; } } // Fixes 'leaning left' issue with Yayo's animations else if (!renderFlags.FlagSet(PawnRenderFlags.Portrait) && (pawnAnimator == null || pawnAnimator.isAnimating == false)) { float bodyAngle = addonRotation.eulerAngles.y; bodyAngle = bodyAngle < 0 ? 360 - (360 % bodyAngle) : bodyAngle; addonRotation = Quaternion.AngleAxis(angle: bodyAngle, axis: Vector3.up); } if (alignWithHead && bodyAddon.alignWithHead == false) { vector2 -= pawn.Drawer.renderer.BaseHeadOffsetAt(apparentRotation); } Vector3 finalPosition = vector + (alignWithHead ? headOffset : Vector3.zero) + vector2.RotatedBy(angle: Mathf.Acos(f: Quaternion.Dot(a: Quaternion.identity, b: addonRotation)) * 2f * 57.29578f); // Draw the addon if visible if (CanDrawAddon(pawn, bodyAddon)) { GenDraw.DrawMeshNowOrLater(mesh: addonGraphic.MeshAt(rot: apparentRotation), loc: finalPosition, quat: Quaternion.AngleAxis(angle: bodyAddonAngle, axis: Vector3.up) * quatAdditional * addonRotation, mat: addonGraphic.MatAt(rot: apparentRotation), renderFlags.FlagSet(PawnRenderFlags.DrawNow)); } // Draw hand over the body part if required if (BasicSettings.showHands && !renderFlags.FlagSet(PawnRenderFlags.Portrait) && handsAvailableCount > 0) { if (HandAnimationUtility.TryToDrawHand(pawn, addonGraphic.path, finalPosition, finalAngle, rotation, renderFlags)) { handsAvailableCount--; } } } // Body addons are sometimes are not appropriately concealed by long hair, so re-draw the pawn's hair here if (pawn.Drawer.renderer.graphics.headGraphic != null) { var methodInfo = AccessTools.Method(typeof(PawnRenderer), "DrawHeadHair", null, null); Rot4 headFacing = pawnAnimator != null && pawnAnimator.isAnimating && !renderFlags.FlagSet(PawnRenderFlags.Portrait) ? pawnAnimator.headFacing : rotation; float headAngle = pawnAnimator != null && pawnAnimator.isAnimating && !renderFlags.FlagSet(PawnRenderFlags.Portrait) ? pawnAnimator.headAngle : quat.eulerAngles.y; RotDrawMode rotDrawMode = (RotDrawMode)AccessTools.Property(typeof(PawnRenderer), "CurRotDrawMode").GetValue(pawn.Drawer.renderer); methodInfo.Invoke(pawn.Drawer.renderer, new object[] { vector + new Vector3(0f, YOffset_Head, 0f), headOffset, headAngle, headFacing, headFacing, rotDrawMode, renderFlags }); } return false; } // List of potentially useful layer offsets //private const float YOffset_CarriedThingUnder = -0.0028957527f; //private const float YOffset_Align_Behind = 0.0028957527f; //private const float YOffset_Body = 0.008687258f; //private const float YOffset_Interval_Clothes = 0.0028957527f; //private const float YOffset_Shell = 0.02027027f; private const float YOffset_Head = 0.023166021f; private const float YOffset_OnHead = 0.028957527f; //private const float YOffset_PostHead = 0.03185328f; //private const float YOffset_Tattoo = 0.0014478763f; //private const float YOffset_WoundOverlays1 = 0.009687258f; //private const float YOffset_WoundOverlays2 = 0.022166021f; /* Details on the above Body = rootLoc + YOffset_Body; (~ 0.009) Tattoo = rootLoc + YOffset_Body + YOffset_Tattoo; (~ 0.010) BodyAddons (not protruding) = rootLoc + 0.011f; (~0.011) Body wounds (under clothes) = rootLoc + YOffset_WoundOverlays1; (~ 0.010) Apparel (not north) = rootLoc + YOffset_Shell; (~ 0.020) BodyAddons (protruding) = rootLoc + 0.011f + 0.010f; (~0.021) Apparel (north) = rootLoc + YOffset_Head; (~ 0.023) Body wounds (over clothes) = rootLoc + YOffset_WoundOverlays1 + YOffset_WoundOverlays2; (~ 0.03) Head (not north) = rootLoc + YOffset_Head (~ 0.023); Head (north) = rootLoc + YOffset_Shell; (~ 0.020) Face tattoo = rootLoc + YOffset_OnHead - YOffset_Tattoo; (~ 0.028) Head wounds (under clothes) = rootLoc + YOffset_OnHead; (~ 0.029) Hair = rootLoc + YOffset_OnHead; (~ 0.029) Hat (over hair) = rootLoc + YOffset_PostHead; (~ 0.031) */ } }