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 AnimationSettings : ModSettings {

        public static bool orgasmQuiver, rapeShiver, soundOverride = true, hearts = true, controlGenitalRotation = false, applySemenOnAnimationOrgasm = false, fastAnimForQuickie = false,
            PlayAnimForNonsexualActs = true;
        public static bool offsetTab = false, debugMode = false;
        public static float shiverIntensity = 2f;

        public static Dictionary<string, Vector2> offsets = new Dictionary<string, Vector2>();
        public static Dictionary<string, float> rotation = new Dictionary<string, float>();
        public static Dictionary<string, int> shiftCount = new Dictionary<string, int>();

        public override void ExposeData() {

            base.ExposeData();

            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 fastAnimForQuickie, "RJWAnimations-fastAnimForQuickie");
            Scribe_Values.Look(ref rapeShiver, "RJWAnimations-rapeShiver");
            Scribe_Values.Look(ref hearts, "RJWAnimation-sheartsOnLovin");
            Scribe_Values.Look(ref PlayAnimForNonsexualActs, "RJWAnims-PlayAnimForNonsexualActs");
            Scribe_Values.Look(ref applySemenOnAnimationOrgasm, "RJWAnimations-applySemenOnOrgasm", false);
            Scribe_Values.Look(ref soundOverride, "RJWAnimations-rjwAnimSoundOverride", true);
            Scribe_Values.Look(ref shiverIntensity, "RJWAnimations-shiverIntensity", 2f);
            //todo: save offsetsByDefName

            Scribe_Collections.Look(ref offsets, "RJWAnimations-animationOffsets");
            Scribe_Collections.Look(ref rotation, "RJWAnimations-rotationOffsets");
            Scribe_Collections.Look(ref shiftCount, "RJWAnimations-shiftCount");

            //needs to be rewritten
            //probably somewhere in options?

        }

    }

    public class RJW_Animations : Mod {

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

        }

        public override void DoSettingsWindowContents(Rect inRect) {

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

            listingStandard.CheckboxLabeled("Enable Sound Override", ref AnimationSettings.soundOverride);
            listingStandard.CheckboxLabeled("Control Genital Rotation", ref AnimationSettings.controlGenitalRotation);
            listingStandard.CheckboxLabeled("Play Fast Animation for Quickie", ref AnimationSettings.fastAnimForQuickie);
            listingStandard.CheckboxLabeled("Apply Semen on Animation Orgasm", ref AnimationSettings.applySemenOnAnimationOrgasm);

            if(AnimationSettings.applySemenOnAnimationOrgasm) {
                listingStandard.Label("Recommended--turn down \"Cum on body percent\" in RJW settings to about 33%");
            }

            listingStandard.CheckboxLabeled("Enable Orgasm Quiver", ref AnimationSettings.orgasmQuiver);
            listingStandard.CheckboxLabeled("Enable Rape Shiver", ref AnimationSettings.rapeShiver);
            listingStandard.CheckboxLabeled("Enable hearts during lovin'", ref AnimationSettings.hearts);
            listingStandard.CheckboxLabeled("Play animation for nonsexual acts (handholding, makeout)", ref AnimationSettings.PlayAnimForNonsexualActs);
            listingStandard.CheckboxLabeled("Enable Animation Manager Tab", ref AnimationSettings.offsetTab);

            listingStandard.Label("Shiver/Quiver Intensity (default 2): " + AnimationSettings.shiverIntensity);
            AnimationSettings.shiverIntensity = listingStandard.Slider(AnimationSettings.shiverIntensity, 0.0f, 12f);

            listingStandard.CheckboxLabeled("Debug Mode", ref AnimationSettings.debugMode);

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

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

        }

        public override string SettingsCategory() {
            return "RJW Animation Settings";
        }
    }
}


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RimWorld;
using rjw.Modules.Interactions.Helpers;
using rjw.Modules.Interactions.Objects;
using UnityEngine;
using Verse;
using Verse.AI;
using rjw.Modules.Interactions.Enums;

namespace Rimworld_Animations {
    public static class AnimationUtility {
        /*
         Note: always make the list in this order:
             Female pawns, animal female pawns, male pawns, animal male pawns
        */
        public static AnimationDef tryFindAnimation(ref List<Pawn> participants, rjw.xxx.rjwSextype sexType = 0, rjw.SexProps sexProps = null) {

            InteractionWithExtension interaction = InteractionHelper.GetWithExtension(sexProps.dictionaryKey);

            if(interaction.HasInteractionTag(InteractionTag.Reverse))
            {
                Pawn buffer = participants[1];
                participants[1] = participants[0];
                participants[0] = buffer;
            }

            participants =
                participants.OrderBy(p => p.jobs.curDriver is rjw.JobDriver_SexBaseInitiator)
                .OrderBy(p => rjw.xxx.can_fuck(p))
                .ToList();

            List<Pawn> localParticipants = new List<Pawn>(participants);

            IEnumerable<AnimationDef> options = DefDatabase<AnimationDef>.AllDefs.Where((AnimationDef x) => {

                if (x.actors.Count != localParticipants.Count) {
                    return false;
                }
                for (int i = 0; i < x.actors.Count; i++) {

                    if (rjw.RJWPreferenceSettings.Malesex == rjw.RJWPreferenceSettings.AllowedSex.Nohomo) {
                        if (rjw.xxx.is_male(localParticipants[i]) && x.actors[i].isFucked) {
                            return false;
                        }
                }
				if (x.actors[i].requiredGender != null && !x.actors[i].requiredGender.Contains(localParticipants[i].gender.ToStringSafe<Gender>()))
				{
					if (AnimationSettings.debugMode)
					{
						Log.Message(string.Concat(new string[]
						{
							x.defName.ToStringSafe<string>(),
							" not selected -- ",
							localParticipants[i].def.defName.ToStringSafe<string>(),
							" ",
							localParticipants[i].Name.ToStringSafe<Name>(),
							" does not match required gender"
						}));
					}
					return false;
				}
                    if ((x.actors[i].blacklistedRaces != null) && x.actors[i].blacklistedRaces.Contains(localParticipants[i].def.defName)) {
                        if(AnimationSettings.debugMode)
                            Log.Message(x.defName.ToStringSafe() + " not selected -- " + localParticipants[i].def.defName.ToStringSafe() + " " + localParticipants[i].Name.ToStringSafe() + " is blacklisted");
                        return false;
                    }

                    if(x.actors[i].defNames.Contains("Human")) {
                        if (!rjw.xxx.is_human(localParticipants[i])) {
                            if (AnimationSettings.debugMode)
                                Log.Message(x.defName.ToStringSafe() + " not selected -- " + localParticipants[i].def.defName.ToStringSafe() + " " + localParticipants[i].Name.ToStringSafe() + " is not human");

                            return false;
                        }

                    }
                    else if (!x.actors[i].bodyDefTypes.Contains(localParticipants[i].RaceProps.body)) {

                        if (!x.actors[i].defNames.Contains(localParticipants[i].def.defName)) {

                            if (rjw.RJWSettings.DevMode) {
                                string animInfo = x.defName.ToStringSafe() + " not selected -- " + localParticipants[i].def.defName.ToStringSafe() + " " + localParticipants[i].Name.ToStringSafe() + " is not ";
                                foreach(String defname in x.actors[i].defNames) {
                                    animInfo += defname + ", ";
                                }
                                if (AnimationSettings.debugMode)
                                    Log.Message(animInfo);
                            }

                            return false;
                        }
                    }
                    //genitals checking

                    if(!GenitalCheckForPawn(x.actors[i].requiredGenitals, localParticipants[i], out string failReason)) {
                        Debug.Log("Didn't select " + x.defName + ", " + localParticipants[i].Name + " " + failReason);
                        return false;
                    }

                    //TESTING ANIMATIONS ONLY REMEMBER TO COMMENT OUT BEFORE PUSH
                    /*
                    if (x.defName != "Cunnilingus")
                        return false;
                    */

                    if (x.actors[i].isFucking && !rjw.xxx.can_fuck(localParticipants[i])) {
                        if (AnimationSettings.debugMode)
                            Log.Message(x.defName.ToStringSafe() + " not selected -- " + localParticipants[i].def.defName.ToStringSafe() + " " + localParticipants[i].Name.ToStringSafe() + " can't fuck");
                        return false;
                    }

                    if (x.actors[i].isFucked && !rjw.xxx.can_be_fucked(localParticipants[i])) {
                        if (AnimationSettings.debugMode)
                            Log.Message(x.defName.ToStringSafe() + " not selected -- " + localParticipants[i].def.defName.ToStringSafe() + " " + localParticipants[i].Name.ToStringSafe() + " can't be fucked");
                        return false;
                    }
                }
                return true;
            });
            List<AnimationDef> optionsWithInteractionType = options.ToList().FindAll(x => x.interactionDefTypes != null && x.interactionDefTypes.Contains(sexProps.sexType.ToStringSafe()));
            if (optionsWithInteractionType.Any()) {
                if (AnimationSettings.debugMode)
                    Log.Message("Selecting animation for interaction type " + sexProps.sexType.ToStringSafe() + "...");
                return optionsWithInteractionType.RandomElement();
            }
            List<AnimationDef> optionsWithSexType = options.ToList().FindAll(x => x.sexTypes != null && x.sexTypes.Contains(sexType));
            if (optionsWithSexType.Any()) {
                if (AnimationSettings.debugMode)
                    Log.Message("Selecting animation for rjwSexType " + sexType.ToStringSafe() + "...");
                return optionsWithSexType.RandomElement();
            }

            /*
            if(optionsWithInitiator.Any()) {
                if (AnimationSettings.debugMode)
                    Log.Message("Selecting animation for initiators...");
            }
            */

            if (options != null && options.Any()) {
                if (AnimationSettings.debugMode)
                    Log.Message("Randomly selecting animation...");
                return options.RandomElement();
            }
            else
                return null;
        }

        public static void RenderPawnHeadMeshInAnimation1(Mesh mesh, Vector3 loc, Quaternion quaternion, Material material, bool drawNow, Pawn pawn) {

            if (pawn == null || pawn.Map != Find.CurrentMap) {
                GenDraw.DrawMeshNowOrLater(mesh, loc, quaternion, material, drawNow);
                return;
            }

            CompBodyAnimator pawnAnimator = pawn.TryGetComp<CompBodyAnimator>();

            if (pawnAnimator == null || !pawnAnimator.isAnimating) {
                GenDraw.DrawMeshNowOrLater(mesh, loc, quaternion, material, drawNow);
            } else {
                Vector3 pawnHeadPosition = pawnAnimator.getPawnHeadPosition();
                pawnHeadPosition.y = loc.y;
                GenDraw.DrawMeshNowOrLater(MeshPool.humanlikeHeadSet.MeshAt(pawnAnimator.headFacing), pawnHeadPosition, Quaternion.AngleAxis(pawnAnimator.headAngle, Vector3.up), material, true);
            }
        }

        public static void AdjustHead(ref Quaternion quat, ref Rot4 bodyFacing, ref Vector3 pos, ref float angle, Pawn pawn, PawnRenderFlags flags)
        {
            if (flags.FlagSet(PawnRenderFlags.Portrait)) return;

            CompBodyAnimator anim = pawn.TryGetComp<CompBodyAnimator>();
            if (anim.isAnimating)
            {
                bodyFacing = anim.headFacing;
                angle = anim.headAngle;
                quat = Quaternion.AngleAxis(anim.headAngle, Vector3.up);
                pos = anim.getPawnHeadOffset();

            }
        }

        public static void RenderPawnHeadMeshInAnimation(Mesh mesh, Vector3 loc, Quaternion quaternion, Material material, bool portrait, Pawn pawn, float bodySizeFactor = 1) {

            if (pawn == null) {
                GenDraw.DrawMeshNowOrLater(mesh, loc, quaternion, material, portrait);
                return;
            }

            CompBodyAnimator pawnAnimator = pawn.TryGetComp<CompBodyAnimator>();

            if (pawnAnimator == null || !pawnAnimator.isAnimating || portrait) {
                GenDraw.DrawMeshNowOrLater(mesh, loc, quaternion, material, portrait);
            }
            else {
                Vector3 pawnHeadPosition = pawnAnimator.getPawnHeadPosition();
                pawnHeadPosition.x *= bodySizeFactor;
                pawnHeadPosition.x *= bodySizeFactor;
                pawnHeadPosition.y = loc.y;
                GenDraw.DrawMeshNowOrLater(mesh, pawnHeadPosition, Quaternion.AngleAxis(pawnAnimator.headAngle, Vector3.up), material, portrait);
            }
        }

        public static bool GenitalCheckForPawn(List<string> requiredGenitals, Pawn pawn, out string failReason) {

            failReason = null;
            if (requiredGenitals != null) {
                if (requiredGenitals.Contains("Vagina")) {

                    if (!rjw.Genital_Helper.has_vagina(pawn)) {
                        failReason = "missing vagina";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("Penis")) {

                    if (!(rjw.Genital_Helper.has_multipenis(pawn) || rjw.Genital_Helper.has_penis_infertile(pawn) || rjw.Genital_Helper.has_penis_fertile(pawn) || rjw.Genital_Helper.has_ovipositorM(pawn) || rjw.Genital_Helper.has_ovipositorF(pawn))) {
                        failReason = "missing penis";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("Mouth")) {

                    if (!rjw.Genital_Helper.has_mouth(pawn)) {
                        failReason = "missing mouth";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("Anus")) {

                    if (!rjw.Genital_Helper.has_anus(pawn)) {
                        failReason = "missing anus";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("Breasts")) {
                    if (!rjw.Genital_Helper.can_do_breastjob(pawn)) {
                        failReason = "missing breasts";
                        return false;
                    }
                }

                if (requiredGenitals.Contains("NoVagina")) {

                    if (rjw.Genital_Helper.has_vagina(pawn)) {
                        failReason = "has vagina";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("NoPenis")) {

                    if ((rjw.Genital_Helper.has_multipenis(pawn) || rjw.Genital_Helper.has_penis_infertile(pawn) || rjw.Genital_Helper.has_penis_fertile(pawn))) {
                        failReason = "has penis";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("NoMouth")) {

                    if (rjw.Genital_Helper.has_mouth(pawn)) {
                        failReason = "has mouth";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("NoAnus")) {

                    if (rjw.Genital_Helper.has_anus(pawn)) {
                        failReason = "has anus";
                        return false;
                    }

                }

                if (requiredGenitals.Contains("NoBreasts")) {
                    if (rjw.Genital_Helper.can_do_breastjob(pawn)) {
                        failReason = "has breasts";
                        return false;
                    }
                }
            }

            return true;

        }

        public static Rot4 PawnHeadRotInAnimation(Pawn pawn, Rot4 regularPos)
        {
            Debug.Log("Test");

            if(pawn?.TryGetComp<CompBodyAnimator>() != null && pawn.TryGetComp<CompBodyAnimator>().isAnimating)
            {
                return pawn.TryGetComp<CompBodyAnimator>().headFacing;
            }

            return regularPos;
        }
    }
}

repo_name: TheGrimLarper/rimworld

license: unknown


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

namespace Rimworld_Animations
{
    public class PatchOperationAddOrReplace : PatchOperationPathed
    {

        protected string key;
        private XmlContainer value;

        protected override bool ApplyWorker(XmlDocument xml)
        {
            XmlNode valNode = value.node;
            bool result = false;
            IEnumerator enumerator = xml.SelectNodes(xpath).GetEnumerator();
            try
            {
                while (enumerator.MoveNext())
                {
                    object obj = enumerator.Current;
                    result = true;
                    XmlNode parentNode = obj as XmlNode;
                    XmlNode xmlNode = parentNode.SelectSingleNode(key);
                    if (xmlNode == null)
                    {
                        // Add - Add node if not existing
                        xmlNode = parentNode.OwnerDocument.CreateElement(key);
                        parentNode.AppendChild(xmlNode);
                    }
                    else
                    {
                        // Replace - Clear existing children
                        xmlNode.RemoveAll();
                    }
                    // (Re)add value
                    xmlNode.AppendChild(parentNode.OwnerDocument.ImportNode(valNode.FirstChild, true));
                }
            }
            finally
            {
                IDisposable disposable = enumerator as IDisposable;
                if (disposable != null)
                {
                    disposable.Dispose();
                }
            }
            return result;
        }

    }

}


code:
<?xml version="1.0" encoding="utf-8"?>

<ModMetaData>
	<name>Rimworld-Animations</name>
	<author>C0ffee</author>
  <url>https://gitgud.io/c0ffeeeeeeee/rimworld-animations</url>
	<supportedVersions>

1.1

1.2

1.3

	</supportedVersions>
	<packageId>c0ffee.rimworld.animations</packageId>
	<modDependencies>

			<packageId>brrainz.harmony</packageId>
			<displayName>Harmony</displayName>
			<steamWorkshopUrl>steam://url/CommunityFilePage/2009463077</steamWorkshopUrl>
			<downloadUrl>https://github.com/pardeike/HarmonyRimWorld/releases/latest</downloadUrl>

			<packageId>UnlimitedHugs.HugsLib</packageId>
			<displayName>HugsLib</displayName>
			<downloadUrl>https://github.com/UnlimitedHugs/RimworldHugsLib/releases/latest</downloadUrl>
			<steamWorkshopUrl>steam://url/CommunityFilePage/818773962</steamWorkshopUrl>

			<packageId>rim.job.world</packageId>
			<displayName>RimJobWorld</displayName>
			<downloadUrl>https://www.loverslab.com/topic/110270-mod-rimjobworld/</downloadUrl>

			<packageId>erdelf.HumanoidAlienRaces</packageId>
			<displayName>Humanoid Alien Races 2.0</displayName>
			<steamWorkshopUrl>https://steamcommunity.com/sharedfiles/filedetails/?id=839005762</steamWorkshopUrl>

	</modDependencies>
	<loadAfter>

UnlimitedHugs.HugsLib

brrainz.harmony

erdelf.humanoidalienraces

nals.facialanimation

com.yayo.yayoAni

	</loadAfter>
	<loadBefore>
	</loadBefore>
	<incompatibleWith>

velc.HatsDisplaySelection

	</incompatibleWith>
	<description>
    Rimworld Animations! Hurray!

    Questions or bugs?
    Chat with me on the forums: https://www.loverslab.com/topic/140386-rjw-animations/
    Or on the rjw discord: https://discord.gg/CXwHhv8

  </description>
</ModMetaData>


code:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Manifest>
  <identifier>Rimworld-Animations</identifier>
  <version>1.2.7</version>
</Manifest>


code:
<?xml version="1.0" encoding="utf-8"?>
<LanguageData>

  <OffsetManager.label>gerenciador de posições</OffsetManager.label>
  <OffsetManager.description>Controla as posições sexuais dos colonos.</OffsetManager.description>

</LanguageData>


code:
<?xml version="1.0" encoding="utf-8"?>
<LanguageData>

  <Dog_Doggystyle.label>estilo cachorrinho</Dog_Doggystyle.label>
  <Horse_Cowgirl.label>vaqueira</Horse_Cowgirl.label>

</LanguageData>


code:
<?xml version="1.0" encoding="utf-8"?>
<LanguageData>

  <Tribadism.label>tribadismo</Tribadism.label>
  <Cunnilingus.label>cunilíngua</Cunnilingus.label>

</LanguageData>


code:
<?xml version="1.0" encoding="utf-8"?>
<LanguageData>

  <Double_Penetration.label>dupla penetração</Double_Penetration.label>

</LanguageData>


code:
<?xml version="1.0" encoding="utf-8"?>
<LanguageData>

  <Doggystyle.label>estilo cachorrinho</Doggystyle.label>
  <Blowjob.label>boquete</Blowjob.label>
  <ReverseStandAndCarry.label>69</ReverseStandAndCarry.label>
  <Cowgirl.label>vaqueira</Cowgirl.label>

</LanguageData>


code:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<loadFolders>
	<v1.1>

/

1.1

	</v1.1>
	<v1.2>

/

1.2

Patch_HatsDisplaySelection/1.2

	</v1.2>
	<v1.3>

/

1.3

Patch_HatsDisplaySelection/1.2

Patch_SexToysMasturbation

Patch_SexToysMasturbation/1.3

	</v1.3>
	<v1.4>

/

1.4

Patch_HatsDisplaySelection/1.2

Patch_SexToysMasturbation

Patch_SexToysMasturbation/1.3

	</v1.4>

</loadFolders>


code:
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
	<Patch_SexToysMasturbation.SexToyAnimationDef>
		<defName>Masturbation_DildoVaginal</defName>
		dildo masturbation
		<requiredBodyPart>vagina</requiredBodyPart>
		<sounds>true</sounds>
		<sexTypes>

Masturbation

		</sexTypes>
		<actors>

				<defNames>

Human

				</defNames>
				<isFucked>true</isFucked>

		</actors>

		<animationStages>

				<stageName>Masturbating</stageName>
				<isLooping>true</isLooping>
				<playTimeTicks>917</playTimeTicks>
				<stageIndex>0</stageIndex>
				<animationClips>

						<layer>LayingPawn</layer>
						<keyframes>

								<tickDuration>40</tickDuration>
								<bodyAngle>73.01611</bodyAngle>
								<headAngle>40.0739746</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.054543376</bodyOffsetZ>
								<bodyOffsetX>0.112624526</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<soundEffect>Slimy</soundEffect>
								<tickDuration>30</tickDuration>
								<bodyAngle>76.4867554</bodyAngle>
								<headAngle>45.3887634</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.0506898165</bodyOffsetZ>
								<bodyOffsetX>0.08564949</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<tickDuration>30</tickDuration>
								<bodyAngle>78.22131</bodyAngle>
								<headAngle>48.0072327</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.039129138</bodyOffsetZ>
								<bodyOffsetX>0.07794231</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<tickDuration>30</tickDuration>
								<bodyAngle>76.4867554</bodyAngle>
								<headAngle>45.3887634</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.0506898165</bodyOffsetZ>
								<bodyOffsetX>0.08564949</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<tickDuration>1</tickDuration>
								<bodyAngle>73.01611</bodyAngle>
								<headAngle>40.0739746</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.054543376</bodyOffsetZ>
								<bodyOffsetX>0.112624526</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

						</keyframes>

						<keyframes>

								<tickDuration>40</tickDuration>
								<positionX>-0.359264076</positionX>
								<positionZ>-0.00901746</positionZ>
								<rotation>114.011215</rotation>

								<tickDuration>30</tickDuration>
								<positionX>-0.2783391</positionX>
								<positionZ>-0.0514066666</positionZ>
								<rotation>81.16443</rotation>

								<tickDuration>30</tickDuration>
								<positionX>-0.1704393</positionX>
								<positionZ>-0.0668209046</positionZ>
								<rotation>72.8611145</rotation>

								<tickDuration>30</tickDuration>
								<positionX>-0.2783391</positionX>
								<positionZ>-0.0514066666</positionZ>
								<rotation>81.16443</rotation>

								<tickDuration>1</tickDuration>
								<positionX>-0.359264076</positionX>
								<positionZ>-0.00901746</positionZ>
								<rotation>114.011215</rotation>

						</keyframes>

				</animationClips>

				<stageName>GettingIntoPosition</stageName>
				<isLooping>false</isLooping>
				<stageIndex>0</stageIndex>
				<animationClips>

						<layer>LayingPawn</layer>
						<keyframes>

								<tickDuration>50</tickDuration>
								<bodyAngle>73.01611</bodyAngle>
								<headAngle>40.0739746</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.054543376</bodyOffsetZ>
								<bodyOffsetX>0.112624526</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<soundEffect>Slimy</soundEffect>
								<tickDuration>1</tickDuration>
								<bodyAngle>81.65927</bodyAngle>
								<headAngle>58.8843079</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03912908</bodyOffsetZ>
								<bodyOffsetX>0.08950315</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

						</keyframes>

						<keyframes>

								<tickDuration>50</tickDuration>
								<positionX>-0.359264076</positionX>
								<positionZ>-0.00901746</positionZ>
								<rotation>114.011215</rotation>

								<tickDuration>1</tickDuration>
								<positionX>-0.2899</positionX>
								<positionZ>-0.0282852575</positionZ>
								<rotation>98.13748</rotation>

						</keyframes>

				</animationClips>

				<stageName>FastMasturbation</stageName>
				<isLooping>true</isLooping>
				<stageIndex>0</stageIndex>
				<playTimeTicks>1610</playTimeTicks>
				<animationClips>

						<layer>LayingPawn</layer>
						<keyframes>

								<tickDuration>20</tickDuration>
								<bodyAngle>81.65927</bodyAngle>
								<headAngle>58.8843079</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03912908</bodyOffsetZ>
								<bodyOffsetX>0.08950315</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<soundEffect>Slimy</soundEffect>
								<tickDuration>25</tickDuration>
								<bodyAngle>85.17255</bodyAngle>
								<headAngle>58.0615845</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03527552</bodyOffsetZ>
								<bodyOffsetX>0.0471138731</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<tickDuration>1</tickDuration>
								<bodyAngle>81.65927</bodyAngle>
								<headAngle>58.8843079</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03912908</bodyOffsetZ>
								<bodyOffsetX>0.08950315</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

						</keyframes>

						<keyframes>

								<tickDuration>25</tickDuration>
								<positionX>-0.2899</positionX>
								<positionZ>-0.0282852575</positionZ>
								<rotation>98.13748</rotation>

								<tickDuration>20</tickDuration>
								<positionX>-0.178146541</positionX>
								<positionZ>-0.01672452</positionZ>
								<rotation>96.95889</rotation>

								<tickDuration>1</tickDuration>
								<positionX>-0.2899</positionX>
								<positionZ>-0.0282852575</positionZ>
								<rotation>98.13748</rotation>

						</keyframes>

				</animationClips>

				<stageName>VeryFastMasturbation</stageName>
				<isLooping>true</isLooping>
				<stageIndex>0</stageIndex>
				<playTimeTicks>225</playTimeTicks>
				<animationClips>

						<layer>LayingPawn</layer>
						<keyframes>

								<tickDuration>6</tickDuration>
								<bodyAngle>81.65927</bodyAngle>
								<headAngle>58.8843079</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03912908</bodyOffsetZ>
								<bodyOffsetX>0.08950315</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<soundEffect>Slimy</soundEffect>
								<tickDuration>8</tickDuration>
								<bodyAngle>85.17255</bodyAngle>
								<headAngle>58.0615845</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03527552</bodyOffsetZ>
								<bodyOffsetX>0.0471138731</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<tickDuration>1</tickDuration>
								<bodyAngle>81.65927</bodyAngle>
								<headAngle>58.8843079</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03912908</bodyOffsetZ>
								<bodyOffsetX>0.08950315</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

						</keyframes>

						<keyframes>

								<tickDuration>6</tickDuration>
								<positionX>-0.2899</positionX>
								<positionZ>-0.0282852575</positionZ>
								<rotation>98.13748</rotation>

								<tickDuration>8</tickDuration>
								<positionX>-0.178146541</positionX>
								<positionZ>-0.01672452</positionZ>
								<rotation>96.95889</rotation>

								<tickDuration>1</tickDuration>
								<positionX>-0.2899</positionX>
								<positionZ>-0.0282852575</positionZ>
								<rotation>98.13748</rotation>

						</keyframes>

				</animationClips>

				<stageName>Orgasm</stageName>
				<isLooping>false</isLooping>
				<stageIndex>0</stageIndex>
				<animationClips>

						<layer>LayingPawn</layer>
						<keyframes>

								<soundEffect>Slimy</soundEffect>
								<tickDuration>6</tickDuration>
								<bodyAngle>81.65927</bodyAngle>
								<headAngle>58.8843079</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03912908</bodyOffsetZ>
								<bodyOffsetX>0.08950315</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<quiver>True</quiver>
								<soundEffect>Cum</soundEffect>
								<tickDuration>80</tickDuration>
								<bodyAngle>85.17255</bodyAngle>
								<headAngle>58.0615845</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03527552</bodyOffsetZ>
								<bodyOffsetX>0.0471138731</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<quiver>True</quiver>
								<soundEffect>Cum</soundEffect>
								<tickDuration>90</tickDuration>
								<bodyAngle>92.15109</bodyAngle>
								<headAngle>96.34238</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.0237147212</bodyOffsetZ>
								<bodyOffsetX>0.0432603136</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<quiver>True</quiver>
								<soundEffect>Cum</soundEffect>
								<tickDuration>70</tickDuration>
								<bodyAngle>92.15109</bodyAngle>
								<headAngle>96.34238</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.0237147212</bodyOffsetZ>
								<bodyOffsetX>0.0432603136</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<tickDuration>70</tickDuration>
								<bodyAngle>92.15109</bodyAngle>
								<headAngle>96.34238</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.0237147212</bodyOffsetZ>
								<bodyOffsetX>0.0432603136</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

								<tickDuration>1</tickDuration>
								<bodyAngle>81.65927</bodyAngle>
								<headAngle>58.8843079</headAngle>
								<genitalAngle>0</genitalAngle>
								<bodyOffsetZ>0.03912908</bodyOffsetZ>
								<bodyOffsetX>0.08950315</bodyOffsetX>
								<headBob>0</headBob>
								<bodyFacing>3</bodyFacing>
								<headFacing>3</headFacing>

						</keyframes>

						<keyframes>

								<tickDuration>6</tickDuration>
								<positionX>-0.2899</positionX>
								<positionZ>-0.0282852575</positionZ>
								<rotation>98.13748</rotation>

								<tickDuration>80</tickDuration>
								<positionX>-0.178146541</positionX>
								<positionZ>-0.01672452</positionZ>
								<rotation>96.95889</rotation>

								<tickDuration>90</tickDuration>
								<positionX>-0.178146541</positionX>
								<positionZ>-0.01672452</positionZ>
								<rotation>96.95889</rotation>

								<tickDuration>70</tickDuration>
								<positionX>-0.178146541</positionX>
								<positionZ>-0.01672452</positionZ>
								<rotation>96.95889</rotation>

								<tickDuration>70</tickDuration>
								<positionX>-0.178146541</positionX>
								<positionZ>-0.01672452</positionZ>
								<rotation>96.95889</rotation>

								<tickDuration>1</tickDuration>
								<positionX>-0.2899</positionX>
								<positionZ>-0.0282852575</positionZ>
								<rotation>98.13748</rotation>

						</keyframes>

				</animationClips>

		</animationStages>
	</Patch_SexToysMasturbation.SexToyAnimationDef>
</Defs>


code:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{87763712-0536-4D5F-9EAA-520F15D4F84E}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>Patch_SexToysMasturbation</RootNamespace>
    <AssemblyName>Patch_SexToysMasturbation</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>false</DebugSymbols>
    <DebugType>none</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>1.3\Assemblies\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="0Harmony">
      <HintPath>..\..\..\..\..\workshop\content\294100\1127530465\1.3\Assemblies\0Harmony.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Assembly-CSharp">
      <HintPath>..\..\..\RimWorldWin64_Data\Managed\Assembly-CSharp.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Rimworld-Animations, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>..\1.3\Assemblies\Rimworld-Animations.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="RJW">
      <HintPath>..\..\rjw\1.3\Assemblies\RJW.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="RJW-ToysAndMasturbation">
      <HintPath>..\..\rjw-toys-and-masturbation\Assemblies\RJW-ToysAndMasturbation.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
    <Reference Include="UnityEngine">
      <HintPath>..\..\..\RimWorldWin64_Data\Managed\UnityEngine.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.CoreModule">
      <HintPath>..\..\..\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="Source\Defs\SexToyAnimationDef.cs" />
    <Compile Include="Source\Patches\HarmonyPatch_JobDriver_SexBaseInitiator.cs" />
    <Compile Include="Source\Harmony\Harmony_PatchAll.cs" />
    <Compile Include="Source\Utilities\AnimSexToyUtility.cs" />
  </ItemGroup>
  <ItemGroup>
    <Folder Include="1.3\Assemblies\" />
  </ItemGroup>
  <ItemGroup>
    <Content Include="1.3\Defs\AnimationDefs\Animations_Dildo.xml" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>


code:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Patch_SexToysMasturbation")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Patch_SexToysMasturbation")]
[assembly: AssemblyCopyright("Copyright ©  2022")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.  If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("87763712-0536-4d5f-9eaa-520f15d4f84e")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]


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

namespace Patch_SexToysMasturbation
{
    public class SexToyAnimationDef : AnimationDef
    {

        public String requiredBodyPart = null;

    }
}


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 Patch_SexToysMasturbation
{

    [StaticConstructorOnStartup]
    public static class Harmony_PatchAll {

        static Harmony_PatchAll() {

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

        }
    }
}


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

namespace Patch_SexToysMasturbation
{
    [HarmonyPatch(typeof(JobDriver_SexBaseInitiator), "Start")]
    public class HarmonyPatch_JobDriver_SexBaseInitiator
    {

        public static void Postfix(ref JobDriver_SexBaseInitiator __instance)
        {

            if(__instance is JobDriver_MasturbateWithToy masturbateJobDriver)
            {

                Log.Message("Rerolling animations...");
                Pawn pawn = masturbateJobDriver.pawn;
                Thing sexToy = masturbateJobDriver.dildo;

                RerollAnimationsForSexToy(pawn, sexToy, masturbateJobDriver.Bed);
            }

        }

        public static void RerollAnimationsForSexToy(Pawn pawn, Thing thing, Thing bed)
        {
            CompSexToy sextoy = thing.TryGetComp<CompSexToy>();

            SexToyAnimationDef anim = AnimSexToyUtility.tryFindAnimation(sextoy, pawn);

            if (anim != null)
            {
                Log.Message("Playing anim " + anim.defName);

                if(bed != null)
                {
                    pawn.TryGetComp<CompBodyAnimator>().setAnchor(bed);
                    thing.TryGetComp<CompThingAnimator>().setAnchor(bed);
                }
                else
                {
                    pawn.TryGetComp<CompBodyAnimator>().setAnchor(pawn.Position);
                    thing.TryGetComp<CompThingAnimator>().setAnchor(pawn.Position);
                }

                bool mirror = GenTicks.TicksGame % 2 == 0;

                pawn.TryGetComp<CompBodyAnimator>().StartAnimation(anim, new List<Pawn> { pawn }, 0, mirror);
                thing.TryGetComp<CompThingAnimator>().StartAnimation(anim, pawn, mirror);

                (pawn.jobs.curDriver as JobDriver_Sex).ticks_left = anim.animationTimeTicks;
                (pawn.jobs.curDriver as JobDriver_Sex).sex_ticks = anim.animationTimeTicks;
                (pawn.jobs.curDriver as JobDriver_Sex).duration = anim.animationTimeTicks;
            }
            else
            {
                Log.Message("No animation found");
            }

        }

    }

}


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

namespace Patch_SexToysMasturbation
{
    public class AnimSexToyUtility
    {

        public static SexToyAnimationDef tryFindAnimation(CompSexToy sexToy, Pawn pawn)
        {

            IEnumerable<SexToyAnimationDef> options = DefDatabase<SexToyAnimationDef>.AllDefs.Where((SexToyAnimationDef x) =>
            {

                if(!sexToy.Props.requiredBodyParts.Contains(x.requiredBodyPart))
                {
                    return false;
                }

                if(x.requiredBodyPart == "vagina" && !Genital_Helper.has_vagina(pawn))
                {
                    return false;
                }

                return true;

            });

            if(options != null && options.Any())
            {
                return options.RandomElement();
            }
            else
            {
                return null;
            }

        }

    }
}


code:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Rimworld-Animations")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Rimworld-Animations")]
[assembly: AssemblyCopyright("Copyright ©  2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.  If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("71b05d71-67b2-4014-82cd-18c20ac0882f")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]


code:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{71B05D71-67B2-4014-82CD-18C20AC0882F}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>Rimworld_Animations</RootNamespace>
    <AssemblyName>Rimworld-Animations</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>false</DebugSymbols>
    <DebugType>none</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>1.3\Assemblies\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>none</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="0Harmony">
      <HintPath>..\..\..\..\workshop\content\294100\2009463077\Current\Assemblies\0Harmony.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="AlienRace">
      <HintPath>..\..\..\..\workshop\content\294100\839005762\1.3\Assemblies\AlienRace.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Assembly-CSharp">
      <HintPath>..\..\RimWorldWin64_Data\Managed\Assembly-CSharp.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="RJW">
      <HintPath>..\rjw\1.3\Assemblies\RJW.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
    <Reference Include="UnityEngine">
      <HintPath>..\..\RimWorldWin64_Data\Managed\UnityEngine.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.CoreModule">
      <HintPath>..\..\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="1.3\Source\Patches\RimworldPatches\HarmonyPatch_Thing.cs" />
    <Compile Include="1.3\Source\Utilities\PatchOperationAddOrReplace.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="1.3\Source\Actors\Actor.cs" />
    <Compile Include="1.3\Source\Actors\AlienRaceOffset.cs" />
    <Compile Include="1.3\Source\Actors\BodyTypeOffset.cs" />
    <Compile Include="1.3\Source\Animations\AnimationStage.cs" />
    <Compile Include="1.3\Source\Animations\Clips\BaseAnimationClip.cs" />
    <Compile Include="1.3\Source\Animations\Clips\PawnAnimationClip.cs" />
    <Compile Include="1.3\Source\Animations\Clips\ThingAnimationClip.cs" />
    <Compile Include="1.3\Source\Animations\Keyframes\Keyframe.cs" />
    <Compile Include="1.3\Source\Animations\Keyframes\PawnKeyframe.cs" />
    <Compile Include="1.3\Source\Animations\Keyframes\ThingKeyframe.cs" />
    <Compile Include="1.3\Source\Utilities\AnimationUtility.cs" />
    <Compile Include="1.3\Source\Comps\CompBodyAnimator.cs" />
    <Compile Include="1.3\Source\Comps\CompProperties_BodyAnimator.cs" />
    <Compile Include="1.3\Source\Comps\CompProperties_ThingAnimator.cs" />
    <Compile Include="1.3\Source\Comps\CompThingAnimator.cs" />
    <Compile Include="1.3\Source\Defs\AnimationDef.cs" />
    <Compile Include="1.3\Source\MainTabWindows\WorldComponent_UpdateMainTab.cs" />
    <Compile Include="1.3\Source\MainTabWindows\MainTabWindow_OffsetConfigure.cs" />
    <Compile Include="1.3\Source\MainTabWindows\OffsetMainButtonDefOf.cs" />
    <Compile Include="1.3\Source\Patches\OtherModPatches\HarmonyPatch_AlienRace.cs" />
    <Compile Include="1.3\Source\Patches\OtherModPatches\HarmonyPatch_CSL.cs" />
    <Compile Include="1.3\Source\Patches\OtherModPatches\HarmonyPatch_DontShaveYourHead.cs" />
    <Compile Include="1.3\Source\Patches\OtherModPatches\HarmonyPatch_FacialAnimation.cs" />
    <Compile Include="1.3\Source\Patches\OtherModPatches\HarmonyPatch_HatsDisplaySelection.cs" />
    <Compile Include="1.3\Source\Patches\OtherModPatches\HarmonyPatch_ShowHairWithHats.cs" />
    <Compile Include="1.3\Source\Patches\RimworldPatches\HarmonyPatch_HeadHair.cs" />
    <Compile Include="1.3\Source\Patches\RimworldPatches\HarmonyPatch_PawnRenderer.cs" />
    <Compile Include="1.3\Source\Patches\RimworldPatches\HarmonyPatch_PawnRotation.cs" />
    <Compile Include="1.3\Source\Patches\RimworldPatches\HarmonyPatch_Pawn_DrawTracker.cs" />
    <Compile Include="1.3\Source\Patches\RimworldPatches\HarmonyPatch_SetPawnAnimatable.cs" />
    <Compile Include="1.3\Source\Patches\Harmony_PatchAll.cs" />
    <Compile Include="1.3\Source\Patches\RJWPatches\JobDrivers\HarmonyPatch_JobDriver_JoinInBed.cs" />
    <Compile Include="1.3\Source\Patches\RJWPatches\JobDrivers\HarmonyPatch_JobDriver_SexBaseInitiator.cs" />
    <Compile Include="1.3\Source\Patches\RJWPatches\HarmonyPatch_PlaySexSounds.cs" />
    <Compile Include="1.3\Source\Patches\RJWPatches\HarmonyPatch_SexTick.cs" />
    <Compile Include="1.3\Source\Patches\RJWPatches\HarmonyPatch_WorkGiverSex.cs" />
    <Compile Include="1.3\Source\Patches\RJWPatches\JobDrivers\HarmonyPatch_JobDriver_SexBaseReceiverLoved.cs" />
    <Compile Include="1.3\Source\Settings\AnimationSettings.cs" />
  </ItemGroup>
  <ItemGroup>
    <Content Include="1.1\Assemblies\Rimworld-Animations.dll" />
    <Content Include="1.3\Assemblies\Rimworld-Animations.dll" />
    <Content Include="About\About.xml" />
    <Content Include="About\Manifest.xml" />
    <Content Include="1.3\Defs\AnimationDefs\Animations_Beast.xml" />
    <Content Include="1.3\Defs\AnimationDefs\Animations_Lesbian.xml" />
    <Content Include="1.3\Defs\AnimationDefs\Animations_Masturbate.xml" />
    <Content Include="1.3\Defs\AnimationDefs\Animations_Multi.xml" />
    <Content Include="1.3\Defs\AnimationDefs\Animations_vanilla.xml" />
    <Content Include="1.3\Defs\AnimationDefs\Animations_Vanilla2.xml" />
    <Content Include="1.3\Defs\MainTabDefs\MainButtonDef.xml" />
    <Content Include="1.3\Defs\SoundDefs\Sounds_Sex.xml" />
    <Content Include="Languages\PortugueseBrazilian\DefInjected\MainButtonDef\MainButtonDef.xml" />
    <Content Include="Languages\PortugueseBrazilian\DefInjected\Rimworld_Animations.AnimationDef\Animations_Beast.xml" />
    <Content Include="Languages\PortugueseBrazilian\DefInjected\Rimworld_Animations.AnimationDef\Animations_Lesbian.xml" />
    <Content Include="Languages\PortugueseBrazilian\DefInjected\Rimworld_Animations.AnimationDef\Animations_Multi.xml" />
    <Content Include="Languages\PortugueseBrazilian\DefInjected\Rimworld_Animations.AnimationDef\Animations_vanilla.xml" />
    <Content Include="LoadFolders.xml" />
  </ItemGroup>
  <ItemGroup>
    <Folder Include="1.2\" />
    <Folder Include="1.3\Defs\JobDefs\" />
    <Folder Include="1.3\Source\JobDrivers\" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>


code:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29905.134
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rimworld-Animations", "Rimworld-Animations.csproj", "{71B05D71-67B2-4014-82CD-18C20AC0882F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patch_HatsDisplaySelection", "Patch_HatsDisplaySelection\Patch_HatsDisplaySelection.csproj", "{BA766964-1716-422D-A09E-29426F8EB9D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patch_SexToysMasturbation", "Patch_SexToysMasturbation\Patch_SexToysMasturbation.csproj", "{87763712-0536-4D5F-9EAA-520F15D4F84E}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{71B05D71-67B2-4014-82CD-18C20AC0882F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{71B05D71-67B2-4014-82CD-18C20AC0882F}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{71B05D71-67B2-4014-82CD-18C20AC0882F}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{71B05D71-67B2-4014-82CD-18C20AC0882F}.Release|Any CPU.Build.0 = Release|Any CPU
		{BA766964-1716-422D-A09E-29426F8EB9D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{BA766964-1716-422D-A09E-29426F8EB9D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{BA766964-1716-422D-A09E-29426F8EB9D5}.Release|Any CPU.Build.0 = Release|Any CPU
		{87763712-0536-4D5F-9EAA-520F15D4F84E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{87763712-0536-4D5F-9EAA-520F15D4F84E}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{87763712-0536-4D5F-9EAA-520F15D4F84E}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{87763712-0536-4D5F-9EAA-520F15D4F84E}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {7E01292A-D76F-4CA3-AA64-085B84ECD905}
	EndGlobalSection
EndGlobal


code:
# Archive Team's archiving project
If you have mediafire links you want to archive you can submit them to the [Archive Team](https://archiveteam.org/index.php?title=MediaFire). If you don't have any fancy raidz configurations this will be the best way to preserve them.

# Description
mf-dl is a command line program for downloading mediafire links in bulk. It also contains a web crawler which can be used for searching for mediafire links.

# Tutorial
If you're looking for a Crash Course, check out Data Horde's Tutorial:

[How to Archive or Scrape MediaFire Files using mf-dl](https://datahorde.org/how-to-archive-or-scrape-mediafire-files-using-mf-dl/).

# Running on Windows:
1. Download and install python3 from their [website](https://www.python.org/)
2. Clone this repo
3. Open the command line
4. Go to the directory with mf-dl using `cd DIRECTORY`
5. Install all of the dependencies using `python3 -m pip install -r requirements.txt`

# Running on GNU/Linux:
1. Clone this repo
2. Go to the directory containing mf-dl
3. Using your package manager install python3 and download all of the dependencies manually or run `python3 -m pip install -r requirements.txt` in the directory with mf-dl to install them automatically

# mfdl.py
	usage: mfdl.py [-h] [--only-meta] [--threads THREADS] [--archive-mode]
	               output input [input ...]

	Mediafire downloader

	positional arguments:
	  output             Output directory
	  input              Input file/files which will be searched for links

	optional arguments:
	  -h, --help         show this help message and exit
	  --only-meta        Only download *.info.json files and avatars
	  --threads THREADS  How many threads to use; in case mediafire starts showing
	                     captchas or smth the amount of threads should be reduced;
	                     default is 6
	  --archive-mode, -a Use a flat directory layout and save all file metadata in *.info.json files

# web_crawler.py
	usage: web_crawler.py [-h] [--threads THREADS]
	                      [--filter FILTER | --regex REGEX]
	                      start output

	Mediafire link web scraper

	positional arguments:
	  start              Start URL from which the scraper will begin to
	                     recursively scrape pages
	  output             File where a list of links will be saved

	optional arguments:
	  -h, --help         show this help message and exit
	  --threads THREADS  How many threads to use; if the site you're trying to
	                     crawl will start displaying captchas or smth the amount
	                     of threads should be reduced; default is 6
	  --filter FILTER    Only scrape websites where filter is found in the url
	  --regex REGEX      Same as filter but uses regex

# Legacy Directory structure:
* File: keys/\*/FILENAME
* File and folder metadata: keys/\*.info.json
* Conv links: conv/\*
* Keys of custom folder names: custom_folders.txt

File keys are always 11, 15 or 31 characters long and folder keys are 13 or 19 characters long.
Folder metadata files contain references to file or folder keys to avoid downloading the same file in a directory
and as a stand alone file. Navigating through folders is a bit of a pain in the ass atm but I plan on making a tool to browse
saved archives

# Supported links
* http(s)://\*.mediafire.com/?KEY
* http(s)://\*.mediafire.com/\*/KEY
* http(s)://\*.mediafire.com/\*.php?KEY
* http(s)://\*.mediafire.com/convkey/\*/KEY??.EXT
* http(s)://\*.mediafire.com/CUSTOM_FOLDER_NAME
* http(s)://\*.mediafire.com/CONV_NAME.EXT (no metadata)
* http(s)://\*.mediafire.com/conv/CONV_NAME.EXT (no metadata)
* http(s)://\*.mediafire.com/?sharekey=SHAREKEY
* mfi.re domain
* Obfuscated versions of the above e.g. mediafire (.) com (/) (?) KEY

license: mit


code:
#!/bin/env python3
import urllib.parse, re

URL_RE = re.compile("http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")

def truncate_schema(url):
	return url[url.find("://")+3:]

def get_urls(string):
	return URL_RE.findall(string)

def get_urls_from_files(files):
	urls = []
	for fl in files: #Read descriptions
		lines = []
		with open(fl, "r") as fl:
			words = fl.read().replace("\n", " ")
		urls = get_urls(words)
	return urls

if(__name__ == "__main__"):
	import sys

	if(len(sys.argv) < 2):
		print("Usage: ")
		print(" ./{} FILE [FILE]".format(sys.argv[0]))
		exit()

	urls = get_urls_from_files(sys.argv[1:])
	for url in urls:
		print(url)


code:
#!/bin/env python3
import re

#Searches for keys and lists of keys; also considers url encoded symbols
MF_KEY_RE = re.compile("(?i)(?:media.{0,5}?fire.{0,5}?com|mfi.{0,5}?re).{0,40}?(?:(?:\/|%2F)[^\r\n\/]{0,40}?(?:\/|%2F)|(?:\?|%3F))[^\r\n\/]{0,40}?([0-9a-z,.]{22,}|[0-9a-z,]{19}|[0-9a-z,]{15}|[0-9a-z,]{13}|[0-9a-z,]{11})")

#Searches for conv links to files (e.g. https://img19.mediafire.com/22d9d9406ca5a970a04cf542eb7e210241fcd8ca47494a3483729a9b7320afbb5g.jpg which can also be
#https://img19.mediafire.com/conv/22d9d9406ca5a970a04cf542eb7e210241fcd8ca47494a3483729a9b7320afbb5g.jpg)
#and for links to custom folders (e.g. https://www.mediafire.com/MonHun); this regex also considers url encoded symbols
MF_LINK_RE = re.compile(r"(?i)(?:media.{0,5}?fire.{0,5}?com|mfi.{0,5}?re)[^\/?%\r\n]{0,5}?(?:\/|%2F)[^\/?%\r\n]{0,5}?([a-zA-Z0-9_.]+)\s")

def get_mediafire_links(text):
	#Get all of the mediafire keys, conv links and custom folders from text
	output = {"keys": [], "conv": [], "custom_folders": []}

	mf_key_matches = MF_KEY_RE.findall(text)
	for match in mf_key_matches:
		if("." in match): #/conv/ link
			output["conv"].append("/conv/" + match)
			continue
		for mf_key in match.split(","): #mf key; mediafire allows specifying multiple keys by separating them with ","
			if(len(mf_key) in [11,13,15,19,31]):
				if(mf_key not in output["keys"]): output["keys"].append(mf_key) #It's a valid key
			else:
				#There are also sharekey links: http://www.mediafire.com/?sharekey=2c58ee7e4f7e7dc2312dbd5f2bdc506260259ca196fcd1eb416b94653a3044fd
				#Don't know how long they are so just to be sure treat every key that isn't 11,13,15,19 or 31 chars long as a sharekey.
				#Difference between custom named folders and these links is that sharekey links redirect to regular links with keys, but
				#the method of resolving custom names to keys should work with these links too.
				sharekey_link = "?sharekey="+mf_key
				if(sharekey_link not in output["custom_folders"]): output["custom_folders"].append(sharekey_link)

	mf_link_matches = MF_LINK_RE.findall(text)
	for match in mf_link_matches:
		if("." in match): #file
			if(match not in output["conv"]): output["conv"].append("/conv/" + match)
		else: #custom folder
			if(match not in output["custom_folders"]): output["custom_folders"].append(match)

	return output

def read_mediafire_links(files):
	#Get all of the mediafire keys, conv links and custom folders from list of files
	output = {"keys": [], "conv": [], "custom_folders": []}
	for fname in files:
		with open(fname, "r") as fl:
			data = fl.read()
		mf_links = get_mediafire_links(data)
		for link in mf_links["keys"]: #Remove duplicates across files
			if(link not in output["keys"]): output["keys"].append(link.lower()) #Everything except links is case insensitive and links are lowercase anyway
		for link in mf_links["conv"]:
			if(link not in output["conv"]): output["conv"].append(link.lower())
		for link in mf_links["custom_folders"]:
			if(link not in output["custom_folders"]): output["custom_folders"].append(link.lower())

	return output

if(__name__ == "__main__"):
	import sys

	if(len(sys.argv) < 2):
		print("Usage: ")
		print(" ./{} FILE [FILE]".format(sys.argv[0]))
		exit()

	urls = read_mediafire_links(sys.argv[1:])

	if(urls["keys"]):
			print("--- KEYS ---")
			for fl in urls["keys"]:
				print("https://mediafire.com/?{}".format(fl))
			print()

	if(urls["conv"]):
			print("--- CONV LINKS ---")
			for fl in urls["conv"]:
				print("https://mediafire.com{}".format(fl))
			print()

	if(urls["custom_folders"]):
			print("--- CUSTOM FOLDERS ---")
			for fl in urls["custom_folders"]:
				print("https://mediafire.com/{}".format(fl))
			print()


code:
Mediafire's API:

# Get file info
POST https://www.mediafire.com/api/1.5/file/get_info.php
quick_key=FILE_KEY
response_format=json

# Get folder info
POST https://www.mediafire.com/api/1.4/folder/get_info.php
recursive=yes
folder_key=FOLDER_KEY
response_format=json

# List folders in folder
GET https://www.mediafire.com/api/1.4/folder/get_content.php
content_type=folders
filter=all
order_by=name
order_direction=asc
chunk=1
version=1.5
folder_key=FOLDER_KEY
response_format=json

# List files in folder
GET https://www.mediafire.com/api/1.4/folder/get_content.php
content_type=files
filter=all
order_by=name
order_direction=asc
chunk=1
version=1.5
folder_key=FOLDER_KEY
response_format=json


code:
import threading
from colorama import init
init() #This should fix ansi escape codes on windows TODO: test it

def log(msg):
	thread_name = threading.current_thread().name
	if(thread_name != "MainThread"):
		print("\033[90mThread #{}\033[0m ".format(thread_name), end="")
	print(msg)


code:
#!/bin/env python3
import requests, json, os, traceback, time, random, sys, argparse, queue, threading, multiprocessing, re, analyze_mediafire
from log import log

TIMEOUT_T = 30
HTTP_HEADERS = {
	"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0" #Spoof firefox user agent
}
CUSTOM_FOLDER_RE = re.compile(r'afI= "([a-z0-9]{13}|[a-z0-9]{19})"') #Regex used for locating keys on pages with custom named folders e.g. https://www.mediafire.com/MonHun

ARCHIVE_LOCK = threading.Lock()
PRINT_LOCK = threading.Lock()

def download_url(url, local_filename):
	#Save url as local_filename
	r = requests.get(url, headers=HTTP_HEADERS, stream=True, timeout=TIMEOUT_T)
	#If error
	if(not r.ok):
		return r.status_code
	#Download
	with open(local_filename, 'wb') as f:
		for chunk in r.iter_content(chunk_size=1024):
			if chunk: # filter out keep-alive new chunks
				f.write(chunk)
	return r.status_code

def get_file_metadata(file_id):
	#Get "response" key from mediafire's file/get_info.php API function
	rq = requests.post("https://www.mediafire.com/api/1.5/file/get_info.php", params={"quick_key": file_id, "response_format": "json"}, headers=HTTP_HEADERS, timeout=TIMEOUT_T)
	return rq.json()["response"]

def find_direct_url(info_url):
	#Find a direct download url on an info page
	rq = requests.get(info_url, headers=HTTP_HEADERS, timeout=TIMEOUT_T)
	web_html = rq.text

	download_link_prefix = '\nPreparing your download…\n<a class="input popsok" aria-label="Download file" href="'
	uploaded_from_prefix = "
This file was uploaded from "

	if((web_html.find(download_link_prefix) == -1)): #If not found
		return {"success": 0}

	#Get direct url
	direct_url = web_html[web_html.find(download_link_prefix)+len(download_link_prefix):]
	direct_url = direct_url[:direct_url.find('"')]

	#Get location which only seems to be available on these info pages
	uploaded_from = web_html[web_html.find(uploaded_from_prefix)+len(uploaded_from_prefix):]
	uploaded_from = uploaded_from[:uploaded_from.find("
")]
	location = uploaded_from[:uploaded_from.find(" on ")]

	return {"url": direct_url, "location": location, "success": 1}

def download_file(mediafire_id, output, only_meta=0, archive_mode=False):
	#Returns 1 on success and 0 otherwise
	metadata = get_file_metadata(mediafire_id)
	if(metadata["result"] != "Success"): #Error from mediafire
		with PRINT_LOCK:
			log("\033[90m{}:\033[0m \033[31m{}: {}\033[0m".format(mediafire_id, metadata["result"], metadata["message"]))
		return 0 #Skip file

	#Display info
	with PRINT_LOCK:
		log("\033[90m{}: {}\033[0m \033[96m{}\033[0m \033[95m{}\033[0m".format(mediafire_id,
		                                                                       metadata["file_info"]["created"],
		                                                                       metadata["file_info"]["owner_name"],
		                                                                       metadata["file_info"]["filename"]))

	if not archive_mode and not only_meta:
		filename = metadata["file_info"]["filename"]
		if "/" in filename or ".." == filename:
			with PRINT_LOCK:
				log("Dangerous name {} for file with id {}".format(filename, mediafire_id))
			filename = mediafire_id
		output = output + "/" + filename
		if(os.path.exists(output)): #Duplicate
			log("\033[90m{}:\033[0m \033[33mFile already exists. Skipping...\033[0m".format(mediafire_id))
			return (1, None)

	#Individually shared files point to an info page, but files shared in a folder point directly to the file
	dwnld_head = requests.head(metadata["file_info"]["links"]["normal_download"], headers=HTTP_HEADERS, timeout=TIMEOUT_T).headers
	if(str(dwnld_head.get("Location")).startswith("https://download")): #Direct
		direct_url = metadata["file_info"]["links"]["normal_download"]
	else: #Info page
		direct_url = find_direct_url(metadata["file_info"]["links"]["normal_download"])
		#If couldn't find a download link; There needs to be an additional check because mediafire's API still returns info about files which were taken down
		if(direct_url["success"] == 0):
			with PRINT_LOCK:
				log("\033[90m{}: \033[0m\033[31mCouldn't find download url\033[0m".format(mediafire_id))
			return 0
		metadata["location"] = direct_url["location"]
		direct_url = direct_url["url"]

	#Download file
	if not only_meta:
		if archive_mode:
			output_dir = output + "/keys/" + mediafire_id
			output_fname = output_dir + "/" + metadata["file_info"]["filename"]
		else:
			output_dir = output[0:output.rfind('/')]
			output_fname = output
		os.makedirs(output_dir, exist_ok=True)
		download_url(direct_url, output_fname)
	#Write metadata
	if archive_mode or only_meta:
		with open(output + "/keys/" + mediafire_id + ".info.json", "w") as fl:
			fl.write(json.dumps(metadata))
	return 1

def get_folder_content(folder_key, content_type, chunk):
	#Get "response" key from mediafire's folder/get_info.php API function
	rq = requests.get("https://www.mediafire.com/api/1.4/folder/get_content.php",
	                  params={"content_type": content_type, "chunk": chunk, "folder_key": folder_key, "response_format": "json"},
					  headers=HTTP_HEADERS,
					  timeout=TIMEOUT_T)
	return rq.json()["response"]

def get_folder_metadata(folder_key):
	#Get "response" key from mediafire's folder/get_info.php API function
	rq = requests.post("https://www.mediafire.com/api/1.5/folder/get_info.php", params={"folder_key": folder_key, "response_format": "json"}, headers=HTTP_HEADERS, timeout=TIMEOUT_T)
	return rq.json()["response"]

def download_folder(mediafire_id, output_dir, only_meta=0, archive=[], archive_mode=False):
	#Recursively downloads a folder
	#Returns 1 on success and 0 otherwise
	metadata = get_folder_metadata(mediafire_id)
	if(metadata["result"] != "Success"): #Error from mediafire
		with PRINT_LOCK:
			log("\033[90m{}: \033[0m\033[31m{}: {}\033[0m".format(mediafire_id, metadata["result"], metadata["message"]))
		return (0, None) #Skip folder

	with PRINT_LOCK:
		log("\033[90m{}: {}\033[0m \033[96m{}\033[0m \033[95m{}\033[0m".format(mediafire_id,
		                                                                       metadata["folder_info"]["created"],
		                                                                       metadata["folder_info"]["owner_name"],
		                                                                       metadata["folder_info"]["name"]))

	metadata["children"] = {"folders": [], "files": []}
	folder_contents = ((metadata["folder_info"]["name"], mediafire_id), [])

	#Download folders inside
	chunk = 1
	more_chunks = True
	while(more_chunks != "no"):
		children_folders_chunk = get_folder_content(mediafire_id, "folders", chunk)
		metadata["children"]["folders"] += children_folders_chunk["folder_content"]["folders"]
		more_chunks = children_folders_chunk["folder_content"]["more_chunks"]
		chunk+=1
	for folder in metadata["children"]["folders"]:
		folder_contents[1].append(folder["folderkey"])

	#Download files inside
	chunk = 1
	more_chunks = True
	while(more_chunks != "no"):
		children_files_chunk = get_folder_content(mediafire_id, "files", chunk)
		metadata["children"]["files"] += children_files_chunk["folder_content"]["files"]
		more_chunks = children_files_chunk["folder_content"]["more_chunks"]
		chunk+=1
	for fl in metadata["children"]["files"]:
		folder_contents[1].append(fl["quickkey"])

	#Download avatar
	avatar_keys = analyze_mediafire.get_mediafire_links(metadata["folder_info"]["avatar"])["keys"]
	for avatar in avatar_keys: #There should be only 1 key in an avatar link, but loop through it just to be sure
		folder_contents[1].append(avatar)

	#Write metadata
	if archive_mode or only_meta:
		if(os.path.exists(output_dir + "/keys/" + mediafire_id + ".info.json")): #Don't overwrite the .info.json file
			log("\033[90m{}:\033[0m \033[33mFile already exists. Skipping...\033[0m".format(mediafire_id))
			return (1, folder_contents)
		with open(output_dir + "/keys/" + mediafire_id + ".info.json", "w") as fl:
			fl.write(json.dumps(metadata))

	return (1, folder_contents)

def download(mediafire_id, output, only_meta=0, archive=[], archive_mode=False):
	#Download mediafire key and save it in output
	#In case of a mediafire error return 0
	#In case of an exception - retry after 10 seconds
	#Otherwise return 1
	while(1): #Retry until download returns
			with PRINT_LOCK:
				log("\033[90m{}: Downloading...\033[0m".format(mediafire_id))
			try:
				if(mediafire_id.startswith("/conv/")): #Conv link
					with ARCHIVE_LOCK:
						if archive_mode:
							output_file = output + mediafire_id
						else:
							output_file = output
						if(os.path.exists(output_file) or (mediafire_id in archive)): #Duplicate
							log("\033[90m{}:\033[0m \033[33mFile already exists. Skipping...\033[0m".format(mediafire_id))
							return (1, None)
						archive.append(mediafire_id)
					if(download_url("https://mediafire.com" + mediafire_id, output_file) == 200): #Success
						log("\033[90m{}: \033[0m\033[96mDownloaded\033[0m".format(mediafire_id))
						return (1, None)
					else:
						log("\033[90m{}: \033[0m\033[31mNot found!\033[0m".format(mediafire_id))
						return (0, None)
				elif(len(mediafire_id) in [11, 15, 31]): #Single file
					with ARCHIVE_LOCK:
						known_path_exists = False
						if only_meta or archive_mode:
							known_path_exists = os.path.exists(output + "/keys/" + mediafire_id + ".info.json")
						if(known_path_exists or (mediafire_id in archive)): #Duplicate
							log("\033[90m{}:\033[0m \033[33mFile already exists. Skipping...\033[0m".format(mediafire_id))
							return (1, None)
						archive.append(mediafire_id)
					return (download_file(mediafire_id, output, only_meta=only_meta, archive_mode=archive_mode), None)
				elif(len(mediafire_id) in [13, 19]): #Folder
					with ARCHIVE_LOCK:
						#Redownload contents even if .info.json file exists (without overwriting it) but skip if another thread already started downloading it
						if(mediafire_id in archive):
							log("\033[90m{}:\033[0m \033[33mFile already exists. Skipping...\033[0m".format(mediafire_id))
							return (1, None)
						archive.append(mediafire_id)
					return download_folder(mediafire_id, output, only_meta=only_meta, archive=archive, archive_mode=archive_mode)
			except Exception:
				with PRINT_LOCK:
					archive.remove(mediafire_id)
					traceback.print_exc()
					log("\033[90m{}: \033[0m\033[31mError while downloading! Retrying in 10s...\033[0m".format(mediafire_id))
				time.sleep(10)

def resolve_custom_folder(name):
	while(1): #Retry if an exception is caught
		log("\033[90mResolving custom folder name:\033[0m \033[96m{}\033[0m".format(name))
		try:
			rq = requests.get("https://mediafire.com/{}".format(name), headers=HTTP_HEADERS, timeout=TIMEOUT_T)
			resolved = CUSTOM_FOLDER_RE.findall(rq.text)
			if(resolved):
				resolved = resolved[0] #First result
				log("\033[96m{}\033[0m \033[90m->\033[0m \033[95m{}\033[0m".format(name, resolved))
				return resolved
			else:
				log("\033[31mUnable to resolve {}\033[0m".format(name))
				return None
		except Exception:
			continue

def worker(args, output, mediafire_id, archive):
	(download_successful, folder_contents) = download(mediafire_id, output, only_meta=args.only_meta, archive=archive, archive_mode=args.archive_mode)
	return (output, folder_contents)

if(__name__ == "__main__"):
	#CLI front end

	parser = argparse.ArgumentParser(description="Mediafire downloader")
	parser.add_argument("--only-meta", action="store_true", help="Only download *.info.json files")
	parser.add_argument("--threads", type=int, default=6, help="How many threads to use; in case mediafire starts showing captchas or smth the amount of threads should be reduced; default is 6")
	parser.add_argument("--archive-mode", "-a", dest="archive_mode", action="store_true", help="Use a flat directory layout and save all file metadata in *.info.json files")
	parser.add_argument("output", help="Output directory")
	parser.add_argument("input", nargs="+", help="Input file/files which will be searched for links")
	args = parser.parse_args()

	#Message to encourage people to submit links to the archive team.
	log("\033[33mIf you have mediafire links you want to archive you can submit them to the Archive Team (https://archiveteam.org/index.php?title=MediaFire). If you don't have any fancy raidz configurations this will be the best way to preserve them.\033[0m")

	#Archive list is a list of files that started downloading. It's used to
	#stop threads from starting to download the same file multiple times.
	#For example if a file is downloading as a single file in one thread and
	#another thread starts downloading it as part of a directory before the first
	#thread finishes
	archive = []

	#Get ids
	mediafire_urls = analyze_mediafire.read_mediafire_links(args.input)

	#Resolve custom names to keys and save names and their keys to custom_folders.txt
	#custom_folders.txt format:
	#"KEY FOLDER_NAME\n"
	custom_folders_fname = args.output + "/custom_folders.txt"
	if(not os.path.exists(custom_folders_fname)): #If doesn't exist create
		with open(custom_folders_fname, 'w'): pass
	custom_folder_lookup = {}
	#Read custom folder lookup table
	with open(custom_folders_fname, "r") as fl:
		for line in fl.read().splitlines():
			lookup_entry = line.split(" ")
			custom_folder_lookup[lookup_entry[1]] = lookup_entry[0]
	#Resolve names
	for custom_folder in mediafire_urls["custom_folders"]:
		if(custom_folder in custom_folder_lookup): #Already in the table
			mediafire_urls["keys"].append(custom_folder_lookup[custom_folder]) #Add resolved key
		else: #Not yet resolved
			resolved = resolve_custom_folder(custom_folder)
			if(resolved):
				with open(custom_folders_fname, "a") as fl:
					fl.write("{} {}\n".format(resolved, custom_folder))
				mediafire_urls["keys"].append(resolved) #Add resolved key
				custom_folder_lookup[custom_folder] = resolved

	#Create download dirs
	if args.archive_mode or args.only_meta:
		os.makedirs(args.output + "/keys", exist_ok=True)
		os.makedirs(args.output + "/conv", exist_ok=True)

	#Download
	archive = []
	task_queue = queue.Queue()

	with multiprocessing.Pool(6) as pool:
		def callback(child_info):
			output_base = child_info[0]
			folder_contents = child_info[1]
			if folder_contents is None:
				return
			if args.archive_mode or args.only_meta:
				# Flat directory structure
				output = args.output
			else:
				# Nest children in this folder
				folder_name = folder_contents[0][0]
				if "/" in folder_name or ".." == folder_name:
					with PRINT_LOCK:
						log("Dangerous name {} for folder with id {}".format(folder_name, folder_contents[0][1]))
					folder_name = folder_contents[0][1]
				output = output_base + "/" + folder_name
			for id in folder_contents[1]:
				task_queue.put(pool.apply_async(worker, args=(args, output, id, archive,), callback=callback))

		for conv_link in mediafire_urls["conv"]: #Download conv links
			task_queue.put(pool.apply_async(worker, args=(args, args.output, conv_link, archive,), callback=callback))

		for mediafire_id in mediafire_urls["keys"]: #Download keys
			task_queue.put(pool.apply_async(worker, args=(args, args.output, mediafire_id, archive,), callback=callback))

		while not task_queue.empty():
			task = task_queue.get()
			task.wait()
			try:
				task.get()
			except Exception as e:
				log(e)


code:
requests
beautifulsoup4
lxml
colorama


code:
--- DIRECTORIES ---
https://mediafire.com/?b021763bj94p3
https://mediafire.com/?a8zve2a51t88p
https://mediafire.com/?giq97bzhycc0y
https://mediafire.com/?n32esvys2qml4
https://mediafire.com/?7722a5cez53bg
https://mediafire.com/?g858wpzxnydln
https://mediafire.com/?fejjpmv5ho1jm
https://mediafire.com/?53qefssy5sbwa
https://mediafire.com/?c5uep2eupfxnx
https://mediafire.com/?6gbtcje5e1ged
https://mediafire.com/?xl354g6562k1x
https://mediafire.com/?9u6a20oy33zjc
https://mediafire.com/?bnzrr3uf4l9tp
https://mediafire.com/?npbp9150sp0lr
https://mediafire.com/?bzxx4we97q395
https://mediafire.com/?9xa7z1anr2o74
https://mediafire.com/?5pcy5q90ndanm
https://mediafire.com/?bcptikgtg84zp
https://mediafire.com/?g129iaziepjdi
https://mediafire.com/?1vmmbet42tdd1
https://mediafire.com/?5d5y3tfygah04
https://mediafire.com/?33skvtgexnfcv
https://mediafire.com/?e3en4sdt406xs
https://mediafire.com/?bh5sa0srgo29y
https://mediafire.com/?2bdy52sesmfp5
https://mediafire.com/?lijzwrnuk0gkx
https://mediafire.com/?4liyay39bs3xb
https://mediafire.com/?abs5ddqldy29e
https://mediafire.com/?sgep4gk13kk1e
https://mediafire.com/?kfocr3qze5wnx
https://mediafire.com/?3hsn0lonsa4l9
https://mediafire.com/?b1jug8ej098eh
https://mediafire.com/?y6g10hhd3844x
https://mediafire.com/?02w9l8l9lyrfy
https://mediafire.com/?xxby5yvniecpu
https://mediafire.com/?lk87i0vc80za3
https://mediafire.com/?48eu8rm2ur7uk
https://mediafire.com/?xggg4tffnu26d
https://mediafire.com/?5jc7w57g7y6qb
https://mediafire.com/?de3e5mgapnyjj
https://mediafire.com/?9t5vqjjxp692i
https://mediafire.com/?qml1gv6ij2ldi
https://mediafire.com/?86pgoimm8rxrp
https://mediafire.com/?5453lkoe1qf91
https://mediafire.com/?eis9b1dahdcw3
https://mediafire.com/?hot3k1t5z6p16
https://mediafire.com/?73nqnt66zf108

path: tests/folders


code:
#Incorrect file
https://mediafire.com/?xxxxxxxxxxxxx

#Incorrect directory
https://mediafire.com/?xxxxxxxxxxxxxxx


code:
#!/bin/env python3
import argparse, threading, queue, ipaddress, os, analyze, requests, time, traceback, re
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from log import log

TIMEOUT_T = 30
HTTP_HEADERS = {
	"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0" #Spoof firefox user agent
}
SCRAPE_CONTENT_TYPE = ["text/plain", "text/html"]
#Don't verify certificates so sites with expired certificates will also work
#This will allow MITM attacks on https sites, but if that's the case then the
#scraper getting incorrect links is the least of your problems
VERIFY_CERTIFICATES=False
requests.packages.urllib3.disable_warnings()

STATUS_NOT_WORKING = 0
STATUS_WORKING = 1
STATUS_ERROR = 2

MAX_RETRIES = 5

def worker(download_queue, output, output_list, archive, print_lock, output_lock, archive_lock, url_filter, url_regex, thread_id, threads_working, threads_working_lock):
	while(1):
		#Get element from the queue
		with threads_working_lock:
			threads_working[thread_id] = STATUS_NOT_WORKING
		url = download_queue.get(block=1)
		with threads_working_lock:
			threads_working[thread_id] = STATUS_WORKING

		#Download page
		retries = MAX_RETRIES
		skip_get = 1 #It's determined later
		while((retries > 0)):
			try:
				#Determine if content type is suitable
				web_rq = requests.head(url, headers=HTTP_HEADERS, timeout=TIMEOUT_T, allow_redirects=True, verify=VERIFY_CERTIFICATES)
				web_headers = web_rq.headers
				for content_type in SCRAPE_CONTENT_TYPE:
					if(content_type in str(web_headers.get("Content-Type"))):
						skip_get = 0
						break
				if(skip_get): #Skip based on content type
					with print_lock:
						log("\033[93mSkipping\033[0m {}".format(url))
				else:
					with print_lock:
						log("\033[95mScraping\033[0m \033[96mstatus={}\033[0m {}".format(web_rq.status_code, url))
					web_html = requests.get(url, headers=HTTP_HEADERS, timeout=TIMEOUT_T, verify=VERIFY_CERTIFICATES).text
					web_html = web_html.replace("<wbr>", "") #Some sites using <wbr> tag break url search
				break
			except Exception:
				with print_lock:
					traceback.print_exc()
					print("\033[31mError while downloading \033[0m{}\033[31m! Retrying in 2 minutes ({} retries remaining)\033[0m".format(url, retries))
				with threads_working_lock:
					threads_working[thread_id] = STATUS_ERROR
				time.sleep(60*2)
				retries-=1
		if(retries == 0):
			with print_lock:
				log("\033[31mMax number of retries exceeded on\033[0m {}\033[31m!\033[0m".format(url))
			continue
		elif(retries<MAX_RETRIES):
			with threads_working_lock:
				threads_working[thread_id] = STATUS_WORKING #Working again
		if(skip_get):
			continue

		#Get urls
		html_urls = analyze.get_urls(web_html) #This will get every absolute URL; this has the advantage of getting all of the urls written in plain text
		#This will also get the absolute URL for every relative path in <a> tags
		soup = BeautifulSoup(web_html, "lxml")
		for link in soup.find_all("a"):
			html_urls.append(urljoin(url, link.get('href')))

		for html_url in html_urls:
			html_url_no_schema = analyze.truncate_schema(html_url)
			try: #Skip private ip addresses
				if(ipaddress.ip_address(html_url_no_schema).is_private):
					with print_lock:
						log("\033[31mSkipping local address\033[0m {}".format(html_url_no_schema))
					continue
			except ValueError:
				pass

			if(("mediafire.com" in html_url) or ("mfi.re" in html_url)): #If mediafire
				with output_lock:
						already_downloaded = 0
						if(html_url_no_schema in output_list):
							already_downloaded = 1
						if(not already_downloaded):
							with open(output, "a") as fl:
								fl.write(html_url)
								fl.write("\n")
							output_list.append(html_url_no_schema)
							with print_lock:
								log("\033[92mFound\033[0m {}".format(html_url))
			else: #Recurse into every site which isn't mediafire
				if(url_filter):
					if(not url_filter in html_url): #Filter url
						continue
				elif(url_regex):
					if(not url_regex.match(html_url)):
						continue
				with archive_lock:
						already_downloaded = 0
						if(html_url_no_schema in archive):
							already_downloaded = 1
						if(not already_downloaded):
							download_queue.put(html_url)
							archive.append(html_url_no_schema)

if(__name__ == "__main__"):
	#CLI front end
	parser = argparse.ArgumentParser(description="Mediafire link web scraper")
	parser.add_argument("--threads", type=int, default=6, help="How many threads to use; if the site you're trying to crawl will start displaying captchas or smth the amount of threads should be reduced; default is 6")
	parser_filters = parser.add_mutually_exclusive_group()
	parser_filters.add_argument("--filter", default="", help="Only scrape websites where filter is found in the url")
	parser_filters.add_argument("--regex", default="", help="Same as filter but uses regex")
	parser.add_argument("start", help="Start URL from which the scraper will begin to recursively scrape pages")
	parser.add_argument("output", help="File where a list of links will be saved")

	args = parser.parse_args()

	#If archive or output file doesn't exist create it
	if(not os.path.exists(args.output)):
		with open(args.output, 'w'): pass

	#Start workers
	output_list = []
	archive = []
	worker_list = []
	print_lock = threading.Lock()
	output_lock = threading.Lock()
	archive_lock = threading.Lock() #Should also be locked while reading to avoid race conditions
	threads_working_lock = threading.Lock()
	download_queue = queue.Queue()
	download_queue.put(args.start)
	threads_working = [0]*args.threads #Keep track of which threads are working
	for i in range(args.threads):
		current_worker = threading.Thread(target=worker,
		                                  daemon=True,
		                                  args=(download_queue,
		                                        args.output,
		                                        output_list,
		                                        archive,
		                                        print_lock,
		                                        output_lock,
		                                        archive_lock,
		                                        args.filter,
		                                        re.compile(args.regex),
		                                        i,
		                                        threads_working,
		                                        threads_working_lock,))
		current_worker.name = i
		current_worker.start()
		worker_list.append(current_worker)
	#Monitor threads
	#In this case waiting for threads to exit wouldn't be possible because if
	#a thread has nothing to do it must wait for pages to scrape. If a thread
	#would exit immediately it wouldn't wait for the first thread to fetch URLs
	#from the starting page and as a result only a single thread would remain.
	any_thread_alive = 1
	while(any_thread_alive):
		time.sleep(15)

		with threads_working_lock:
			threads_working_copy = threads_working.copy() #Copy thread status
		#Check if any thread is alive
		any_thread_alive = 0
		for i in range(args.threads):
			if(threads_working_copy[i]):
				any_thread_alive = 1
				break

		#Show thread status
		for i in range(args.threads):
			thread_color = ""
			if(threads_working_copy[i] == 0): thread_color = "\033[31m"
			elif(threads_working_copy[i] == 1): thread_color = "\033[32m"
			elif(threads_working_copy[i] == 2): thread_color = "\033[33m"
			threads_working_copy[i] = thread_color + str(i) + "\033[0m"
		with print_lock:
			log("\033[90mThread status:\033[0m " + "".join(threads_working_copy))


code:
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs     diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln       merge=binary
#*.csproj    merge=binary
#*.vbproj    merge=binary
#*.vcxproj   merge=binary
#*.vcproj    merge=binary
#*.dbproj    merge=binary
#*.fsproj    merge=binary
#*.lsproj    merge=binary
#*.wixproj   merge=binary
#*.modelproj merge=binary
#*.sqlproj   merge=binary
#*.wwaproj   merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg   binary
#*.png   binary
#*.gif   binary

###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc   diff=astextplain
#*.DOC   diff=astextplain
#*.docx  diff=astextplain
#*.DOCX  diff=astextplain
#*.dot   diff=astextplain
#*.DOT   diff=astextplain
#*.pdf   diff=astextplain
#*.PDF   diff=astextplain
#*.rtf   diff=astextplain
#*.RTF   diff=astextplain

repo_name: wonwoojin/rimworld

path: .gitattributes


code:
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb
/Source/Patches/PawnAnimationPatches/HarmonyPatch_Pawn_DrawTracker.cs
/Source/Patches/PawnAnimationPatches/HarmonyPatch_PawnRotation.cs
/Source/Patches/PawnAnimationPatches/HarmonyPatch_PawnRenderer.cs
/Source/Patches/OtherModPatches/HarmonyPatch_ShowHairWithHats.cs
/Source/Patches/OtherModPatches/HarmonyPatch_FacialAnimation.cs
/Source/Patches/OtherModPatches/HarmonyPatch_DontShaveYourHead.cs
/Source/Patches/OtherModPatches/HarmonyPatch_CSL.cs
/Source/Patches/OtherModPatches/HarmonyPatch_AlienRace.cs
/Source/Patches/ThingAnimationPatches/HarmonyPatch_ThingDrawAt.cs
/Defs/AnimationDefs/Animations_SexToys.xml
/1.5/Defs/AnimationDefs/BasicBestiality/HumanRotatedOffset.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality4v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality3v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality2v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality1v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/!BasicBestiality.xml
/1.5/Defs/TestDoNotPush
/1.5/Textures/AnimationProps/BEV

path: .gitignore