Compare commits

..

5 commits

Author SHA1 Message Date
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>