Compare commits

...

6 commits

Author SHA1 Message Date
Talos
2af60a8c33 Merge branch 'master' into 'master'
Edit AnimationPatch_PawnRenderTree_OffsetSubWorker.xml

See merge request c0ffeeeeeeee/rimworld-animations!38
2026-10-12 08:13:40 +08:00
c0ffeeeeeeee
56867bab69 updated to version 2.2.1
fixes to necro anims, voices, anim loading, lots of cool stuff by Rob The Robot Guard
Simplified Chinese by LVKavidya
2026-10-11 21:03:28 +00:00
c0ffeeeeeeee
52feae9ba6 Merge branch 'review-fixes' into 'master'
Fix necrophilia animations and other general issues

See merge request c0ffeeeeeeee/rimworld-animations!41
2026-10-11 21:00:22 +00:00
c0ffeeeeeeee
8c9df3c0da Merge branch 'translation-zh' into 'master'
Update Simplified Chinese translation

See merge request c0ffeeeeeeee/rimworld-animations!40
2026-10-11 20:59:03 +00:00
Rob The Robot Guard
21ef8d24e0 Fix necrophilia animations and issues from code review
Necrophilia:
- Anchor the living pawn to the corpse instead of its inner pawn, whose
  position is stale once the corpse is hauled
- Corpses only TickRare, so a living participant's animator now ticks dead
  actors' animators; multi-stage animations no longer desync
- Save driven corpses with saveDestroyedThings (dead pawns count as
  destroyed and would otherwise load as null)
- Reapply the renderer animation mid-way after loading, since vanilla
  doesn't save it

Review fixes:
- Priority-0 fallback contexts can now be selected; negative priorities
  no longer log "Negative weight" errors; drop no-op OrderBy
- Remove dead HAR and SexToys entries from the v1.6 LoadFolders block
- Neverending sex: retry about once a second instead of every tick, guard
  null participants, reset RJW timers, handle corpses
- Ignore anchors that were destroyed or despawned
- Use pivot (not scale) for the inverse pivot translation in TryGetMatrix
- Offset tab Reset removes the saved override and restores def offsets
- Merge root/child sound handling; respect playSounds for the root node;
  read child voices from the child node; no voices from corpses
- Force-save settings whose load defaults differ from field defaults
- Debug-only interaction logging; null checks for partner, offsetDefs,
  whitelist and short blacklists
- Remove empty or no-op Harmony patch classes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:01:46 +01:00
LVKavidya
aacf38c8a2 Update Simplified Chinese translation 2026-07-02 12:52:15 +08:00
23 changed files with 310 additions and 297 deletions

View file

@ -91,7 +91,7 @@ namespace Rimworld_Animations
public List<Pawn> FindAnyWorkingSet(List<Pawn> actors)
{
if (actors.Count != whitelist.Count) return null;
if (whitelist.NullOrEmpty() || actors.Count != whitelist.Count) return null;
int numActors = actors.Count;
@ -170,7 +170,7 @@ namespace Rimworld_Animations
return false;
}
if (!blacklist.NullOrEmpty() && blacklist[index].PawnTest(actor))
if (!blacklist.NullOrEmpty() && index < blacklist.Count && blacklist[index] != null && blacklist[index].PawnTest(actor))
{
return false;
}

View file

@ -167,7 +167,9 @@ namespace Rimworld_Animations
string bodyTypeDef = (pawn.story?.bodyType != null) ? pawn.story.bodyType.ToString() : "";
if (offsetDefs[actor].FindOffset(pawn, out BaseAnimationOffset animationOffset))
//defs without offsetDefs (or fewer than numActors) just get no offset
if (!offsetDefs.NullOrEmpty() && actor < offsetDefs.Count && offsetDefs[actor] != null
&& offsetDefs[actor].FindOffset(pawn, out BaseAnimationOffset animationOffset))
{
position = animationOffset.getOffset(pawn);
rotation = animationOffset.getRotation(pawn);

View file

@ -26,6 +26,12 @@ namespace Rimworld_Animations {
private VoiceDef voice;
private VoiceTagDef lastVoiceTag;
//dead actors (corpses) in this pawn's animation; corpses only TickRare, so this comp ticks their animators for them
private List<Pawn> drivenPawns;
//the renderer's current animation isn't saved by vanilla; reapply it on the first tick after loading
private bool restoreAnimationAfterLoad;
private bool isAnimating = false;
@ -41,7 +47,7 @@ namespace Rimworld_Animations {
{
get
{
return anchor != null;
return anchor != null && anchor.IsValid;
}
}
@ -135,6 +141,42 @@ namespace Rimworld_Animations {
public override void CompTick()
{
AnimatorTick();
if (!drivenPawns.NullOrEmpty())
{
foreach (Pawn drivenPawn in drivenPawns.ToList())
{
CompExtendedAnimator drivenAnimator = drivenPawn?.TryGetComp<CompExtendedAnimator>();
//stop driving if the corpse finished, was destroyed or got resurrected (it ticks itself again)
if (drivenAnimator == null || !drivenAnimator.IsAnimating || !drivenPawn.Dead)
{
drivenPawns.Remove(drivenPawn);
continue;
}
drivenAnimator.AnimatorTick();
}
}
base.CompTick();
}
public void AddDrivenPawn(Pawn deadPawn)
{
if (drivenPawns == null) drivenPawns = new List<Pawn>();
drivenPawns.AddDistinct(deadPawn);
}
public void AnimatorTick()
{
if (restoreAnimationAfterLoad)
{
restoreAnimationAfterLoad = false;
RestoreAnimationAfterLoad();
}
if (isAnimating)
{
animationTicks++;
@ -157,10 +199,6 @@ namespace Rimworld_Animations {
CheckAndPlaySounds();
}
base.CompTick();
}
//returns false if still more animations
@ -186,8 +224,33 @@ namespace Rimworld_Animations {
}
}
private void RestoreAnimationAfterLoad()
{
if (!isAnimating || animationQueue.NullOrEmpty()) return;
PawnRenderer renderer = pawn.Drawer?.renderer;
if (renderer?.renderTree == null) return;
renderer.SetAnimation(animationQueue[0]);
//resume partway through the current animation instead of from its first frame
renderer.renderTree.animationStartTick = GenTicks.TicksGame - animationTicks;
renderer.SetAllGraphicsDirty();
}
public void StopAnimating()
{
//stop any corpses this pawn was animating, since they can't stop themselves
if (!drivenPawns.NullOrEmpty())
{
List<Pawn> pawnsToStop = drivenPawns;
drivenPawns = null;
foreach (Pawn drivenPawn in pawnsToStop)
{
drivenPawn?.TryGetComp<CompExtendedAnimator>()?.StopAnimating();
}
}
isAnimating = false;
animationQueue = null;
anchor = null;
@ -203,6 +266,7 @@ namespace Rimworld_Animations {
this.actorIndex = actorIndex;
this.currentGroupAnimation = groupAnimationDef;
drivenPawns = null;
ticksSinceLastVoicePlayed = GenTicks.TicksGame;
ticksUntilNextVoice = 0;
@ -236,9 +300,23 @@ namespace Rimworld_Animations {
Scribe_Collections.Look<AnimationDef>(ref animationQueue, "animations_queue");
Scribe_Defs.Look<GroupAnimationDef>(ref currentGroupAnimation, "animations_groupAnimationDef");
Scribe_Deep.Look<BaseExtendedAnimatorAnchor>(ref this.anchor, "animations_anchor");
//dead pawns count as destroyed, so their references must be saved with saveDestroyedThings or they load as null
Scribe_Collections.Look<Pawn>(ref this.drivenPawns, "animations_drivenPawns", true, LookMode.Reference);
Scribe_Values.Look<Vector3>(ref this.offset, "animations_offset", Vector3.zero);
Scribe_Values.Look<int>(ref this.rotation, "animations_rotationOffset", 0);
Scribe_Defs.Look<VoiceDef>(ref this.voice, "animations_voice");
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
drivenPawns?.RemoveAll(x => x == null);
if (isAnimating)
{
//animation defs can go missing between saves (e.g. an anim pack was removed)
if (animationQueue.NullOrEmpty()) isAnimating = false;
else restoreAnimationAfterLoad = true;
}
}
}
@ -311,123 +389,79 @@ namespace Rimworld_Animations {
public void CheckAndPlaySounds()
{
//use held position/map so it also works for pawns inside corpses
Map map = pawn.MapHeld;
if (map == null) return;
PawnRenderNode rootNode = pawn.Drawer?.renderer?.renderTree?.rootNode;
//check if the rootnode has sounds; if so play it
if (rootNode?.AnimationWorker is AnimationWorker_KeyframesExtended animWorker)
//check the rootnode for sounds
if (rootNode != null)
{
SoundDef sound = animWorker.soundAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode);
PlaySoundsForNode(rootNode, map);
}
//do the same for all the child nodes
if (rootNode?.children != null)
{
foreach (PawnRenderNode node in rootNode.children)
{
if (node != null) PlaySoundsForNode(node, map);
}
}
}
private void PlaySoundsForNode(PawnRenderNode node, Map map)
{
if (!(node.AnimationWorker is AnimationWorker_KeyframesExtended animWorker)) return;
if (RJWAnimationSettings.playSounds)
{
SoundDef sound = animWorker.soundAtTick(node.tree.AnimationTick, node.tree.currentAnimation, node);
if (sound != null)
{
SoundInfo soundInfo = new TargetInfo(pawn.Position, pawn.Map);
SoundInfo soundInfo = new TargetInfo(pawn.PositionHeld, map);
//temp; does not consider non-rjw animations
//todo: replace with value stored in comp or somewhere else?
soundInfo.volumeFactor = RJWAnimationSettings.soundVolume;
sound.PlayOneShot(soundInfo);
}
if (RJWAnimationSettings.playVoices)
{
SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map);
voiceInfo.volumeFactor = RJWAnimationSettings.voicesVolume;
//play voice sounds
VoiceTagDef voiceTag = animWorker.voiceAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode);
if (voiceTag != null
&& (lastVoiceTag != voiceTag || ticksSinceLastVoicePlayed + ticksUntilNextVoice < GenTicks.TicksGame)) //play a new voice tag, or wait until time elapsed
{
lastVoiceTag = voiceTag;
ticksSinceLastVoicePlayed = GenTicks.TicksGame;
if (voice != null && voice.sounds.ContainsKey(voiceTag))
{
ticksUntilNextVoice = voice.ticksBetweenPlays.RandomInRange;
voice.sounds[voiceTag].PlayOneShot(voiceInfo);
}
else if (pawn.RaceProps.Humanlike && RJWAnimationSettings.playHumanlikeVoicesAsDefault)
{
//play default voice
VoiceDef pawnDefaultVoice = (pawn.gender == Gender.Male ? VoiceDefOf.Voice_HumanMale : VoiceDefOf.Voice_HumanFemale);
ticksUntilNextVoice = pawnDefaultVoice.ticksBetweenPlays.RandomInRange;
if (pawnDefaultVoice.sounds.ContainsKey(voiceTag))
{
ticksUntilNextVoice = pawnDefaultVoice.ticksBetweenPlays.RandomInRange;
pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo);
}
}
}
}
}
//check rootnodes and children
if (rootNode?.children != null)
//corpses don't talk
if (!RJWAnimationSettings.playVoices || pawn.Dead) return;
//play voice sounds
VoiceTagDef voiceTag = animWorker.voiceAtTick(node.tree.AnimationTick, node.tree.currentAnimation, node);
if (voiceTag == null
|| (lastVoiceTag == voiceTag && ticksSinceLastVoicePlayed + ticksUntilNextVoice >= GenTicks.TicksGame)) //play a new voice tag, or wait until time elapsed
{
foreach (PawnRenderNode node in rootNode?.children)
return;
}
lastVoiceTag = voiceTag;
ticksSinceLastVoicePlayed = GenTicks.TicksGame;
SoundInfo voiceInfo = new TargetInfo(pawn.PositionHeld, map);
voiceInfo.volumeFactor = RJWAnimationSettings.voicesVolume;
if (voice != null && voice.sounds.ContainsKey(voiceTag))
{
ticksUntilNextVoice = voice.ticksBetweenPlays.RandomInRange;
voice.sounds[voiceTag].PlayOneShot(voiceInfo);
}
else if (pawn.RaceProps.Humanlike && RJWAnimationSettings.playHumanlikeVoicesAsDefault)
{
//play default voice
VoiceDef pawnDefaultVoice = (pawn.gender == Gender.Male ? VoiceDefOf.Voice_HumanMale : VoiceDefOf.Voice_HumanFemale);
if (pawnDefaultVoice.sounds.ContainsKey(voiceTag))
{
if (node?.AnimationWorker is AnimationWorker_KeyframesExtended childrenAnimWorker)
{
if (RJWAnimationSettings.playSounds)
{
SoundDef sound = childrenAnimWorker.soundAtTick(node.tree.AnimationTick, node.tree.currentAnimation, node);
if (sound != null)
{
SoundInfo soundInfo = new TargetInfo(pawn.Position, pawn.Map);
soundInfo.volumeFactor = RJWAnimationSettings.soundVolume;
sound.PlayOneShot(soundInfo);
}
}
if (RJWAnimationSettings.playVoices)
{
SoundInfo voiceInfo = new TargetInfo(pawn.Position, pawn.Map);
voiceInfo.volumeFactor = RJWAnimationSettings.voicesVolume;
//play voice sounds
VoiceTagDef voiceTag = childrenAnimWorker.voiceAtTick(rootNode.tree.AnimationTick, rootNode.tree.currentAnimation, rootNode);
if (voiceTag != null
&& (lastVoiceTag != voiceTag || ticksSinceLastVoicePlayed + ticksUntilNextVoice < GenTicks.TicksGame)) //play a new voice tag, or wait until time elapsed
{
lastVoiceTag = voiceTag;
ticksSinceLastVoicePlayed = GenTicks.TicksGame;
if (voice != null && voice.sounds.ContainsKey(voiceTag))
{
ticksUntilNextVoice = voice.ticksBetweenPlays.RandomInRange;
voice.sounds[voiceTag].PlayOneShot(voiceInfo);
}
else if (pawn.RaceProps.Humanlike && RJWAnimationSettings.playHumanlikeVoicesAsDefault)
{
VoiceDef pawnDefaultVoice = (pawn.gender == Gender.Male ? VoiceDefOf.Voice_HumanMale : VoiceDefOf.Voice_HumanFemale);
if (pawnDefaultVoice.sounds.ContainsKey(voiceTag))
{
ticksUntilNextVoice = pawnDefaultVoice.ticksBetweenPlays.RandomInRange;
pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo);
}
}
}
}
}
ticksUntilNextVoice = pawnDefaultVoice.ticksBetweenPlays.RandomInRange;
pawnDefaultVoice.sounds[voiceTag].PlayOneShot(voiceInfo);
}
}
//do the same for all the child nodes
}

View file

@ -15,6 +15,9 @@ namespace Rimworld_Animations
public virtual void ExposeData() { }
public abstract Vector3 getDrawPos();
//false if the anchor can't be drawn at anymore (e.g. the thing was destroyed)
public virtual bool IsValid => true;
public string GetUniqueLoadID()
{
throw new NotImplementedException();

View file

@ -26,6 +26,9 @@ namespace Rimworld_Animations
this.thing = thing;
}
//spawned check also covers destroyed things, and things that didn't come back after loading
public override bool IsValid => thing != null && thing.Spawned;
public override Vector3 getDrawPos()
{
//x and z position, regular altitude for pawns

View file

@ -58,13 +58,20 @@ namespace Rimworld_Animations {
if (listingStandard.ButtonText("RimAnims_Reset".Translate()))
{
if (!RJWAnimationSettings.offsets.NullOrEmpty()
&& RJWAnimationSettings.offsets.ContainsKey(AnimationUtility.OffsetLookupKey(curPawn)))
{
RJWAnimationSettings.offsets[AnimationUtility.OffsetLookupKey(curPawn)].Reset();
}
posX = posY = posZ = rot = 0;
//remove the saved override entirely; an entry left behind would keep overriding the def's offset with zeros
RJWAnimationSettings.offsets?.Remove(AnimationUtility.OffsetLookupKey(curPawn));
//go back to the offset from the animation's offsetDefs
extendedAnimator.CurrentGroupAnimation.GetOffset(extendedAnimator.ActorIndex, curPawn, out Vector3? defOffset, out int? defRotation);
extendedAnimator.Offset = defOffset;
extendedAnimator.Rotation = defRotation;
Vector3 resetOffset = defOffset ?? Vector3.zero;
posX = resetOffset.x;
posY = resetOffset.y;
posZ = resetOffset.z;
rot = defRotation ?? 0;
}
listingStandard.GapLine();

View file

@ -1,55 +0,0 @@
using HarmonyLib;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse;
using Verse.AI;
namespace Rimworld_Animations
{
[HarmonyPatch(typeof(JobDriver_Masturbate), "SetupDurationTicks")]
public class HarmonyPatch_JobDriver_Masturbate
{
public static void Postfix(JobDriver_Masturbate __instance)
{
//prevent early stoppage of masturbate jobdriver during animation
/* not needed anymore? Ticks are assigned during animation
__instance.duration = 10000000;
__instance.ticks_left = __instance.duration;
*/
}
}
[HarmonyPatch(typeof(JobDriver_Masturbate), "MakeNewToils")]
public class HarmonyPatch_JobDriver_Masturbate2
{
public static void Postfix(JobDriver_Masturbate __instance, ref IEnumerable<Toil> __result)
{
var toils = __result.ToList();
//sex toil
toils[1].initAction += delegate ()
{
CompExtendedAnimator pawnAnimator = __instance.pawn.TryGetComp<CompExtendedAnimator>();
// if pawn was given an animation to play,
if (pawnAnimator.IsAnimating)
{
//set duration of masturbate toil to anim length
toils[1].defaultDuration = pawnAnimator.AnimationLength;
}
};
__result = toils.AsEnumerable();
}
}
}

View file

@ -30,36 +30,57 @@ namespace Rimworld_Animations
public static void Postfix(JobDriver_Sex __instance, Pawn pawn, Thing target)
{
//if neverending sex and pawn doesn't have an animation,
if (__instance.neverendingsex
&& !pawn.TryGetComp<CompExtendedAnimator>().IsAnimating)
//only retry about once a second; searching every animation def every tick is expensive when none fit
if (!__instance.neverendingsex
|| !pawn.IsHashIntervalTick(60)
|| !(pawn.TryGetComp<CompExtendedAnimator>() is CompExtendedAnimator animator)
|| animator.IsAnimating)
{
//start a new animation for all the pawns paired with receiver job driver
List<Pawn> participants;
if (target is Pawn receiverPawn)
{
participants = (receiverPawn?.jobs?.curDriver as JobDriver_SexBaseReciever)?.parteners;
if (!participants.NullOrEmpty() && !participants.Contains(receiverPawn))
{
participants = participants.Append(receiverPawn).ToList();
}
}
else
{
participants = new List<Pawn> { pawn };
}
GroupAnimationDef animation = AnimationUtility.FindGroupAnimation(participants, out List<Pawn> participantsOrdered);
if (animation != null)
{
Thing anchor = (Thing)__instance.Bed ?? pawn;
AnimationUtility.StartGroupAnimation(participantsOrdered, animation, anchor);
}
return;
}
//start a new animation for all the pawns paired with receiver job driver
List<Pawn> participants;
Thing anchor = __instance.Bed;
if (target is Pawn receiverPawn)
{
participants = (receiverPawn.jobs?.curDriver as JobDriver_SexBaseReciever)?.parteners?.ToList();
if (participants.NullOrEmpty()) return;
if (!participants.Contains(receiverPawn))
{
participants.Add(receiverPawn);
}
anchor = anchor ?? receiverPawn;
}
else if (target is Corpse corpse && corpse.InnerPawn != null)
{
participants = new List<Pawn> { pawn, corpse.InnerPawn };
anchor = anchor ?? corpse;
}
else
{
participants = new List<Pawn> { pawn };
anchor = anchor ?? pawn;
}
GroupAnimationDef animation = AnimationUtility.FindGroupAnimation(participants, out List<Pawn> participantsOrdered);
if (animation == null) return;
AnimationUtility.StartGroupAnimation(participantsOrdered, animation, anchor);
//keep rjw's timers in step with the new animation, same as when sex starts
int animTicks = AnimationUtility.GetAnimationLength(pawn);
foreach (Pawn participant in participants)
{
if (participant?.jobs?.curDriver is JobDriver_Sex participantJobDriver)
{
participantJobDriver.ticks_left = animTicks;
participantJobDriver.sex_ticks = animTicks;
participantJobDriver.orgasmStartTick = animTicks;
participantJobDriver.duration = animTicks;
}
}
}
}

View file

@ -24,6 +24,10 @@ namespace Rimworld_Animations
{
participants = new List<Pawn>() { pawn };
}
else if (partner == null)
{
return;
}
else if (__instance is JobDriver_ViolateCorpse)
{
participants = new List<Pawn>() { pawn, partner };
@ -52,18 +56,23 @@ namespace Rimworld_Animations
if (groupAnimation != null)
{
Thing anchor = (Thing)__instance.Bed ?? partner;
//anchor to the corpse itself, not its InnerPawn; a pawn inside a corpse keeps the position it died at
Thing anchor = (Thing)__instance.Bed ?? (Thing)(__instance.Target as Corpse) ?? partner;
AnimationUtility.StartGroupAnimation(participantsOrdered, groupAnimation, anchor);
int animTicks = AnimationUtility.GetAnimationLength(pawn);
Log.Message("InteractionDef: " + __instance.Sexprops.dictionaryKey);
if (RJWAnimationSettings.debugMode)
{
Log.Message("InteractionDef: " + __instance.Sexprops?.dictionaryKey);
}
foreach (Pawn participant in participants)
{
if (RJWAnimationSettings.debugMode)
{
Log.Message("Participant: " + participant.Name);
Log.Message("JobDriver: " + participant.CurJobDef.defName);
Log.Message("Participant: " + participant?.Name);
Log.Message("JobDriver: " + participant?.CurJobDef?.defName);
}
//null ref check for pawns that might have lost their jobs or become null for some reason

View file

@ -1,29 +0,0 @@
using HarmonyLib;
using rjw;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Verse.AI;
namespace Rimworld_Animations
{
[HarmonyPatch(typeof(JobDriver_SexBaseRecieverRaped), "MakeNewToils")]
public class HarmonyPatch_JobDriver_SexBaseReceiverRaped
{
/*
* Doesn't work; ienumerables are read-only, can't modify toil
* would need to harmonypatch; stopped partner animating in sexbaseinitiator instead
*
public static void Postfix(JobDriver_SexBaseRecieverRaped __instance, ref IEnumerable<Toil> __result)
{
//added for sudden end of jobdriver
__result.Last().AddFinishAction(delegate {
AnimationUtility.StopGroupAnimation(__instance.pawn);
});
}
*/
}
}

View file

@ -57,7 +57,7 @@ namespace Rimworld_Animations
if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(pivot);
if (quaternion != Quaternion.identity) matrix *= Matrix4x4.Rotate(quaternion);
if (scale != Vector3.one) matrix *= Matrix4x4.Scale(scale);
if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(scale).inverse;
if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(pivot).inverse;
float num = node.Worker.AltitudeFor(node, parms);
if (num != 0f)
@ -96,7 +96,7 @@ namespace Rimworld_Animations
if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(pivot);
if (canRotate && rotation != Quaternion.identity) matrix *= Matrix4x4.Rotate(rotation);
if (canScale && scale != Vector3.one) matrix *= Matrix4x4.Scale(scale);
if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(scale).inverse;
if (pivot != Vector3.zero) matrix *= Matrix4x4.Translate(pivot).inverse;
}
float num = node.Worker.AltitudeFor(node, parms);

View file

@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HarmonyLib;
using RimWorld;
using Verse;
namespace Rimworld_Animations
{
[HarmonyPatch(typeof(Thing), "DrawAt")]
public static class HarmonyPatch_Thing
{
/*
public static bool Prefix(Thing __instance)
{
CompThingAnimator thingAnimator = __instance.TryGetComp<CompThingAnimator>();
if (thingAnimator != null && thingAnimator.isAnimating)
{
thingAnimator.AnimateThing(__instance);
return false;
}
return true;
}
*/
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -46,7 +46,7 @@ public class PawnScaledOffsets : IExposable
{
smallestSizeOffset = largestSizeOffset = Vector3.zero;
smallestSizeRotation = largestSizeRotation = 0;
smallestSize = largestSize = 1;
smallestSize = largestSize = -1; //back to uninitialized
}
public void GetOffsets(Pawn pawn, out Vector3 offset, out int rotation)

View file

@ -27,21 +27,25 @@ namespace Rimworld_Animations {
{
base.ExposeData();
//some load defaults below differ from the field defaults. keys equal to their load default were never written,
//so a missing key in an existing file usually means the player chose that value; keep those load defaults
//and force-save these keys so the file is always explicit from now on
Scribe_Values.Look(ref playVoices, "RJWAnimations_PlayVoices", true);
Scribe_Values.Look(ref playSounds, "RJWAnimations_PlaySounds", true);
Scribe_Values.Look(ref playHumanlikeVoicesAsDefault, "RJWAnimations-playHumanlikeVoicesAsDefault", true);
Scribe_Values.Look(ref maleSounds, "RJWAnimations-maleSounds", true);
Scribe_Values.Look(ref femaleSounds, "RJWAnimations-femaleSounds", true);
Scribe_Values.Look(ref maleSounds, "RJWAnimations-maleSounds", true, true);
Scribe_Values.Look(ref femaleSounds, "RJWAnimations-femaleSounds", true, true);
Scribe_Values.Look(ref debugMode, "RJWAnimations-AnimsDebugMode", false);
Scribe_Values.Look(ref offsetTab, "RJWAnimations-EnableOffsetTab", false);
Scribe_Values.Look(ref hearts, "RJWAnimation-heartsOnLovin");
Scribe_Values.Look(ref offsetTab, "RJWAnimations-EnableOffsetTab", false, true);
Scribe_Values.Look(ref hearts, "RJWAnimation-heartsOnLovin", false, true);
Scribe_Values.Look(ref soundOverride, "RJWAnimations-rjwAnimSoundOverride", true);
Scribe_Values.Look(ref maleAnalCanBeFucked, "RJWAnimations-maleAnalCanBeFucked", true);
Scribe_Values.Look(ref floatRangeInRenderTreeMenu, "RJWAnimations-FloatRangeRenderMenu", 1f);
Scribe_Values.Look(ref soundVolume, "RJWAnimations-soundVolume", 1f);
Scribe_Values.Look(ref voicesVolume, "RJWAnimations-voicesVolume", 1f);
Scribe_Values.Look(ref voicesFrequencyTicks, "RJWAnimations-voicesFrequencyTicks", 1f);
Scribe_Values.Look(ref voicesFrequencyTicks, "RJWAnimations-voicesFrequencyTicks", 1f, true);
Scribe_Collections.Look(ref offsets, "RJWAnimations-animationScaledOffsets", LookMode.Value, LookMode.Deep);

View file

@ -27,7 +27,9 @@ namespace Rimworld_Animations {
for (int i = 0; i < participants.Count; i++)
{
if (anchor is Pawn pawn && pawn == participants[i])
//don't anchor a pawn to itself, or a dead pawn to its own corpse
if ((anchor is Pawn pawn && pawn == participants[i])
|| (anchor is Corpse corpse && corpse.InnerPawn == participants[i]))
{
participants[i].TryGetComp<CompExtendedAnimator>().PlayGroupAnimation(groupAnimationDef, null, i, seed, participants);
@ -38,6 +40,27 @@ namespace Rimworld_Animations {
participants[i].TryGetComp<CompExtendedAnimator>().PlayGroupAnimation(groupAnimationDef, animatorAnchor, i, seed, participants);
}
}
DriveDeadParticipants(participants);
}
//corpses only TickRare, so their animators never advance on their own;
//hand them to a living participant's animator to tick
private static void DriveDeadParticipants(List<Pawn> participants)
{
Pawn driver = participants.FirstOrDefault(x => !x.Dead);
if (driver == null) return;
CompExtendedAnimator driverAnimator = driver.TryGetComp<CompExtendedAnimator>();
if (driverAnimator == null) return;
foreach (Pawn participant in participants)
{
if (participant.Dead)
{
driverAnimator.AddDrivenPawn(participant);
}
}
}
//startgroupanimation without anchor; just play where standing
@ -49,6 +72,8 @@ namespace Rimworld_Animations {
{
participants[i].TryGetComp<CompExtendedAnimator>().PlayGroupAnimation(groupAnimationDef, i, seed, participants);
}
DriveDeadParticipants(participants);
}
@ -67,8 +92,9 @@ namespace Rimworld_Animations {
public static GroupAnimationDef FindGroupAnimation(List<Pawn> participants, out List<Pawn> pawnOrder)
{
pawnOrder = null;
if (participants.NullOrEmpty() || participants.Contains(null)) return null;
/* This method is fooken expensive
List<(GroupAnimationDef, List<Pawn>, int Priority)> potentialAnimations = new List<(GroupAnimationDef, List<Pawn>, int)>();
@ -108,13 +134,17 @@ namespace Rimworld_Animations {
}
//Randomize the list,
potentialAnimations.OrderBy(_ => Rand.Int);
(GroupAnimationDef, List<Pawn>, int) selectedAnimation;
potentialAnimations.TryRandomElementByWeight(((GroupAnimationDef, List<Pawn>, int) x) => x.Item3, out selectedAnimation);
//Find a highest priority animation, out as selectedAnimation
// .TryMaxBy(((GroupAnimationDef, List<Pawn>, int) x) => x.Item3, out (GroupAnimationDef, List<Pawn>, int) selectedAnimation);
//pick randomly, weighted by priority
//negative priorities count as 0 so they don't log "Negative weight" errors
if (!potentialAnimations.TryRandomElementByWeight(((GroupAnimationDef, List<Pawn>, int) x) => Math.Max(x.Item3, 0), out selectedAnimation)
&& potentialAnimations.Count > 0)
{
//every candidate has priority <= 0 (weak/fallback contexts); pick evenly between the best of them
int bestPriority = potentialAnimations.Max(x => x.Item3);
potentialAnimations.Where(x => x.Item3 == bestPriority).TryRandomElement(out selectedAnimation);
}
//set pawnOrder as selected animation permutation
pawnOrder = selectedAnimation.Item2;

View file

@ -8,7 +8,7 @@
<li>1.6</li>
</supportedVersions>
<packageId>c0ffee.rimworld.animations</packageId>
<modVersion>2.2.0</modVersion>
<modVersion>2.2.1</modVersion>
<modIconPath>UI/MainTab</modIconPath>
<modDependencies>
<li>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<LanguageData>
<OffsetManager.label>动画管理器</OffsetManager.label>
<OffsetManager.description>编辑角色动画的偏移量。</OffsetManager.description>
</LanguageData>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<LanguageData>
<AnimProp_Cowgirl_Xray.animPropProperties.debugLabel>女牛仔式-透视</AnimProp_Cowgirl_Xray.animPropProperties.debugLabel>
</LanguageData>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<LanguageData>
<AnimationProp_Xray_Inside.animPropProperties.debugLabel>体内透视</AnimationProp_Xray_Inside.animPropProperties.debugLabel>
</LanguageData>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<LanguageData>
<AnimationProp_Penis.animPropProperties.debugLabel>阴茎透视</AnimationProp_Penis.animPropProperties.debugLabel>
</LanguageData>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<LanguageData>
<AnimationProp_Vagina.alternateProps.0.props.debugLabel>阴道透视</AnimationProp_Vagina.alternateProps.0.props.debugLabel>
<AnimationProp_Vagina.animPropProperties.debugLabel>阴道透视</AnimationProp_Vagina.animPropProperties.debugLabel>
</LanguageData>

View file

@ -1,28 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<LanguageData>
<!-- Mod menu -->
<RimAnim_ModSettings>RJW Animation Settings</RimAnim_ModSettings>
<RimAnim_SoundOverride>启用声音覆盖</RimAnim_SoundOverride>
<RimAnim_GenitalRotation>控制生殖器旋转</RimAnim_GenitalRotation>
<RimAnim_OrgasmQuiver>启用高潮颤抖</RimAnim_OrgasmQuiver>
<RimAnim_RapeShiver>启用强奸颤动</RimAnim_RapeShiver>
<RimAnim_HeartsDuringLovin>性爱时启用心形</RimAnim_HeartsDuringLovin>
<RimAnim_PlayNonsexual>播放非性行为的动画 (牵手、亲热)</RimAnim_PlayNonsexual>
<RimAnim_AnimManagerTab>启用动画管理器选项卡</RimAnim_AnimManagerTab>
<RimAnim_ShiverIntensity>颤抖/颤动强度 (默认为 2): </RimAnim_ShiverIntensity>
<RimAnim_Voices>在动画期间播放语音</RimAnim_Voices>
<RimAnim_HumanlikeVoicesDefault>如果没有找到外星种族的声音,则默认播放人类的声音</RimAnim_HumanlikeVoicesDefault>
<RimAnim_DebugMode>调试模式</RimAnim_DebugMode>
<RimAnim_FloatRangeRenderTree>调整渲染树偏移表单的浮动范围:</RimAnim_FloatRangeRenderTree>
<!-- Main Tab Window -->
<RimAnims_AnimManager>Animation Manager</RimAnims_AnimManager>
<RimAnims_Warning>警告--一般情况下,你不想更改人类偏移值,只想更改外星种族偏移值或动物偏移值</RimAnims_Warning>
<RimAnims_CopyToClipboard>将偏移值复制到剪贴板</RimAnims_CopyToClipboard>
<RimAnims_ShareSettings>将偏移值粘贴到 OffsetDef 中,或在 Discord 中共享</RimAnims_ShareSettings>
<RimAnim_ModSettings>RJW 动画设置</RimAnim_ModSettings>
<RimAnim_SoundOverride>启用声音覆盖</RimAnim_SoundOverride>
<RimAnim_GenitalRotation>控制生殖器旋转</RimAnim_GenitalRotation>
<RimAnim_OrgasmQuiver>启用高潮颤抖</RimAnim_OrgasmQuiver>
<RimAnim_RapeShiver>启用强奸颤动</RimAnim_RapeShiver>
<RimAnim_HeartsDuringLovin>性爱时启用心形</RimAnim_HeartsDuringLovin>
<RimAnim_PlayNonsexual>播放非性行为的动画 (牵手、亲热)</RimAnim_PlayNonsexual>
<RimAnim_AnimManagerTab>启用动画管理器选项卡</RimAnim_AnimManagerTab>
<RimAnim_ShiverIntensity>颤抖/颤动强度 (默认为 2): </RimAnim_ShiverIntensity>
<RimAnim_Voices>在动画期间播放语音</RimAnim_Voices>
<RimAnim_HumanlikeVoicesDefault>如果没有找到外星种族的声音,则默认播放人类的声音</RimAnim_HumanlikeVoicesDefault>
<RimAnim_DebugMode>调试模式</RimAnim_DebugMode>
<RimAnim_FloatRangeRenderTree>调整渲染树偏移表单的浮动范围:</RimAnim_FloatRangeRenderTree>
<RimAnim_MaleAnalCanBeFucked>为男性角色播放肛交动画</RimAnim_MaleAnalCanBeFucked>
<RimAnim_PlaySounds>在动画期间播放声音</RimAnim_PlaySounds>
<RimAnim_VoicesVolume>语音音量:</RimAnim_VoicesVolume>
<RimAnim_SoundsVolume>声音音量:</RimAnim_SoundsVolume>
<RimAnims_AnimManager>动画管理器</RimAnims_AnimManager>
<RimAnims_Warning>警告--一般情况下,你不想更改人类偏移值,只想更改外星种族偏移值或动物偏移值</RimAnims_Warning>
<RimAnims_CopyToClipboard>将偏移值复制到剪贴板</RimAnims_CopyToClipboard>
<RimAnims_ShareSettings>将偏移值粘贴到 OffsetDef 中,或在 Discord 中共享</RimAnims_ShareSettings>
<RimAnims_Reset>重置</RimAnims_Reset>
</LanguageData>

View file

@ -35,8 +35,6 @@
<v1.6>
<li>/</li>
<li>1.6</li>
<li IfModActive="erdelf.HumanoidAlienRaces">Patch_HumanoidAlienRaces/1.5</li>
<li IfModActive="c0ffee.SexToysMasturbation">Patch_SexToysMasturbation/1.5</li>
<li IfModNotActive="shauaputa.rimnudeworld">Patch_NoRimNudeWorld/1.5</li>
<li IfModNotActive="shauaputa.rimnudeworldzoo">Patch_NoRimNudeWorldZoo/1.5</li>
</v1.6>