code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public class AnimationWorker_KeyframesExtended : AnimationWorker_Keyframes
    {

        public AnimationWorker_KeyframesExtended(AnimationDef def, Pawn pawn, AnimationPart part, PawnRenderNode node) : base(def, pawn, part, node)
        {
        }

        public override bool Enabled()
        {
            return true;
        }

        public override Vector3 OffsetAtTick(int tick, PawnDrawParms parms)
        {
            //Todo: Use this for bodyoffsets
            //or maybe make a rendersubworker for it instead
            return base.OffsetAtTick(tick, parms);
        }

        public VoiceTagDef voiceAtTick(int tick)
        {

            Verse.Keyframe keyframe2 = this.part.keyframes[this.part.keyframes.Count - 1];
            foreach (Verse.Keyframe keyframe in this.part.keyframes)
            {
                if (tick == keyframe.tick)
                {
                    VoiceTagDef voiceTag = (keyframe as ExtendedKeyframe).voice;

                    //probability checking null fix
                    if (voiceTag != null && Rand.Chance(voiceTag.probability))
                    {

                        return voiceTag;

                    }

                    return null;
                }

            }

            return null;

        }

        public SoundDef soundAtTick(int tick)
        {

            //Verse.Keyframe keyframe = this.part.keyframes[0];
            Verse.Keyframe keyframe2 = this.part.keyframes[this.part.keyframes.Count - 1];
            foreach (Verse.Keyframe keyframe in this.part.keyframes)
            {
                if (tick == keyframe.tick)
                {
                    return (keyframe as ExtendedKeyframe).sound;
                }

            }

            return null;
        }

        //use extendedkeyframes to determine addon facing
        public Rot4 facingAtTick(int tick)
        {

            //if ticks are < first keyframe tick, just be stuck to first keyframe rot
            if (tick <= this.part.keyframes[0].tick) {

                return (this.part.keyframes[0] as ExtendedKeyframe).rotation;

            }

            //if ticks are > last keyframe tick, just be stuck to last keyframe rot
            if (tick >= this.part.keyframes[this.part.keyframes.Count - 1].tick)
            {

                return (this.part.keyframes[this.part.keyframes.Count - 1] as ExtendedKeyframe).rotation;

            }
            Verse.Keyframe keyframe = this.part.keyframes[0];
            Verse.Keyframe keyframe2 = this.part.keyframes[this.part.keyframes.Count - 1];
            int i = 0;
            while (i < this.part.keyframes.Count)
            {
                if (tick <= this.part.keyframes[i].tick)
                {
                    keyframe2 = this.part.keyframes[i];
                    if (i > 0)
                    {
                        keyframe = this.part.keyframes[i - 1];
                        break;
                    }
                    break;
                }
                else
                {
                    i++;
                }
            }

            return (keyframe as ExtendedKeyframe).rotation;

        }
        public bool visibleAtTick(int tick)
        {
            //if ticks are < first keyframe tick, just be stuck to first keyframe rot
            if (tick <= this.part.keyframes[0].tick)
            {

                return (this.part.keyframes[0] as ExtendedKeyframe).visible;

            }

            //if ticks are > last keyframe tick, just be stuck to last keyframe rot
            if (tick >= this.part.keyframes[this.part.keyframes.Count - 1].tick)
            {

                return (this.part.keyframes[this.part.keyframes.Count - 1] as ExtendedKeyframe).visible;

            }

            Verse.Keyframe keyframe = this.part.keyframes[0];
            Verse.Keyframe keyframe2 = this.part.keyframes[this.part.keyframes.Count - 1];

            int i = 0;
            while (i < this.part.keyframes.Count)
            {
                if (tick <= this.part.keyframes[i].tick)
                {
                    keyframe2 = this.part.keyframes[i];
                    if (i > 0)
                    {
                        keyframe = this.part.keyframes[i - 1];
                        break;
                    }
                    break;
                }
                else
                {
                    i++;
                }
            }

            return (keyframe as ExtendedKeyframe).visible;

        }

        public virtual bool shouldRecache(int tick)
        {
            if (facingAtTick(tick) != facingAtTick(tick - 1)
                || visibleAtTick(tick) != visibleAtTick(tick - 1)
                || TexPathVariantAtTick(tick) != TexPathVariantAtTick(tick - 1))
            {
                return true;
            }

            return true;
        }

        public int? TexPathVariantAtTick(int tick)
        {

            //if ticks are < first keyframe tick, just be stuck to first keyframe rot
            if (tick <= this.part.keyframes[0].tick)
            {

                return (this.part.keyframes[0] as ExtendedKeyframe).variant;

            }

            //if ticks are > last keyframe tick, just be stuck to last keyframe rot
            if (tick >= this.part.keyframes[this.part.keyframes.Count - 1].tick)
            {

                return (this.part.keyframes[this.part.keyframes.Count - 1] as ExtendedKeyframe).variant;

            }
            Verse.Keyframe keyframe = this.part.keyframes[0];
            Verse.Keyframe keyframe2 = this.part.keyframes[this.part.keyframes.Count - 1];
            int i = 0;
            while (i < this.part.keyframes.Count)
            {
                if (tick <= this.part.keyframes[i].tick)
                {
                    keyframe2 = this.part.keyframes[i];
                    if (i > 0)
                    {
                        keyframe = this.part.keyframes[i - 1];
                        break;
                    }
                    break;
                }
                else
                {
                    i++;
                }
            }

            return (keyframe as ExtendedKeyframe).variant;

        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public abstract class BaseGroupAnimationContext
    {
        public int actorShift = 0;
        public int priority = 0;

        public List<BasePawnTest> whitelist;
        public List<BasePawnTest> blacklist;

        public virtual bool CanAnimationBeUsed(List<Pawn> actors, int numActors)
        {

            if (numActors != actors.Count)
            {
                return false;
            }

            if (!whitelist.NullOrEmpty())
            {
                for (int i = 0; i < whitelist.Count; i++)
                {
                    // check whitelist to make sure pawn can be in this act
                    //for each whitelist item, pawntest must hold true for that pawn
                    if (!whitelist[i].PawnTest(actors[i]))
                    {
                        return false;
                    }

                }
            }

            if (!blacklist.NullOrEmpty())
            {
                for (int i = 0; i < blacklist.Count; i++)
                {
                    // check blacklist to make sure pawn can be in this act
                    // for each blacklist item, pawntest must hold false for that pawn
                    if (blacklist[i].PawnTest(actors[i]))
                    {
                        return false;
                    }

                }
            }

            return true;
        }
        public virtual int AnimationReorder()
        {
            return actorShift;
        }
        public virtual int AnimationPriority()
        {
            return priority;
        }
        public abstract string DebugMessage();

        //cool class for designating contexts for animations
        // configure CanAnimationBeUsed to test whether it can be used
    }
}

repo_name: iwontreply/rimworld

license: unknown


code:
using RimWorld;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;
using static rjw.xxx;

namespace Rimworld_Animations
{
    public class GroupAnimationContext_RJWSex : BaseGroupAnimationContext
    {

        public List<InteractionDef> interactionDefs;

        public override bool CanAnimationBeUsed(List<Pawn> actors, int numActors)
        {

            JobDriver_SexBaseInitiator latestSexBaseInitiator = (actors.FindLast(x => x.jobs?.curDriver is JobDriver_SexBaseInitiator).jobs.curDriver as JobDriver_SexBaseInitiator);

            if (!interactionDefs.Contains(latestSexBaseInitiator.Sexprops.dictionaryKey)) {
                return false;
            }

            return base.CanAnimationBeUsed(actors, numActors);
        }

        public override string DebugMessage()
        {
            return "Checking for RJWSex AnimationContext\n"
                + "InteractionDefs: " + interactionDefs;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public class GroupAnimationDef : Def
    {
        public int numActors;
        public List<AnimationStage> animationStages;
        public List<BaseGroupAnimationContext> contexts;
        public List<AnimationOffsetDef> offsetDefs;

        public bool canAnimationBeUsed(List<Pawn> actors)
        {

            if (!contexts.NullOrEmpty())
            {
                foreach (BaseGroupAnimationContext context in contexts)
                {
                    if (context.CanAnimationBeUsed(actors, numActors))
                    {
                        //find all where context matches actors
                        return true;

                    }
                }
            }

            return false;
        }

        public int Priority(List<Pawn> actors)
        {
            int priority = -999999999;

            foreach (BaseGroupAnimationContext context in contexts)
            {
                if (context.CanAnimationBeUsed(actors, numActors))
                {
                    if (context.AnimationPriority() > priority)
                    {
                        //get highest priority context for fitting animation
                        priority = context.AnimationPriority();

                    }

                }
            }

            return priority;

        }

        public int Reorder(List<Pawn> actors)
        {
            int priority = -999999999;
            int reorder = 0;

            foreach (BaseGroupAnimationContext context in contexts)
            {
                if (context.CanAnimationBeUsed(actors, numActors))
                {
                    if (context.AnimationPriority() > priority)
                    {
                        //get the reorder for highest priority context for fitting animation
                        priority = context.AnimationPriority();
                        reorder = context.AnimationReorder();

                    }

                }
            }

            return reorder;

        }

        public List<AnimationDef> GetAllAnimationsForActor(int actor, int seed, int reorder = 0)
        {
            List<AnimationDef> animations = new List<AnimationDef>();
            int actorNumber = (actor + reorder) % numActors;

            foreach (AnimationStage stage in animationStages)
            {
                //add all new animations to list of animations
                animations.AddRange(stage.GetAnimations(actorNumber, seed));
            }

            return animations;
        }

        public bool GetOffset(int actor, Pawn pawn, out Vector3? position, out int? rotation, int reorder = 0)
        {

            position = null;
            rotation = null;

            //element at or default to stop errors
            if (offsetDefs == null) return false;
            if ((actor + reorder) % numActors >= offsetDefs.Count) return false;
            if (offsetDefs[(actor + reorder) % numActors].FindOffset(pawn, out BaseAnimationOffset animationOffset))
            {
                position = animationOffset.getOffset(pawn);
                rotation = animationOffset.getRotation(pawn);
                return true;
            }

            return false;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public abstract class AnimationStage
    {

        //Return a list containing a tuple; int for how long the animation should play for
        public int loops;
        public abstract List<AnimationDef> GetAnimations(int actorNumber, int seed);

    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class AnimationStage_Branch : AnimationStage
    {
        public List<GroupAnimationDef> paths;
        public override List<AnimationDef> GetAnimations(int actorNumber, int seed)
        {
            List<AnimationDef> animationDefs = new List<AnimationDef>();

            for (int i = 0; i < loops; i++)
            {
                GroupAnimationDef selectedDef = paths[Rand.RangeSeeded(0, paths.Count, seed + i)];

                animationDefs.AddRange(selectedDef.GetAllAnimationsForActor(actorNumber, seed));

            }

            return animationDefs;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class AnimationStage_LoopRandomSelectChance : AnimationStage
    {
        public List<AnimationLoopOption> loopOptions;

        public override List<AnimationDef> GetAnimations(int actorNumber, int seed)
        {
            int numberOfActors = loopOptions[0].animationDefs.Count;
            List<AnimationDef> animations = new List<AnimationDef>();

            for (int i = 0; i < loops; i++)
            {
                AnimationLoopOption option = getAnimationLoopOptionByWeight(seed + i);
                animations.Add(option.animationDefs[actorNumber]);
            }

            return animations;
        }

        //select random element from loop options by weight; also calculate the longest anim length
        private AnimationLoopOption getAnimationLoopOptionByWeight(int seed)
        {
            int totalWeight = loopOptions.Sum(x => x.probability);
            int randomNumber = Rand.RangeInclusiveSeeded(0, loopOptions.Count - 1, seed);

            int cumulativeWeight = 0;
            for (int i = 0; i < loopOptions.Count; i++) {

                cumulativeWeight += loopOptions[i].probability;

                //random number is same for all pawns because they all have the same seed

                if (randomNumber <= cumulativeWeight)
                {
                    return loopOptions[i];
                }
            }

            //default
            return loopOptions.Last();
        }
    }

    public class AnimationLoopOption
    {
        public int probability;
        public List<AnimationDef> animationDefs;
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    /* don't use? just use looprandomselect once
    public class AnimationStage_TicksDuration : AnimationStage
    {
        public int ticks;
        public List<AnimationDef> animationDefs;

        public override List<Tuple<int, AnimationDef>> GetAnimations(int actor, int seed)
        {
            return new List<Tuple<int, AnimationDef>>() { Tuple.Create(ticks, animationDefs[actor]) };
        }
    }

    */
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public class ExtendedKeyframe : Verse.Keyframe
    {
        public int? variant;
        public Rot4 rotation = Rot4.North;
        public SoundDef sound = null;
        public VoiceTagDef voice = null;
        public bool visible = false;
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public abstract class BasePawnTest
    {
        public abstract bool PawnTest(Pawn pawn);

    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_Always : BasePawnTest
    {
        public override bool PawnTest(Pawn pawn)
        {
            return true;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_Humanlike : BasePawnTest
    {
        public override bool PawnTest(Pawn pawn)
        {
            return pawn.RaceProps.Humanlike;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_JobDef : BasePawnTest
    {
        public JobDef jobDef;

        public override bool PawnTest(Pawn pawn)
        {
            if (pawn.CurJobDef == jobDef)
            {
                return true;
            }
            return false;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_Multi : BasePawnTest
    {
        public List<BasePawnTest> tests = new List<BasePawnTest>();
        public override bool PawnTest(Pawn pawn)
        {
            //check all different pawn tests in list for pawn
            foreach (BasePawnTest test in tests)
            {
                if (!test.PawnTest(pawn))
                {
                    return false;
                }
            }

            return true;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_Never : BasePawnTest
    {
        public override bool PawnTest(Pawn pawn)
        {
            return false;
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    class PawnTest_PrisonerOfColony : BasePawnTest
    {
        public override bool PawnTest(Pawn pawn)
        {
            return pawn.IsPrisonerOfColony;
        }
    }
}


code:
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_RJWCanBeFucked : BasePawnTest
    {
        public override bool PawnTest(Pawn pawn)
        {
            return xxx.can_be_fucked(pawn);
        }
    }
}


code:
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_RJWCanFuck : BasePawnTest
    {
        public override bool PawnTest(Pawn pawn)
        {
            return xxx.can_fuck(pawn);
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_Race : BasePawnTest
    {

        public List<ThingDef> races = new List<ThingDef>();

        public override bool PawnTest(Pawn pawn)
        {

            foreach (ThingDef race in races)
            {
                if (pawn.def == race)
                {
                    return true;
                }
            }

            return false;

        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    class PawnTest_SlaveOfColony : BasePawnTest
    {
        public override bool PawnTest(Pawn pawn)
        {
            return pawn.IsSlaveOfColony;
        }
    }
}


code:
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnTest_Trait : BasePawnTest
    {
        TraitDef traitDef;
        int? degree;

        public override bool PawnTest(Pawn pawn)
        {

            if (degree != null)
            {
                return pawn.story.traits.HasTrait(traitDef, (int)degree);
            }

            return pawn.story.traits.HasTrait(traitDef);
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using rjw;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;

namespace Rimworld_Animations {
    public class CompExtendedAnimator : ThingComp
    {

        // CompExtendedAnimator
        // Helps manage AnimationQueue, AbsolutePosition
        //ticks of current animation
        private int animationTicks;

        private List<AnimationDef> animationQueue;
        private BaseExtendedAnimatorAnchor anchor;
        private VoiceDef voice;

        private bool isAnimating = false;

        public bool IsAnimating
        {
            get
            {
                return isAnimating;
            }
        }

        public bool IsAnchored
        {
            get
            {
                return anchor != null;
            }
        }

        private Vector3? offset;

        public Vector3? Offset
        {
            get
            {
                return offset;
            }
            set
            {
                this.offset = value;
            }
        }

        private int? rotation;

        public int? Rotation
        {
            get
            {
                return rotation;
            }
            set
            {
                this.rotation = value;
            }
        }

        public int AnimationLength
        {
            get
            {
                if (!IsAnimating) return 0;

                int groupAnimLength = 0;
                foreach(AnimationDef anim in animationQueue)
                {
                    groupAnimLength += anim.durationTicks;
                }

                return groupAnimLength;

            }
        }

        public AnimationDef CurrentAnimation {

            get
            {
                return IsAnimating ? animationQueue[0] : null;
            }

        }

        public override void PostSpawnSetup(bool respawningAfterLoad)
        {
            if (voice == null)
            {
                AssignNewVoice();
            }

        }

        public Vector3 getAnchor()
        {
            return anchor.getDrawPos();
        }

        public override void CompTick()
        {
            if (isAnimating)
            {
                animationTicks++;

                //if animationticks is equal to cur. anim duration,
                if (animationTicks >= animationQueue[0].durationTicks)
                {
                    //dequeue; returns false if more animations
                    if (!PopAnimationQueue())
                    {
                        //play next if more anims still
                        PlayNextAnimation();
                    }
                    else
                    {
                        StopAnimating();
                    }
                }

                CheckAndPlaySounds();

            }

            base.CompTick();
        }

        //returns false if still more animations
        public bool PopAnimationQueue()
        {

            if (!animationQueue.Empty())
            {
                //pop queue
                animationQueue.RemoveAt(0);
            }

            return animationQueue.Empty();
        }

        public void PlayNextAnimation()
        {
            if (!animationQueue.Empty())
            {
                isAnimating = true;
                animationTicks = 0;
                pawn.Drawer.renderer.SetAnimation(animationQueue[0]);
            }
        }

        public void StopAnimating()
        {
            isAnimating = false;
            animationQueue = null;
            anchor = null;
            offset = null;
            pawn.Drawer.renderer.SetAnimation(null);

            pawn.Drawer.renderer.SetAllGraphicsDirty();
        }

        public void PlayGroupAnimation(List<AnimationDef> groupAnimation, Vector3? positionOffset, int? rotationOffset)
        {
            this.Offset = positionOffset;
            this.Rotation = rotationOffset;
            animationQueue = groupAnimation;

            //set all graphics dirty; necessary because sometimes rjw doesn't call during threesomes
            pawn.Drawer.renderer.SetAllGraphicsDirty();

            PlayNextAnimation();
        }

        public void PlayGroupAnimation(List<AnimationDef> groupAnimation, Vector3? positionOffset, int? rotationOffset, BaseExtendedAnimatorAnchor anchor)
        {
            this.anchor = anchor;
            PlayGroupAnimation(groupAnimation, positionOffset, rotationOffset);
        }

        public override void PostExposeData()
        {
            base.PostExposeData();
            Scribe_Values.Look<bool>(ref this.isAnimating, "animations_isAnimating", false);
            Scribe_Values.Look<int>(ref this.animationTicks, "animations_ticks", 0);
            Scribe_Collections.Look<AnimationDef>(ref animationQueue, "animations_queue");
            Scribe_Deep.Look<BaseExtendedAnimatorAnchor>(ref this.anchor, "animations_anchor");
            Scribe_Defs.Look<VoiceDef>(ref this.voice, "animations_voice");

        }

        public override List<PawnRenderNode> CompRenderNodes()
        {
            //only if pawn is animating for performance
            if (IsAnimating)
            {

                List<PawnRenderNode> animRenderNodes = new List<PawnRenderNode>();

                // for all animationpropdefs,
                foreach (AnimationPropDef animationProp in DefDatabase<AnimationPropDef>.AllDefsListForReading)
                {
                    //if animation makes use of prop,
                    if (AnimationMakesUseOfProp(animationProp))
                    {
                        PawnRenderNodeProperties props = animationProp.animPropProperties;

                        if (props.texPath.NullOrEmpty())
                        {

                            props.texPath = "AnimationProps/MissingTexture/MissingTexture";
                        }

                        //create new render node
                        PawnRenderNode animRenderNode = (PawnRenderNode)Activator.CreateInstance(props.nodeClass, new object[] {
                            this.pawn,
                            props,
                            pawn.Drawer.renderer.renderTree
                        });

                        animRenderNodes.Add(animRenderNode);
                    }

                }

                //return list of rendernodes that should animate
                return animRenderNodes;

            }
            else
            {
                return null;
            }

        }

        public void AssignNewVoice()
        {
            //all voice options
            List<VoiceDef> voiceOptions =
                DefDatabase<VoiceDef>.AllDefsListForReading
                .FindAll(voiceDef => voiceDef.VoiceFitsPawn(pawn));

            //all voice options, with priority (for traitdef specific voices)
            List<VoiceDef> voiceOptionsWithPriority =
                voiceOptions.FindAll(voiceDef => voiceDef.takesPriority);

            if (!voiceOptionsWithPriority.NullOrEmpty())
            {
                voice = voiceOptionsWithPriority.RandomElementByWeight(x => x.randomChanceFactor);

            }
            else if (!voiceOptions.NullOrEmpty())
            {
                voice = voiceOptions.RandomElementByWeight(x => x.randomChanceFactor);
            }

        }

        public void CheckAndPlaySounds()
        {

            PawnRenderNode rootNode = pawn.Drawer?.renderer?.renderTree?.rootNode;

            //check if the rootnode has sounds; if so play it
            if (rootNode?.AnimationWorker is AnimationWorker_KeyframesExtended animWorker)
            {
                SoundDef sound = animWorker.soundAtTick(rootNode.tree.AnimationTick);

                if (sound != null)
                {
                    SoundInfo soundInfo = new TargetInfo(pawn.Position, pawn.Map);

                    //temp; does not consider non-rjw animations
                    //todo: replace with value stored in comp or somewhere else?
                    soundInfo.volumeFactor = RJWSettings.sounds_sex_volume;

                    sound.PlayOneShot(soundInfo);
                }

                if (RJWAnimationSettings.playVoices)
                {
                    SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map);
                    voiceInfo.volumeFactor = RJWSettings.sounds_voice_volume;

                    //play voice sounds
                    VoiceTagDef voiceTag = animWorker.voiceAtTick(rootNode.tree.AnimationTick);
                    if (voiceTag != null)
                    {
                        if (voice != null && voice.sounds.ContainsKey(voiceTag))
                        {
                            voice.sounds[voiceTag].PlayOneShot(voiceInfo);
                        }

                        else if (pawn.RaceProps.Humanlike && RJWAnimationSettings.playHumanlikeVoicesAsDefault)
                        {
                            //play default voice
                            VoiceDef pawnDefaultVoice = (pawn.gender == Gender.Male ? VoiceDefOf.Voice_HumanMale : VoiceDefOf.Voice_HumanFemale);
                            if (pawnDefaultVoice.sounds.ContainsKey(voiceTag))
                            {
                                pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo);
                            }

                        }

                    }
                }

            }

            //check rootnodes and children
            if (rootNode?.children != null)
            {
                foreach (PawnRenderNode node in rootNode?.children)
                {
                    if (node?.AnimationWorker is AnimationWorker_KeyframesExtended childrenAnimWorker)
                    {

                        SoundDef sound = childrenAnimWorker.soundAtTick(node.tree.AnimationTick);
                        if (sound != null)
                        {
                            SoundInfo soundInfo = new TargetInfo(pawn.Position, pawn.Map);
                            soundInfo.volumeFactor = RJWSettings.sounds_sex_volume;
                            sound.PlayOneShot(soundInfo);
                        }

                        if (RJWAnimationSettings.playVoices)
                        {

                            SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map);
                            voiceInfo.volumeFactor = RJWSettings.sounds_voice_volume;

                            //play voice sounds
                            VoiceTagDef voiceTag = childrenAnimWorker.voiceAtTick(rootNode.tree.AnimationTick);
                            if (voiceTag != null)
                            {
                                if (voice != null && voice.sounds.ContainsKey(voiceTag))
                                {
                                    voice.sounds[voiceTag].PlayOneShot(voiceInfo);
                                }
                                else if (pawn.RaceProps.Humanlike && RJWAnimationSettings.playHumanlikeVoicesAsDefault)
                                {

                                    VoiceDef pawnDefaultVoice = (pawn.gender == Gender.Male ? VoiceDefOf.Voice_HumanMale : VoiceDefOf.Voice_HumanFemale);
                                    if (pawnDefaultVoice.sounds.ContainsKey(voiceTag))
                                    {
                                        pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            //do the same for all the child nodes

        }

        public bool AnimationMakesUseOfProp(AnimationPropDef animationProp)
        {
            // never true if not animating; anim props shouldn't be attached
            if (!IsAnimating) return false;

            //for all anims in queue (because it's only recached at start)
            foreach (AnimationDef animation in animationQueue)
            {
                foreach (PawnRenderNodeTagDef propTag in animation.animationParts.Keys)
                {

                    // if that proptag is the same as the one for animationProp,
                    if (propTag == animationProp.animPropProperties.tagDef)
                    {
                        //that prop is being used in the animation
                        return true;
                    }
                }

            }

            return false;
        }

        private Pawn pawn => base.parent as Pawn;

    }

}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;
using RimWorld;

namespace Rimworld_Animations {
    public class CompProperties_ExtendedAnimator : CompProperties
    {
        public CompProperties_ExtendedAnimator()
        {
            base.compClass = typeof(CompExtendedAnimator);

        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations {
    public class CompProperties_ThingAnimator : CompProperties
    {

        public CompProperties_ThingAnimator()
        {
            base.compClass = typeof(CompThingAnimator);
        }
    }
}


code:
using RimWorld;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations {
    public class CompThingAnimator : ThingComp
    {

        public override void CompTick()
        {
            //todo: If item is held by pawn, and pawn is doing thingcomp animation,
            //animate thingcomp; see CompPowerPlantWind for how thingcomps are animated
            return;
        }
    }

}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public abstract class BaseExtendedAnimatorAnchor : IExposable
    {
        public BaseExtendedAnimatorAnchor() { }

        public virtual void ExposeData() { }
        public abstract Vector3 getDrawPos();

        public string GetUniqueLoadID()
        {
            throw new NotImplementedException();
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public class ExtendedAnimatorAnchor_Thing : BaseExtendedAnimatorAnchor
    {

        private Thing thing;

        public ExtendedAnimatorAnchor_Thing() : base() { }

        public ExtendedAnimatorAnchor_Thing(Thing thing) : base()
        {
            this.thing = thing;
        }

        public override Vector3 getDrawPos()
        {
            //x and z position, regular altitude for pawns
            return new Vector3(thing.DrawPos.x, AltitudeLayer.Pawn.AltitudeFor(), thing.DrawPos.z);
        }

        public override void ExposeData()
        {
            base.ExposeData();
            Scribe_References.Look<Thing>(ref this.thing, "animations_anchor_thing", false);
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public class ExtendedAnimatorAnchor_Vector3 : BaseExtendedAnimatorAnchor
    {

        public ExtendedAnimatorAnchor_Vector3() : base() { }

        private Vector3 position;
        public ExtendedAnimatorAnchor_Vector3(Vector3 position) : base()
        {
            //default to altitude for layer for y
            this.position = new Vector3(position.x, AltitudeLayer.Pawn.AltitudeFor(), position.z);
        }

        public override Vector3 getDrawPos()
        {
            return position;
        }

        public override void ExposeData()
        {
            base.ExposeData();
            Scribe_Values.Look<Vector3>(ref position, "animations_anchor_position", Vector3.zero);
        }
    }
}


code:
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    [DefOf]
    public static class AnimationDefOf
    {
        static AnimationDefOf()
        {
            DefOfHelper.EnsureInitializedInCtor(typeof(AnimationDefOf));
        }

        public static AnimationDef TestAnimation1;
        public static AnimationDef TestAnimation2;
    }
}


code:
using System.Collections.Generic;
using Verse;
using RimWorld;
using UnityEngine;
using System.Windows;

namespace Rimworld_Animations {
    class MainTabWindow_OffsetConfigure : MainTabWindow
    {

        public override Vector2 RequestedTabSize => new Vector2(505, 500);
        public override void DoWindowContents(Rect inRect) {

            Rect position = new Rect(inRect.x, inRect.y, inRect.width, inRect.height);

            Listing_Standard listingStandard = new Listing_Standard();
            listingStandard.Begin(position);

            listingStandard.Label("RimAnims_AnimManager".Translate());

            listingStandard.GapLine();

            if (Find.Selector.SingleSelectedThing is Pawn curPawn
                && curPawn.TryGetComp<CompExtendedAnimator>(out CompExtendedAnimator extendedAnimator)
                && extendedAnimator.IsAnimating)
            {
                //Pawn info about their body, race
                Vector3 offsetPosition = extendedAnimator.Offset != null ? (Vector3)extendedAnimator.Offset : Vector3.zero;
                int offsetRotation = extendedAnimator.Rotation != null ? (int)extendedAnimator.Rotation : 0;

                string pawnDef = curPawn.def.defName;
                string bodyTypeDef = (curPawn.story?.bodyType != null) ? curPawn.story.bodyType.ToString() : "None";
                string genderDef = curPawn.gender.ToString();
                string currentAnimation = extendedAnimator.CurrentAnimation != null ? extendedAnimator.CurrentAnimation.defName : "None";

                listingStandard.Label(curPawn.Name + ": " + curPawn.def.defName + ", " + bodyTypeDef + ", " + genderDef + ",  Animation: " + currentAnimation);

                if (curPawn.def.defName == "Human")
                {
                    listingStandard.Label("RimAnims_Warning".Translate());
                }

                float posX = offsetPosition.x, posY = offsetPosition.y, posZ = offsetPosition.z;
                int rot = offsetRotation;

                float.TryParse(listingStandard.TextEntryLabeled("X: ", posX.ToString()), out posX);
                posX = listingStandard.Slider(posX, -2, 2);

                float.TryParse(listingStandard.TextEntryLabeled("Y: ", offsetPosition.y.ToString()), out posY);
                posY = listingStandard.Slider(posY, -2, 2);

                float.TryParse(listingStandard.TextEntryLabeled("Z: ", posZ.ToString()), out posZ);
                posZ = listingStandard.Slider(posZ, -2, 2);

                int.TryParse(listingStandard.TextEntryLabeled("Rotation: ", rot.ToString()), out rot);
                rot = (int)listingStandard.Slider(rot, -180, 180);

                listingStandard.GapLine();
                Vector3 newOffsetVector = new Vector3(posX, posY, posZ);

                string offset = "<li>";
                offset += bodyTypeDef != "None" ? "<bodyType>" + bodyTypeDef + "</bodyType>" : "";
                offset += newOffsetVector != Vector3.zero ? "<offset>(" + posX + ", " + posY + ", " + posZ + ")</offset>" : "";
                offset += rot != 0 ? "<rotation>" + rot + "</rotation>" : "";
                offset += "</li>";

                listingStandard.Label("Appropriate Offset value for " + currentAnimation + ", " + pawnDef + ", " + bodyTypeDef + ", " + genderDef + ": ");
                listingStandard.Label(offset);

                if (listingStandard.ButtonText("RimAnims_CopyToClipboard".Translate()))
                {

                    GUIUtility.systemCopyBuffer = offset;

                }

                listingStandard.Label("RimAnims_ShareSettings".Translate());

                extendedAnimator.Offset = newOffsetVector;
                extendedAnimator.Rotation = rot;

            }

            else
            {
                listingStandard.Label("Select a pawn currently in an animation to change their offsets");
            }

            listingStandard.End();

        }
    }
}

/**
 if (curPawn.TryGetComp<CompExtendedAnimator> animator) {

                    /*
        CompBodyAnimator compBodyAnimator = curPawn.TryGetComp<CompBodyAnimator>();
        AnimationDef def = compBodyAnimator.CurrentAnimation;
        int ActorIndex = compBodyAnimator.ActorIndex;
        float offsetX = 0, offsetZ = 0, rotation = 0;

        string bodyTypeDef = (curPawn.story?.bodyType != null) ? curPawn.story.bodyType.ToString() : "";

        if (AnimationSettings.offsets.ContainsKey(def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex)) {
            offsetX = AnimationSettings.offsets[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex].x;
            offsetZ = AnimationSettings.offsets[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex].y;
        } else {
            AnimationSettings.offsets.Add(def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex, new Vector2(0, 0));
        }

        if (AnimationSettings.rotation.ContainsKey(def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex)) {
            rotation = AnimationSettings.rotation[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex];
        }
        else {
            AnimationSettings.rotation.Add(def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex, 0);
        }

        listingStandard.Label("Name: " + curPawn.Name + " Race: " + curPawn.def.defName + " Actor Index: " + curPawn.TryGetComp<CompBodyAnimator>().ActorIndex + " Body Type (if any): " + bodyTypeDef + " Animation: " + def.label + (curPawn.TryGetComp<CompBodyAnimator>().Mirror ? " mirrored" : ""));

        if(curPawn.def.defName == "Human") {
            listingStandard.Label("Warning--You generally don't want to change human offsets, only alien offsets");
        }

        float.TryParse(listingStandard.TextEntryLabeled("X Offset: ", offsetX.ToString()), out offsetX);
        offsetX = listingStandard.Slider(offsetX, -2, 2);

        float.TryParse(listingStandard.TextEntryLabeled("Z Offset: ", offsetZ.ToString()), out offsetZ);
        offsetZ = listingStandard.Slider(offsetZ, -2, 2);

        float.TryParse(listingStandard.TextEntryLabeled("Rotation: ", rotation.ToString()), out rotation);
        rotation = listingStandard.Slider(rotation, -180, 180);

        if(listingStandard.ButtonText("Reset All")) {
            offsetX = 0;
            offsetZ = 0;
            rotation = 0;
        }

        listingStandard.GapLine();

        if(listingStandard.ButtonText("Shift Actors")) {

            if(AnimationSettings.debugMode) {
                Log.Message("Shifting actors in animation...");
            }

            for(int i = 0; i < curPawn.TryGetComp<CompBodyAnimator>().actorsInCurrentAnimation.Count; i++) {

                Pawn actor = curPawn.TryGetComp<CompBodyAnimator>().actorsInCurrentAnimation[i];

                actor.TryGetComp<CompBodyAnimator>()?.shiftActorPositionAndRestartAnimation();

                //reset the clock time of every pawn in animation
                if(actor.jobs.curDriver is rjw.JobDriver_Sex) {
                    (actor.jobs.curDriver as rjw.JobDriver_Sex).ticks_left = def.animationTimeTicks;
                    (actor.jobs.curDriver as rjw.JobDriver_Sex).ticksLeftThisToil = def.animationTimeTicks;
                    (actor.jobs.curDriver as rjw.JobDriver_Sex).duration = def.animationTimeTicks;
                }

            }

        }

        if (offsetX != AnimationSettings.offsets[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex].x || offsetZ != AnimationSettings.offsets[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex].y) {
            AnimationSettings.offsets[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex] = new Vector2(offsetX, offsetZ);

        }

        if(rotation != AnimationSettings.rotation[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex]) {
            AnimationSettings.rotation[def.defName + curPawn.def.defName + bodyTypeDef + ActorIndex] = rotation;
        }

    }

}
                    */


code:
using RimWorld;
using Verse;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Rimworld_Animations {

    [DefOf]
    public static class OffsetMainButtonDefOf {

        public static MainButtonDef OffsetManager;

        static OffsetMainButtonDefOf() {
            DefOfHelper.EnsureInitializedInCtor(typeof(OffsetMainButtonDefOf));
        }

    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RimWorld;
using RimWorld.Planet;
using Verse;

namespace Rimworld_Animations {
    public class WorldComponent_UpdateMainTab : WorldComponent {

        public WorldComponent_UpdateMainTab(World world) : base(world) {

        }

        public override void FinalizeInit() {
            base.FinalizeInit();
            OffsetMainButtonDefOf.OffsetManager.buttonVisible = RJWAnimationSettings.offsetTab;
        }

    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;
using HarmonyLib;
using System.Reflection;

namespace Rimworld_Animations {

    [StaticConstructorOnStartup]
    public static class Harmony_PatchAll {

        static Harmony_PatchAll() {

            Harmony val = new Harmony("rjwanim");
            val.PatchAll(Assembly.GetExecutingAssembly());

        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using rjw;
using HarmonyLib;
using Verse;
using RimWorld;
using Verse.AI;

namespace Rimworld_Animations {
    /*
    [HarmonyPatch(typeof(WorkGiver_Sex), "JobOnThing")]
    public static class HarmonyPatch_WorkGiverSex {

        public static bool Prefix(ref Job __result, ref Thing t) {

            Building_Bed bed = RestUtility.CurrentBed(t as Pawn);
            if (bed == null) {
                return false;
            }
            __result = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("JoinInBedAnimation", true), t as Pawn, bed);
            return false;

        }

    }

    */
}


code:
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using RimWorld;
using Verse;
using rjw;
using Verse.AI;

namespace Rimworld_Animations
{

    [HarmonyPatch(typeof(Bed_Utility), "in_same_bed")]
    public static class HarmonyPatch_JobDriver_InSameBedPatch
    {

        public static bool Prefix(Pawn partner, ref bool __result)
        {

            if(partner != null && partner.CurJobDef == xxx.casual_sex)
            {
                __result = true;
                return false;
            }

            return true;
        }
    }

    [HarmonyPatch(typeof(JobDriver_JoinInBed), "MakeNewToils")]
    public static class HarmonyPatch_JobDriver_JoinInBed
    {

        public static void Postfix(JobDriver_JoinInBed __instance, ref IEnumerable<Toil> __result)
        {

            var toils = __result.ToList();

            Toil goToPawnInBed = Toils_Goto.GotoThing(__instance.iTarget, PathEndMode.OnCell);
            goToPawnInBed.FailOn(() => !RestUtility.InBed(__instance.Partner) && __instance.Partner.CurJobDef != xxx.gettin_loved && !Bed_Utility.in_same_bed(__instance.Partner, __instance.pawn));

            toils[1] = goToPawnInBed;

            Toil startPartnerSex = new Toil();
            startPartnerSex.initAction = delegate {

                if (!(__instance.Partner.jobs.curDriver is JobDriver_SexBaseReciever)) // allows threesomes
                {
                    Job gettinLovedJob = JobMaker.MakeJob(xxx.gettin_loved, __instance.pawn, __instance.Bed); // new gettin loved toil that wakes up the pawn goes here
                    __instance.Partner.jobs.jobQueue.EnqueueFirst(gettinLovedJob);
                    __instance.Partner.jobs.EndCurrentJob(JobCondition.InterruptForced);
                }

            };

            toils[2] = startPartnerSex;

            __result = toils.AsEnumerable();
        }
    }
}


code:
using HarmonyLib;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{

    [HarmonyPatch(typeof(JobDriver_Sex), "setup_ticks")]
    public class HarmonyPatch_JobDriver_Sex
    {
        public static void Postfix(JobDriver_Sex __instance)
        {
            if (!RJWAnimationSettings.hearts)
            {
                __instance.ticks_between_hearts = int.MaxValue;
            }

        }
    }

    [HarmonyPatch(typeof(JobDriver_Sex), "SexTick")]
    public class HarmonyPatch_JobDriver_Sex2
    {
        public static void Postfix(JobDriver_Sex __instance, Pawn pawn)
        {
            //if neverending sex and pawn doesn't have an animation,
            if (__instance.neverendingsex
                && __instance is JobDriver_SexBaseReciever receiverJobDriver
                && !pawn.TryGetComp<CompExtendedAnimator>().IsAnimating)
            {

                //start a new animation for all the pawns paired with receiver job driver
                List<Pawn> participants = receiverJobDriver.parteners.Append(pawn).ToList();
                GroupAnimationDef animation = AnimationUtility.FindGroupAnimation(participants, out int reorder);
                if (animation != null)
                {
                    Thing anchor = (Thing)__instance.Bed ?? pawn;
                    AnimationUtility.StartGroupAnimation(participants, animation, reorder, anchor);
                }

            }

        }
    }

}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using RimWorld;
using Verse;
using rjw;

namespace Rimworld_Animations
{

    [HarmonyPatch(typeof(JobDriver_SexBaseInitiator), "Start")]
    static class HarmonyPatch_JobDriver_SexBaseInitiator_Start
    {
        public static void Postfix(ref JobDriver_SexBaseInitiator __instance)
        {

            Pawn pawn = __instance.pawn;
            Pawn partner = __instance.Target as Pawn;

            if (partner == null)
                partner = pawn;

            List<Pawn> participants = GetAllSexParticipants(pawn);
            GroupAnimationDef groupAnimation = AnimationUtility.FindGroupAnimation(participants, out int reorder);

            if (groupAnimation != null)
            {
                Thing anchor = (Thing)__instance.Bed ?? partner;

                AnimationUtility.StartGroupAnimation(participants, groupAnimation, reorder, anchor);
                int animTicks = AnimationUtility.GetAnimationLength(pawn);

                foreach (Pawn participant in participants)
                {
                    if (RJWAnimationSettings.debugMode)
                    {
                        Log.Message("Participant: " + participant.Name);
                        Log.Message("JobDriver: " + participant.CurJobDef.defName);
                    }

                    //null ref check for pawns that might have lost their jobs or become null for some reason
                    if (participant?.jobs?.curDriver is JobDriver_Sex participantJobDriver)
                    {
                        participantJobDriver.ticks_left = animTicks;
                        participantJobDriver.sex_ticks = animTicks;
                        participantJobDriver.orgasmStartTick = animTicks;
                        participantJobDriver.duration = animTicks;
                    }
                }
            }
        }

        public static List<Pawn> GetAllSexParticipants(this Pawn pawn)
        {
            List<Pawn> participants = new List<Pawn>();

            if (pawn?.jobs?.curDriver == null ||
                (pawn.jobs.curDriver is JobDriver_Sex) == false)
                return participants;

            var receiver = pawn.GetSexReceiver();

            if (receiver != null)
            {
                participants = (receiver.jobs.curDriver as JobDriver_SexBaseReciever).parteners;
                participants.AddDistinct(receiver);

                return participants;
            }

            participants.AddDistinct(pawn);

            return participants;
        }

        public static Pawn GetSexReceiver(this Pawn pawn)
        {
            if (pawn?.jobs?.curDriver == null)
                return null;

            if (pawn.jobs.curDriver is JobDriver_SexBaseReciever)
                return pawn;

            JobDriver_SexBaseInitiator jobDriver = pawn.jobs.curDriver as JobDriver_SexBaseInitiator;

            if (jobDriver?.Partner?.jobs?.curDriver == null)
                return null;

            if (jobDriver.Partner.jobs.curDriver is JobDriver_SexBaseReciever)
                return jobDriver.Partner;

            return null;
        }

        static IEnumerable<String> NonSexActRulePackDefNames = new String[]
        {
            "MutualHandholdingRP",
            "MutualMakeoutRP",
        };

        public static bool NonSexualAct(JobDriver_SexBaseInitiator sexBaseInitiator)
        {
            if (NonSexActRulePackDefNames.Contains(sexBaseInitiator.Sexprops.rulePack))
            {
                return true;
            }
            return false;
        }
    }

    [HarmonyPatch(typeof(JobDriver_SexBaseInitiator), "End")]
    static class HarmonyPatch_JobDriver_SexBaseInitiator_End
    {

        public static void Prefix(ref JobDriver_SexBaseInitiator __instance)
        {
            //stop pawn animating
            AnimationUtility.StopGroupAnimation(__instance.pawn);

            //stop partner animating
            if (__instance.Partner is Pawn partner)
            {
                AnimationUtility.StopGroupAnimation(partner);
            }

            //stop partner's other partners (threesome pawns) animating
            //added null ref checks for instances when pawns get nulled or lose their jobs
            if (__instance.Partner?.jobs?.curDriver is JobDriver_SexBaseReciever partnerReceiverJob)
            {
                foreach (Pawn pawn in partnerReceiverJob.parteners)
                {
                    if (pawn != null) AnimationUtility.StopGroupAnimation(pawn);
                }
            }
        }
    }
}


code:
using HarmonyLib;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    [HarmonyPatch(typeof(JobDriver_Sex), "Animate")]
    public class HarmonyPatch_Animate
    {
        public static bool Prefix(ref JobDriver_Sex __instance, ref Pawn pawn, ref Thing target)
        {
			//remove all bumping stuff in animations; keep draw nude code
			__instance.RotatePawns(pawn, __instance.Partner);
			if (target != null)
			{
				Pawn pawn2 = target as Pawn;

				if (pawn2 != null && !__instance.Sexprops.isRapist)
				{
					// if not (pawn has root node and rootnode is animating)
					if (!(pawn2?.Drawer?.renderer?.renderTree?.rootNode is PawnRenderNode rootNode
					&& (rootNode.AnimationWorker is AnimationWorker_KeyframesExtended || rootNode.children.Any(x => x.AnimationWorker is AnimationWorker_KeyframesExtended))))
                    {
						//play bumpin anim
						pawn.Drawer.Notify_MeleeAttackOn(target);
					}

				}

				if (!__instance.isEndytophile)
				{
					SexUtility.DrawNude(pawn, false);
					if (pawn2 != null)
					{
						SexUtility.DrawNude(pawn2, false);
						return false;
					}
				}
			}
			else if (!__instance.isEndytophile)
			{
				SexUtility.DrawNude(pawn, false);
			}

			return false;
        }
    }
}


code:
using HarmonyLib;
using rjw;

namespace Rimworld_Animations
{
    [HarmonyPatch(typeof(JobDriver_Sex), "PlaySexSound")]
    class HarmonyPatch_PlaySexSounds
    {
        public static bool Prefix(JobDriver_Sex __instance)
        {
            return false;
        }
    }
}


code:
using HarmonyLib;
using RimWorld;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;
using Verse.AI;

namespace Rimworld_Animations
{
    [HarmonyPatch(typeof(JobDriver_Sex), "SexTick")]
    public class HarmonyPatch_SexTick
    {
        public static bool Prefix(JobDriver_Sex __instance, Pawn pawn, Thing target)
        {

			if ((target is Pawn) &&
				!(
				(target as Pawn)?.jobs?.curDriver is JobDriver_SexBaseReciever
				&&
				((target as Pawn).jobs.curDriver as JobDriver_SexBaseReciever).parteners.Any()
				&&
				((target as Pawn).jobs.curDriver as JobDriver_SexBaseReciever).parteners[0] == pawn))
            {

				__instance.ticks_left--;
                __instance.sex_ticks--;
                __instance.Orgasm();

				if (pawn.IsHashIntervalTick(__instance.ticks_between_thrusts))
				{
					__instance.ChangePsyfocus(pawn, target);
					__instance.Animate(pawn, target);
					__instance.PlaySexSound();
					if (!__instance.Sexprops.isRape)
					{
						pawn.GainComfortFromCellIfPossible(false);
						if (target is Pawn)
						{
							(target as Pawn).GainComfortFromCellIfPossible(false);
						}
					}
					if(!__instance.isEndytophile)
                    {
						SexUtility.DrawNude(pawn, false);
                    }
				}

				return false;
            }

            return true;
        }

    }

}


code:
using HarmonyLib;
using RimWorld;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
using Verse.AI;

namespace Rimworld_Animations
{
    [HarmonyPatch(typeof(JobDriver_SexBaseRecieverLoved), "MakeSexToil")]
    public class HarmonyPatch_JobDriver_SexBaseReceiverLoved
    {
        public static void Postfix(JobDriver_SexBaseRecieverLoved __instance, ref Toil __result)
        {
            //added for sudden end of jobdriver
            __result.AddFinishAction(delegate {
                AnimationUtility.StopGroupAnimation(__instance.pawn);
            });
        }

        public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> codeInstructions)
        {

            var ins = codeInstructions.ToList();
            for (int i = 0; i < ins.Count; i++)
            {
                if (i < ins.Count && ins[i].opcode == OpCodes.Call && ins[i].OperandIs(AccessTools.DeclaredMethod(typeof(Toils_LayDown), "LayDown")))
                {

                    ins[i].operand = AccessTools.DeclaredMethod(typeof(HarmonyPatch_JobDriver_SexBaseReceiverLoved), "DoNotLayDown");
                    yield return ins[i];

                }

                else
                {
                    yield return ins[i];
                }
            }

        }

        public static Toil DoNotLayDown(TargetIndex bedOrRestSpotIndex, bool hasBed, bool lookForOtherJobs, bool canSleep = true, bool gainRestAndHealth = true, PawnPosture noBedLayingPosture = PawnPosture.LayingMask, bool deathrest = false)
        {
            return new Toil();
        }
    }
}


code:
using HarmonyLib;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse.AI;

namespace Rimworld_Animations
{
    [HarmonyPatch(typeof(JobDriver_SexBaseRecieverRaped), "MakeNewToils")]
    public class HarmonyPatch_JobDriver_SexBaseReceiverRaped
    {
        /*
         * Doesn't work; ienumerables are read-only, can't modify toil
         * would need to harmonypatch; stopped partner animating in sexbaseinitiator instead
         *
        public static void Postfix(JobDriver_SexBaseRecieverRaped __instance, ref IEnumerable<Toil> __result)
        {
            //added for sudden end of jobdriver
            __result.Last().AddFinishAction(delegate {
                AnimationUtility.StopGroupAnimation(__instance.pawn);
            });
        }

        */
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;
using UnityEngine;
using RimWorld;

namespace Rimworld_Animations {

    public class RJWAnimationSettings : ModSettings {

        public static bool orgasmQuiver, rapeShiver, soundOverride = true, hearts = true, controlGenitalRotation = false,
            PlayAnimForNonsexualActs = true;

        //probably move this setting to a different mod menu if moving rjw parts of code
        public static bool playVoices = true, playHumanlikeVoicesAsDefault = true;
        public static float floatRangeInRenderTreeMenu = 1f;

        public static bool offsetTab = false, debugMode = false;
        public static float shiverIntensity = 2f;

        public override void ExposeData() {

            base.ExposeData();
            Scribe_Values.Look(ref playVoices, "RJWAnimations_PlayVoices", true);
            Scribe_Values.Look(ref playHumanlikeVoicesAsDefault, "RJWAnimations-playHumanlikeVoicesAsDefault", true);
            Scribe_Values.Look(ref debugMode, "RJWAnimations-AnimsDebugMode", false);
            Scribe_Values.Look(ref offsetTab, "RJWAnimations-EnableOffsetTab", false);
            Scribe_Values.Look(ref controlGenitalRotation, "RJWAnimations-controlGenitalRotation", false);
            Scribe_Values.Look(ref orgasmQuiver, "RJWAnimations-orgasmQuiver");
            Scribe_Values.Look(ref rapeShiver, "RJWAnimations-rapeShiver");
            Scribe_Values.Look(ref hearts, "RJWAnimation-heartsOnLovin");
            Scribe_Values.Look(ref PlayAnimForNonsexualActs, "RJWAnims-PlayAnimForNonsexualActs");
            Scribe_Values.Look(ref soundOverride, "RJWAnimations-rjwAnimSoundOverride", true);
            Scribe_Values.Look(ref shiverIntensity, "RJWAnimations-shiverIntensity", 2f);
            Scribe_Values.Look(ref floatRangeInRenderTreeMenu, "RJWAnimations-FloatRangeRenderMenu", 1f);
            //todo: save offsetsByDefName

        }

    }

    public class RJW_Animations : Mod {

        public RJW_Animations(ModContentPack content) : base(content) {
            GetSettings<RJWAnimationSettings>();

        }

        public override void DoSettingsWindowContents(Rect inRect) {

            Listing_Standard listingStandard = new Listing_Standard();
            listingStandard.Begin(inRect);

            listingStandard.CheckboxLabeled("RimAnim_SoundOverride".Translate(), ref RJWAnimationSettings.soundOverride);
            listingStandard.CheckboxLabeled("RimAnim_GenitalRotation".Translate(), ref RJWAnimationSettings.controlGenitalRotation);
            listingStandard.CheckboxLabeled("RimAnim_OrgasmQuiver".Translate(), ref RJWAnimationSettings.orgasmQuiver);
            listingStandard.CheckboxLabeled("RimAnim_RapeShiver".Translate(), ref RJWAnimationSettings.rapeShiver);
            listingStandard.CheckboxLabeled("RimAnim_HeartsDuringLovin".Translate(), ref RJWAnimationSettings.hearts);
            listingStandard.CheckboxLabeled("RimAnim_PlayNonsexual".Translate(), ref RJWAnimationSettings.PlayAnimForNonsexualActs);
            listingStandard.CheckboxLabeled("RimAnim_AnimManagerTab".Translate(), ref RJWAnimationSettings.offsetTab);
            listingStandard.CheckboxLabeled("RimAnim_Voices".Translate(), ref RJWAnimationSettings.playVoices);
            if (RJWAnimationSettings.playVoices)
            {
                listingStandard.CheckboxLabeled("RimAnim_HumanlikeVoicesDefault".Translate(), ref RJWAnimationSettings.playHumanlikeVoicesAsDefault);
            }

            listingStandard.Label("RimAnim_ShiverIntensity".Translate() + RJWAnimationSettings.shiverIntensity);
            RJWAnimationSettings.shiverIntensity = listingStandard.Slider(RJWAnimationSettings.shiverIntensity, 0.0f, 12f);

            listingStandard.Label("RimAnim_FloatRangeRenderTree".Translate() + RJWAnimationSettings.floatRangeInRenderTreeMenu);
            RJWAnimationSettings.floatRangeInRenderTreeMenu = listingStandard.Slider(RJWAnimationSettings.floatRangeInRenderTreeMenu, 0.1f, 12f);

            listingStandard.CheckboxLabeled("RimAnim_DebugMode".Translate(), ref RJWAnimationSettings.debugMode);

            listingStandard.End();
            base.DoSettingsWindowContents(inRect);
        }

        public override void WriteSettings() {
            base.WriteSettings();
            OffsetMainButtonDefOf.OffsetManager.buttonVisible = RJWAnimationSettings.offsetTab;

        }

        public override string SettingsCategory() {
            return "RimAnim_ModSettings".Translate();
        }
    }
}


code:
using HarmonyLib;
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    [HarmonyPatch(typeof(Dialog_DebugRenderTree), "RightRect")]
    public static class HarmonyPatch_Dialog_DebugRenderTree
    {

        static MethodInfo replaceFloatRangeMethod = SymbolExtensions.GetMethodInfo(() => HarmonyPatch_Dialog_DebugRenderTree.ReplaceFloatValueRange());

        public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
        {
            var codes = new List<CodeInstruction>(instructions);

            for (int i = 0; i < codes.Count; i++)
            {
                //increase granularity of x and z sliders to be 0.01 instead
                if (codes[i].opcode == OpCodes.Ldc_R4 && (float)codes[i].operand == 0.05f)
                {
                    codes[i].operand = 0.001f;
                    codes[i - 8].opcode = OpCodes.Call;
                    codes[i - 8].operand = replaceFloatRangeMethod;

                }

            }

            return codes.AsEnumerable();

        }

        public static FloatRange ReplaceFloatValueRange()
        {
            return new FloatRange(-RJWAnimationSettings.floatRangeInRenderTreeMenu, RJWAnimationSettings.floatRangeInRenderTreeMenu);
        }

    }
}


code:
using HarmonyLib;
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    // Head Rotation Code - Textures
    // it's fine to just edit each AppendRequests individually
    // because they all the parms are passed down to each child node recursively
    [HarmonyPatch(typeof(PawnRenderNode), "AppendRequests")]
    public static class HarmonyPatch_PawnRenderNode
    {
        //if rendernodetag is head, update PawnDrawParms so that head, and all children, are rotated for anim
        public static bool Prefix(ref PawnRenderNode __instance, ref PawnDrawParms parms)
        {
            if (__instance.AnimationWorker is AnimationWorker_KeyframesExtended extendedAnimWorker)
            {

                if (parms.Portrait) return true;

                // ADJUST FACING get rotated textures
                // compare the previous tick to the current tick; if the current tick rotation is different, recache
                parms.facing = extendedAnimWorker.facingAtTick(__instance.tree.AnimationTick);

                //INVIS IF ANIM CALLS FOR IT
                //replace maybe?

                //cheaper call now comparing prev tick to cur tick

                //not necessary because of new rendernodeworker hiding props now
                //nvm, keep it because you can hide head and body too, if need be
                return extendedAnimWorker.visibleAtTick(__instance.tree.AnimationTick);

            }

            return true;

        }
    }
    /*
     * no longer needed; taken care of by graphic variants
     *
    // For changing texture path of thing to variant
    [HarmonyPatch(typeof(PawnRenderNode), "TexPathFor")]
    public static class HarmonyPatch_PawnRenderNode2
    {
        public static void Postfix(ref PawnRenderNode __instance, ref string __result)
        {
            if (__instance.AnimationWorker is AnimationWorker_KeyframesExtended animWorker)
            {
                __result += animWorker.TexPathVariantAtTick(__instance.tree.AnimationTick);
            }
        }
    }

    */
}


code:
using HarmonyLib;
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{

    [HarmonyPatch(typeof(PawnRenderNodeWorker), "CanDrawNow")]
    public class HarmonyPatch_PawnRenderTreeWorker
    {
        public static bool Prefix(PawnRenderNode node, ref bool __result)
        {
            //switching to this system so that head or body can be hidden separate from other nodes
            //(hide head but not addons, etc)
            //in case someone wanted to do that

            if (node.AnimationWorker is AnimationWorker_KeyframesExtended animWorker)
            {
                if (!animWorker.visibleAtTick(node.tree.AnimationTick))
                {
                    __result = false;
                    return false;
                }

                //visible when animating
                return true;
            }

            return true;
        }

    }

}


code:
using HarmonyLib;
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{

    [HarmonyPatch(typeof(PawnRenderTree), "TryGetMatrix")]
    public class HarmonyPatch_PawnRenderTree
    {
        public static bool Prefix(PawnRenderTree __instance, Dictionary<PawnRenderNodeTagDef, PawnRenderNode> ___nodesByTag, PawnRenderNode node, ref PawnDrawParms parms, ref Matrix4x4 matrix, ref bool __result)
        {
            /*
             * Facing offsets fix
             */
            //find lowest parent that is animating, or nothing if not animating

            //don't do anything if portrait
            if (parms.Portrait) return true;

            PawnRenderNode animatingNode = node;
            while (animatingNode != null
                && !(animatingNode.AnimationWorker is AnimationWorker_KeyframesExtended))
            {
                animatingNode = animatingNode.parent;
            }

            //if animating parent node found,
            if (animatingNode?.AnimationWorker is AnimationWorker_KeyframesExtended animatingNodeAnimationWorker)
            {
                //change parm to facing to animate correctly
                parms.facing = animatingNodeAnimationWorker.facingAtTick(__instance.AnimationTick);
            }

            /*
             * Set Render Node to absolute position
             */
            if (node.Props is PawnRenderNodeProperties_GraphicVariants graphicVariantProp
                && graphicVariantProp.absoluteTransform)
            {
                matrix = parms.matrix;

                //absolute transform -- just use the node's transform, not its ancestors
                node.GetTransform(parms, out Vector3 offset, out Vector3 pivot, out Quaternion quaternion, out Vector3 scale);

                if (offset != Vector3.zero) matrix *= Matrix4x4.Translate(offset);
                if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(pivot);
                if (quaternion != Quaternion.identity) matrix *= Matrix4x4.Rotate(quaternion);
                if (scale != Vector3.one) matrix *= Matrix4x4.Scale(scale);
                if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(scale).inverse;

                float num = node.Worker.AltitudeFor(node, parms);
                if (num != 0f)
                {
                    matrix *= Matrix4x4.Translate(Vector3.up * num);
                }

                __result = true;
                return false;

            }

            return true;

        }

    }

    //recaching
    //done here because changing parms causes recaching anyway, so might as well do it here
    [HarmonyPatch(typeof(PawnRenderTree), "AdjustParms")]
    public class HarmonyPatch_PawnRenderTree2
    {
        public static void Prefix(PawnRenderTree __instance, ref PawnDrawParms parms)
        {

            int animationTick = __instance.AnimationTick;

            if (__instance.rootNode.AnimationWorker is AnimationWorker_KeyframesExtended rootAnimWorkerExtended)
            {
                //recache during facing turn
                if (rootAnimWorkerExtended.shouldRecache(animationTick))
                {
                    __instance.rootNode.requestRecache = true;
                    return;
                }
            }

            foreach (PawnRenderNode node in __instance.rootNode.children)
            {
                if (node.AnimationWorker is AnimationWorker_KeyframesExtended animWorkerExtended)
                {
                    //recache during flicker on/off
                    if (animWorkerExtended.shouldRecache(animationTick))
                    {
                        node.requestRecache = true;
                        return;
                    }
                }
            }
        }
    }
}


code:
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{

    [HarmonyPatch(typeof(PawnRenderer), "BodyAngle")]
    public class HarmonyPatch_PawnRenderer
    {
        public static bool Prefix(ref Pawn ___pawn, ref float __result)
        {
            //set body angle to zero, for when downed
            if (___pawn?.Drawer?.renderer?.renderTree?.rootNode?.AnimationWorker is AnimationWorker_KeyframesExtended)
            {
                __result = 0;
                return false;

            }

            return true;

        }
    }

    [HarmonyPatch(typeof(PawnRenderer), "GetBodyPos")]
    public class HarmonyPatch_PawnRenderer2
    {
        //patch so that pawns appear at the same altitude layer, at layer Pawn
        public static void Postfix(PawnRenderer __instance, ref Vector3 __result)
        {

            if (__instance.renderTree?.rootNode?.AnimationWorker is AnimationWorker_KeyframesExtended
                || (__instance.renderTree?.rootNode?.children is PawnRenderNode[] childNodes && childNodes.Any(x => x.AnimationWorker is AnimationWorker_KeyframesExtended)))
            {
                __result.y = AltitudeLayer.Pawn.AltitudeFor();
            }

        }
    }

}


code:
using HarmonyLib;
using rjw;
using UnityEngine;
using Verse;

namespace Rimworld_Animations {

    [HarmonyPatch(typeof(Pawn_DrawTracker), "DrawPos", MethodType.Getter)]
    public static class HarmonyPatch_Pawn_DrawTracker {

        //switch to postfix to get pawn original height first
        public static void Postfix(ref Pawn ___pawn, ref Vector3 __result) {

            //align pos on top of partner, position, etc., based on animatoranchor
            if (___pawn.TryGetComp<CompExtendedAnimator>() is CompExtendedAnimator animator)
            {
                if (animator.IsAnchored)
                {
                    Vector3 anchor = animator.getAnchor();
                    __result.x = anchor.x;
                    __result.z = anchor.z;
                }

            }

        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HarmonyLib;
using RimWorld;
using Verse;

namespace Rimworld_Animations
{
    [HarmonyPatch(typeof(Thing), "DrawAt")]
    public static class HarmonyPatch_Thing
    {
        /*
        public static bool Prefix(Thing __instance)
        {
            CompThingAnimator thingAnimator = __instance.TryGetComp<CompThingAnimator>();
            if (thingAnimator != null && thingAnimator.isAnimating)
            {
                thingAnimator.AnimateThing(__instance);
                return false;

            }

            return true;

        }

        */
    }
}


code:
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNodeProperties_BodyTypeVariants : PawnRenderNodeProperties_GraphicVariants
    {

        public List<TexPathVariants_BodyType> bodyTypeVariantsDef;

    }

    public class TexPathVariants_BodyType
    {

        public BodyTypeDef bodyType;
        public TexPathVariantsDef texPathVariantsDef;

    }

}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Rimworld_Animations
{
    public class PawnRenderNodeWorker_BodyTypeVariants : PawnRenderNodeWorker_GraphicVariants
    {
        //same functionality as graphicvariants worker
        //just here for readability

    }
}


code:
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNode_BodyTypeVariants : PawnRenderNode_GraphicVariants
    {

        private BodyTypeDef bodyType;
        protected new PawnRenderNodeProperties_BodyTypeVariants props;

        public PawnRenderNode_BodyTypeVariants(Pawn pawn, PawnRenderNodeProperties props, PawnRenderTree tree) : base(pawn, props, tree)
        {

            this.props = (PawnRenderNodeProperties_BodyTypeVariants)props;

        }

        protected Dictionary<int, Graphic> GraphicBodyTypeVariantsFor(Pawn pawn)
        {

            if (props.bodyTypeVariantsDef == null)
            {
                Log.Error("[Anims] Error: Tried to use BodyTypeVariants node, but bodyTypeVariants weren't given");
                return null;
            }

            //for each different hediff-based texpathvariants,
            foreach (TexPathVariants_BodyType texPathVariant_BodyType in props.bodyTypeVariantsDef)
            {
                if (pawn.story?.bodyType == texPathVariant_BodyType.bodyType)
                {
                    //return that specific variant
                    bodyType = texPathVariant_BodyType.bodyType;
                    return GenerateVariants(pawn, texPathVariant_BodyType.texPathVariantsDef);

                }

            }

            return null;

        }

        protected override void EnsureMaterialsInitialized()
        {
            if (variants == null
                || this.tree.pawn.story?.bodyType != bodyType)
            variants = GraphicBodyTypeVariantsFor(this.tree.pawn);

            //call this in case variants wasn't set, and there is no graphic bodytype variants appropriate; it'll set variants based on default
            base.EnsureMaterialsInitialized();
        }

    }

}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNodeProperties_GraphicHediffSeverityVariants : PawnRenderNodeProperties_GraphicVariants
    {

        public BodyPartDef bodyPart = null;
        public List<HediffWithSeverity> hediffSeverityVariants;

    }

    public class HediffWithSeverity
    {
        public HediffDef hediff;
        public List<TexPathVariants_Severity> severityVariants;
    }

    public class TexPathVariants_Severity
    {
        public int severity;
        public TexPathVariantsDef texPathVariantsDef;

    }

}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Rimworld_Animations
{
    public class PawnRenderNodeWorker_GraphicHediffSeverityVariants : PawnRenderNodeWorker_GraphicVariants
    {
        //same functionality as graphicvariants worker
        //just here for readability

    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNode_GraphicHediffSeverityVariants : PawnRenderNode_GraphicVariants
    {

        protected HediffDef hediffWithSeverity;
        protected float curSeverity;

        protected new PawnRenderNodeProperties_GraphicHediffSeverityVariants props;
        private HediffDef curHediff;

        public PawnRenderNode_GraphicHediffSeverityVariants(Pawn pawn, PawnRenderNodeProperties props, PawnRenderTree tree) : base(pawn, props, tree)
        {

            this.props = (PawnRenderNodeProperties_GraphicHediffSeverityVariants)props;

        }

        protected Dictionary<int, Graphic> GraphicHediffSeverityVariantsFor(Pawn pawn)
        {

            if (props.hediffSeverityVariants == null)
            {
                Log.Error("[Anims] Error: Tried to use GraphicBodyPartHediffSeverityVariants node, but hediffSeverityVariants weren't given");
            }

            Hediff idealHediff = null;
            HediffWithSeverity idealHediffSeverity = null;

            if (props.bodyPart == null)
            {
                //search the entire body for the hediff because no bodypart was set
                for (int i = 0; i < props.hediffSeverityVariants.Count; i++)
                {
                    idealHediff = pawn.health.hediffSet.hediffs.Find((Hediff hediffWithSeverity) =>
                        hediffWithSeverity.def == props.hediffSeverityVariants[i].hediff);

                    if (idealHediff != null)
                    {
                        //get the ideal hediff severity variants, to iterate through and find the right one for the severity
                        idealHediffSeverity = props.hediffSeverityVariants[i];
                        break;

                    }

                }
            }

            else
            {
                //search for a hediff with a specific body part

                for (int i = 0; i < props.hediffSeverityVariants.Count; i++)
                {
                    //right hediff with the right hediff and right body part

                    idealHediff = pawn.health.hediffSet.hediffs.Find((Hediff hediffWithSeverity) =>
                        hediffWithSeverity.def == props.hediffSeverityVariants[i].hediff
                        && hediffWithSeverity.Part.def == props.bodyPart);

                    if (idealHediff != null) {

                        //get the ideal hediff severity variants, to iterate through and find the right one for the severity
                        idealHediffSeverity = props.hediffSeverityVariants[i];
                        break;
                    }
                }
            }

            if (idealHediff != null)
            {
                //set severity so that recache when this is different
                curSeverity = idealHediff.Severity;

                //look for the right severity-based texpathvariants
                TexPathVariants_Severity texPathVariants_Severity = idealHediffSeverity.severityVariants.Find((TexPathVariants_Severity texPathVariants) =>
                    texPathVariants.severity < idealHediff.Severity);

                //if null, assume value is really too small
                if (texPathVariants_Severity == null)
                {
                    //return largest value
                    return GenerateVariants(pawn, idealHediffSeverity.severityVariants.First().texPathVariantsDef);
                }

                //return right severity variants
                return GenerateVariants(pawn, texPathVariants_Severity.texPathVariantsDef);

            }

            //there is no graphic hediff variants appropriate
            curHediff = null;
            return null;

        }

        protected override void EnsureMaterialsInitialized()
        {
            //if pawn no longer has the hediff,
            if (variants == null ||
                !(this.tree.pawn.health?.hediffSet?.hediffs is List<Hediff> hediffs
                && hediffs.Any((Hediff hediff) => hediff.def == curHediff && hediff.Severity == curSeverity)))
            {
                //do graphicvariantsfor
                variants = GraphicHediffSeverityVariantsFor(this.tree.pawn);
            }

            //call this in case variants wasn't set, and there is no graphic hediff variants appropriate; it'll set variants based on default
            base.EnsureMaterialsInitialized();
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNodeProperties_GraphicHediffVariants : PawnRenderNodeProperties_GraphicVariants
    {

        public List<TexPathVariants_Hediff> hediffVariants;

    }

    public class TexPathVariants_Hediff
    {

        public List<HediffDef> hediffs;
        public TexPathVariantsDef texPathVariantsDef;

    }

}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Rimworld_Animations
{
    public class PawnRenderNodeWorker_GraphicHediffVariants : PawnRenderNodeWorker_GraphicVariants
    {
        //same functionality as graphicvariants worker
        //just here for readability

    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNode_GraphicHediffVariants : PawnRenderNode_GraphicVariants
    {

        protected new PawnRenderNodeProperties_GraphicHediffVariants props;
        private HediffDef curHediff;

        public PawnRenderNode_GraphicHediffVariants(Pawn pawn, PawnRenderNodeProperties props, PawnRenderTree tree) : base(pawn, props, tree)
        {

            this.props = (PawnRenderNodeProperties_GraphicHediffVariants)props;

        }

        protected Dictionary<int, Graphic> GraphicHediffVariantsFor(Pawn pawn)
        {

            if (props.hediffVariants == null)
            {
                Log.Error("[Anims] Error: Tried to use GraphicHediffVariants node, but hediffVariants weren't given");
                return null;
            }

            //for each different hediff-based texpathvariants,
            foreach (TexPathVariants_Hediff texPathVariant_Hediff in props.hediffVariants)
            {
                //for all the hediffs corresponding to that texpathvariant,
                foreach (HediffDef hediffDef in texPathVariant_Hediff.hediffs)
                {
                    //if the pawn has that hediff,
                    if (pawn?.health?.hediffSet?.hediffs is List<Hediff> pawnHediffs && pawnHediffs.Any((Hediff hediff) => hediff.def == hediffDef))
                    {
                        //return that specific variant
                        curHediff = hediffDef;
                        return GenerateVariants(pawn, texPathVariant_Hediff.texPathVariantsDef);

                    }
                }
            }

            //there is no graphic hediff variants appropriate
            curHediff = null;
            return null;

        }

        protected override void EnsureMaterialsInitialized()
        {
            //if pawn no longer has the hediff,
            if (variants == null ||
                !(this.tree.pawn.health?.hediffSet?.hediffs is List<Hediff> hediffs
                && hediffs.Any((Hediff hediff) => hediff.def == curHediff)))
            {
                //do graphicvariantsfor
                variants = GraphicHediffVariantsFor(this.tree.pawn);
            }
            //call this in case variants wasn't set, and there is no graphic hediff variants appropriate; it'll set variants based on default
            base.EnsureMaterialsInitialized();
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNodeProperties_GraphicVariants : PawnRenderNodeProperties
    {

        public AnimationOffsetDef propOffsetDef = null;
        public TexPathVariantsDef texPathVariantsDef = null;
        public bool absoluteTransform = false;

    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNodeWorker_GraphicVariants : PawnRenderNodeWorker
    {

        public override bool CanDrawNow(PawnRenderNode node, PawnDrawParms parms)
        {
            if (!base.CanDrawNow(node, parms)) return false;

            if (parms.Portrait) return false;

            //don't draw if not visible at tick
            if (node.AnimationWorker is AnimationWorker_KeyframesExtended extendedAnimator)
            {
                return extendedAnimator.visibleAtTick(node.tree.AnimationTick);
            }

            //don't draw at all if not animating
            return false;
        }
        protected override Material GetMaterial(PawnRenderNode node, PawnDrawParms parms)
        {

            //if node is animating, and is a graphic variant type of node
            //and node is one with graphic variants
            //and texpathvariant is set
            if ((node.AnimationWorker is AnimationWorker_KeyframesExtended extendedAnimWorker)
                && (node is PawnRenderNode_GraphicVariants nodeWithGraphicVariants)
                && extendedAnimWorker.TexPathVariantAtTick(node.tree.AnimationTick) != null)
            {
                Material materialVariant = GetMaterialVariant(nodeWithGraphicVariants, parms, (int)extendedAnimWorker.TexPathVariantAtTick(node.tree.AnimationTick));

                if (materialVariant != null) {
                    return materialVariant;
                }

            }

            //otherwise return original texture
            return base.GetMaterial(node, parms);
        }

        public virtual Material GetMaterialVariant(PawnRenderNode_GraphicVariants node, PawnDrawParms parms, int variant)
        {
            Material material = node.getGraphicVariant(variant)?.NodeGetMat(parms);

            if (material == null) return null;

            if (!parms.Portrait && parms.flags.FlagSet(PawnRenderFlags.Invisible))
            {
                material = InvisibilityMatPool.GetInvisibleMat(material);
            }

            return material;

        }

        public override Vector3 OffsetFor(PawnRenderNode node, PawnDrawParms parms, out Vector3 pivot)
        {
            Vector3 regularOffsets = base.OffsetFor(node, parms, out pivot);

            if ((node.Props as PawnRenderNodeProperties_GraphicVariants)?.propOffsetDef?.offsets is List<BaseAnimationOffset> offsets)
            {
                foreach (BaseAnimationOffset offset in offsets)
                {

                    if (offset.appliesToPawn(node.tree.pawn))
                    {

                        //modify offset of prop for animationOffset position
                        regularOffsets += offset.getOffset(node.tree.pawn) ?? Vector3.zero;
                        return regularOffsets;

                    }
                }
            }

            //unmodified; no offsets found
            return regularOffsets;
        }

        public override Vector3 ScaleFor(PawnRenderNode node, PawnDrawParms parms)
        {
            Vector3 regularScale = base.ScaleFor(node, parms);

            if ((node.Props as PawnRenderNodeProperties_GraphicVariants)?.propOffsetDef?.offsets is List<BaseAnimationOffset> offsets)
            {
                foreach (BaseAnimationOffset offset in offsets)
                {

                    if (offset.appliesToPawn(node.tree.pawn))
                    {

                        //modify scale of prop for animationOffset position
                        regularScale = regularScale.MultipliedBy(offset.getScale(node.tree.pawn) ?? Vector3.one);
                        return regularScale;

                    }
                }
            }

            return regularScale;
        }

        public override Quaternion RotationFor(PawnRenderNode node, PawnDrawParms parms)
        {
            Quaternion rotation = base.RotationFor(node, parms);

            if ((node.Props as PawnRenderNodeProperties_GraphicVariants)?.propOffsetDef?.offsets is List<BaseAnimationOffset> offsets)
            {
                foreach (BaseAnimationOffset offset in offsets)
                {
                    if (offset.appliesToPawn(node.tree.pawn))
                    {
                        //modify offset of prop for animationOffset rotation
                        rotation *= Quaternion.AngleAxis(offset.getRotation(node.tree.pawn) ?? 0, Vector3.up);
                        return rotation;

                    }
                }
            }

            //unmodified; no rotation offsets found
            return rotation;

        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;

namespace Rimworld_Animations
{
    public class PawnRenderNode_GraphicVariants : PawnRenderNode
    {

        protected new PawnRenderNodeProperties_GraphicVariants props;
        protected Graphic missingTextureGraphic;
        protected Dictionary<int, Graphic> variants;

        public Graphic getGraphicVariant(int variant)
        {
            if (variants == null || !variants.ContainsKey(variant))
            {
                return missingTextureGraphic;
            }

            return variants[variant];
        }

        public PawnRenderNode_GraphicVariants(Pawn pawn, PawnRenderNodeProperties props, PawnRenderTree tree) : base(pawn, props, tree) {

            this.props = (PawnRenderNodeProperties_GraphicVariants)props;

        }

        protected virtual Dictionary<int, Graphic> GraphicVariantsFor(Pawn pawn)
        {

            if (props.texPathVariantsDef == null)
            {
                return null;
            }

            return GenerateVariants(pawn, props.texPathVariantsDef);

        }

        protected override void EnsureMaterialsInitialized()
        {

            if (variants == null)
            {
                variants = GraphicVariantsFor(this.tree.pawn);
            }
            if (missingTextureGraphic == null)
            {
                missingTextureGraphic = GenerateMissingTextureGraphic();
            }

            base.EnsureMaterialsInitialized();
        }

        //used by all, including base classes, to create texPathVariants for pawn
        protected Dictionary<int, Graphic> GenerateVariants(Pawn pawn, TexPathVariantsDef texPathVariants)
        {

            Dictionary<int, Graphic> variantGraphics = new Dictionary<int, Graphic>();

            //for each graphic variant
            for (int i = 0; i < texPathVariants.variants.Count; i++)
            {

                //get new graphic
                Graphic variant = GraphicDatabase.Get<Graphic_Multi>(texPathVariants.variants[i], this.ShaderFor(pawn), Vector2.one, this.ColorFor(pawn));

                //add it to the variants dictionary; i + 1 for easier readability in logs
                variantGraphics.Add(i + 1, variant);

            }

            return variantGraphics;

        }

        protected Graphic GenerateMissingTextureGraphic()
        {
            return GraphicDatabase.Get<Graphic_Multi>("AnimationProps/MissingTexture/MissingTexture");
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;

namespace Rimworld_Animations
{
    public class TexPathVariantsDef : Def
    {

        public List<string> variants;

    }
}