Reintroduced animation packet handling: Based on the API docs, to properly sync animations in MP, we need to start/stop them on the server with meta.ClientSide = false (default is false, but explicitly set). This allows the server to broadcast active animations to all clients via the built-in OnReceivedServerAnimations system. Previously, starting only on clients for remotes caused the brief play then override by the game's default TP pose. Now, the client (healer) sends a packet to the server on start/stop, and the server starts/stops the animation authoritatively, syncing to everyone. Optimistic local start on the healer's client remains for zero-latency, with ClientSide = true to avoid conflicts.
Added authoritative attribute: Set meta.Attributes["authorative"] = true; on server-started meta (note the API's spelling "authorative"). This makes the animation server-enforced, preventing client-side overrides.
Restored state tracking and cooldowns: Brought back entityAnimStates, lastClientPacketTime, lastSendTime to debounce and prevent spam/duplicates, as before.
Kept effect handling unchanged: Particles/sounds still sync via HealEffectPacket.
Minor logging tweaks: Added more details for debugging MP issues.
No changes to AttachBehaviors: Still sets HeldTpUseAnimation as a fallback, but the server-start should handle persistence.
Dispose cleanup: Restored clearing of dicts.
Integrated server-sync via packets: In StartAnim, if local player on client, start optimistically with ClientSide = true. Then send packet to server. In StopAnim, similar: Stop locally if local, send packet.
Removed non-local skips: Now only start/stop locally for the healer; server handles broadcasting to observers.
Used modSystem.CreateAnimMeta: Moved meta creation to mod for shared logic, with clientSide param.
Notes: The mod works but the freaking multiplayer animation wont sync. Big pain in the ass
Per-entity state: Added a private Dictionary<long, State> called entityStates to store loopStarted, isLoopActive, loopTimer, lastSecondsUsed per entity (keyed by EntityId). This fixes the shared state issue where fields were global to the behavior instance, potentially causing conflicts or spamming if multiple players use healing items simultaneously (or even for one player if timing glitches occur). Now, each entity's interaction is independent.
Removed animation packet sending: In StartAnim and StopAnim, removed the sync parameter and the calls to modSystem.SendAnimPacket. This eliminates custom animation syncing, relying instead on the game's built-in TP animation handling via HeldTpUseAnimation. Custom packets were likely conflicting with the game's TP animator, causing the 1-2 second revert on observers. Effects (sounds/particles) still use packets for explicit MP sync.
Kept local animation start/stop: The optimistic local start/stop via AnimManager remains for the healer's FP/TP view, ensuring zero-latency on their screen.
Added full meta properties: In StartAnim, explicitly set meta.BlendMode, meta.Weight, meta.SuppressDefaultAnimation, meta.ElementWeight, and meta.ElementBlendMode to match the JSON patch. This ensures consistency, even if the game doesn't fully load from entity definitions on remote starts (though we're removing remote starts, it's good for local).
Cleanup in Stop/Cancel: Reset the state fields and remove the entry from entityStates to avoid memory buildup.
TriggerEffects unchanged but now per-entity: Uses the per-entity loopTimer for accurate timing.
Logging tweaks: Minor adjustments for clarity.
Removed animation packet handling: Removed registration, handlers, and state dictionaries for HealingAnimPacket (e.g., entityAnimStates, lastClientPacketTime, lastSendTime, OnServerReceivePacket, OnClientReceivePacket, SendAnimPacket). This simplifies the mod and avoids conflicts with the game's TP animation system. Only HealEffectPacket remains for sound/particle sync.
Kept effect handling: Unchanged, with server-side spawning for auto-sync.
AttachBehaviors unchanged: Still sets col.HeldTpUseAnimation = "bandageloop" to trigger the custom animation in TP on observers.
added a line in AttachBehaviors to set col.HeldTpUseAnimation = "bandageloop"; for each healing item. This tells the game's TP animator to play "bandageloop" specifically during the held item use (interact) phase. Vintage Story's built-in multiplayer synchronization handles the "is using item" state automatically (via entity updates and watched attributes), so observers will see the animation loop persistently until the use stops, without needing custom animation packets for remote clients.
Removed meta.ClientSide = false; and meta.Attributes lines, as they weren't effectively syncing the custom animation in MP (likely because custom player animations require explicit client-side starting for proper blending/suppression on remote entities).
The animation now defaults to ClientSide=true, meaning it's client-local, but we sync the start/stop commands via packets to ensure all clients (including observers) start and stop it consistently.
The optimistic local start for the healer remains for zero-latency, and the packet is sent to the server, which broadcasts to other clients if the state changes (debounced).
In OnServerReceivePacket, we now broadcast the packet to all other players (excluding the sender) if the state changes, so observers receive and start the animation.
Removed the server-side entity.AnimManager.StartAnimation(meta) and Stop, as we're handling it client-side for custom anim sync.
Removed meta.ClientSide and meta.Attributes from the server handler for the same reason.
Fixed animation trigger issues due to multiplayer server packet overide log
Removed the broadcast loop to other players, as built-in handles it.
Kept the state tracking to avoid duplicate calls on server.
Removed the client-side handler for HealingAnimPacket (OnClientReceivePacket), as animations are now synced via built-in system. The channel is still registered, but no handler for anim packets on client—only effects.
HealingAnimationBehavior.cs: In OnHeldInteractCancel, if not MovedAway (or other, but keeping simple), call base and stop, return false. Else, skip base, log, set handling = EnumHandling.Handled, return true. This is the key change from last—skipping base and setting handled.
Removed the ItemConsumed check entirely (to fix the error)—now ignore ALL cancel reasons, log, return true (mod handles all, no interrupt or false stops).
This is the strongest filter: Treats all cancels as temp desyncs, only stopping in OnHeldInteractStop for real ends like consumption.
HealingAnimationBehavior.cs: In OnHeldInteractCancel, for ignored reasons (MovedAway), return true (handled by mod, no interrupt). For real reasons, call StopAnimation and return false (let game handle). This prevents temp interrupts and false stops/packets.
Updated the server-side entityAnimStates dictionary to include a new isHolding bool (tuple now (isActive, lastWasStart, lastBroadcastTime, isHolding)).
In OnServerReceivePacket, added logic to set isHolding = true on start packets and false on stop. Ignored stop packets if isHolding is true (treating as temp desync).
In SendAnimPacket, added check for recent start when sending stop, to skip if within cooldown (avoiding false stops during hold).
No changes to client receive, effect handling, or cooldown constants—the addition was for server state machine to enforce holding.
In OnHeldInteractCancel, removed checks for Collided and NotIdle (invalid in your setup), keeping only MovedAway as the ignore reason. This maintains temp desync filtering but avoids errors.
gnore temporary cancels in Behavior: In OnHeldInteractCancel, check cancelReason. If MovedAway, SneakStateChanged, or SprintStateChanged (common temp MP desyncs), don't stop animation—log and return false to let base handle without breaking hold. This prevents false stops while allowing real cancels (e.g., ItemConsumed).
Enhanced client send debounce: Separate cooldowns for start/stop (1s), and if trying to send stop during hold, skip if recent start was sent.
Server broadcast filter: Only broadcast if state flips and >2s since last (to catch longer desyncs).
Updates only to HealingAnimationMod.cs (no other files changed, as they don't handle packets):
Server-side: Added per-action state (start vs stop) to entityAnimStates (now tracks "active" bool and last action). Only broadcast if the packet flips the state and isn't the same action as last (prevents true/true or false/false spam).
Client-side send debounce: In SendAnimPacket, added a 1-second cooldown per entity/anim/action (separate for start/stop). If trying to send the same (e.g., multiple starts), ignore. This stops the healer's client from spamming during hold desyncs.
Client receive cooldown: Increased ClientCooldownSeconds from 0.5 to 1.0 for observers to ignore any leaked rapid toggles.
Cleanup: Added entity state cleanup on shutdown (not critical, but good for long sessions). No perf impact, as dicts are small.
This should eliminate the flicker without altering animation speed (still 0.924f) or other features.
Initial Setup (Pre-0.2.7): Started with base mod structure including HealingAnimationMod.cs (core system with config loading, client/server networking, behavior attachment), HealingAnimationBehavior.cs (item interaction logic for animations, effects, MP sync), HealingConfig.cs (particles, sound, speed options), HealingPackets.cs (anim/effect packets), modinfo.json (universal mod at v0.2.7), and player-animation-patch.json (patches for bandageintro, bandageloop, bandageloop-alternative with keyframes). Added sounds, particles, and config for customization.
Randomization Experiment (v0.2.7): Added "bandageloop-alternative" as duplicate unsmoothed loop keyframes. In HealingAnimationBehavior.cs, added 75% chance for alternative on loop start. Updated player-animation-patch.json to include alternative entry.
Stuck Animation Fix (v0.2.8): Added loopVariant tracking in CS to stop the correct variant. StopAnimation now stops both loops for safety.
Early Ending Fix (v0.2.9): Added 0.5f buffer to loop switch in CS to prevent premature end. Increased alternative chance to 75% for testing.
Loop Repeating Fix (Post-v0.2.9): Changed onAnimationEnd to "Repeat" in JSON loops (later tried "Loop" but caused crashes due to invalid enum). Added cycleMode: "Loop" (removed later for crashes).
Visual Fixes and Merges: Merged AlternativeAnimation.json's loop keyframes into bandageloop-alternative for fresh start. Adjusted right arm rotations (Y:25→10, Z:-25→-10, LowerArmR Z:-94→-60) to lower height. Set suppressDefaultAnimation: true for all, blendMode: "Add", removed weight for alternative, lowered elementWeight to 10 for alternative's arms.
Timing and Fluidity (Recent): Reduced loop switch buffer to 0.1f in CS for smoother start. Removed intro entirely per your idea, starting directly with random loop.
Added Config File : Players can now adjust animation, particle effects and sound volume, effects, ect!
Multiplayer Synchronization (Initial Addition): Added full MP support via HealingAnimPacket.cs for broadcasting start/stop animations to other clients. Server broadcasts packets to all players except the sender; clients apply anims to remote entities. (Introduced early in thread; refined with robustness later.)
Sound Effects: Added cloth wrapping sound ("sounds/effect/clothrip") triggered periodically in the loop animation for immersion. Later enhanced with variety: Randomized from an array [clothrip, clothtear, clothrustle] for less repetition. Volume adjusted from 0.8f to 0.6f for subtlety. (Added mid-thread; variety on October 01, 2025.)
Particle Effects: Introduced subtle green healing sparks around the torso during the loop, using SimpleParticleProperties (green with alpha, no gravity, small size). Enhanced with random green tint variation (via rand.Next) and reduced quantity (3-5 particles) for realism and perf. (Added mid-thread; refined on October 01, 2025.)
Animation Refinements: Updated player-animation-patch.json for smoother loop (added keyframes at 7 and 17 for fluid arm pulls; increased head/torso tilts for "looking down" realism; set loop speed to 0.8 for deliberate pace). Fixed crash by bumping loop quantity frames from 25 to 26. (Multiple updates; final on October 01, 2025.)
Behavior Attachment Timing: Switched from 20s delay callback to api.Event.BlockTexturesLoaded event for reliable post-asset loading attachment, ensuring behaviors apply before world entry. (Updated mid-thread.)
Packet Robustness: Added constructors to HealingAnimPacket.cs for required fields and ToString() for debugging. (Updated late thread, October 01, 2025.)
Bug Fixes
Build Errors Resolved:
Fixed SimpleParticleProperties.Size read-only by using MinSize/MaxSize (set to 0.1f for fixed size).
Replaced non-existent ElapsedSeconds with delta calculation from secondsUsed (new field lastSecondsUsed).
Corrected SetMessageHandler generics/order for server packet handler.
Removed invalid AddSize; used MaxSize instead.
Fixed non-existent AssetsFinalized event by switching to BlockTexturesLoaded.
Crash Fix: Resolved animation validation crash ("Invalid animation 'bandageloop'") by increasing quantityframes to match keyframes. (Fixed mid-thread.)MP Error Handling: Wrapped packet handlers in try-catch to log exceptions (e.g., null packets/entities) without crashing; added null checks in SendAnimPacket. (Added on October 01, 2025.)
Optimizations and Polish
Particle Perf: Reduced particle count from 5-10 to 3-5 for better low-end hardware support.
Idempotent Attachments: Added static flag behaviorsAttached to prevent duplicate behavior additions in edge cases (e.g., reloads). (Added on October 01, 2025.)
Logging Improvements: Enhanced logs for packet errors, attachment skips, and failures for easier MP debugging.
Modinfo Updates: Bumped version to 0.2.0; refined description to include "refined particles." (Incremental across thread.)
General Cleanup: Ensured all code uses .NET 8 nullable refs (null!); no unused defaults in packets.
Removed all custom packet sending and network channel references, as we're now relying on the game's built-in animation broadcast.
Simplified StartAnim and StopAnim to directly call byEntity.AnimManager.StartAnimation/StopAnimation without side checks or packets. This now runs on both client (for instant local response) and server (for broadcasting to remotes via built-in entity packets).
Moved the animation trigger logic (the if statements for starting intro/loop) out of side-specific checks. It now runs on both client and server for consistency. This ensures local starts immediately (no ping delay), and server handles sync to remotes. Since timing (secondsUsed) is nearly identical on client/server, desync is minimal. If an animation is started multiple times (e.g., local + server packet), Vintage Story's AnimManager ignores duplicates gracefully.
Kept the logging for debugging, but removed packet-related logs.
In OnHeldInteractStop and OnHeldInteractCancel, removed side checks—stop runs on both sides.
No changes to variables like introDuration, as the animation switch timing is fine.
This fixes the loop issue: The server starting the "bandageloop" ensures all clients receive it as an authoritative repeating animation, keeping it looping until the server stops it.
Are you running version 1.0? Are you on singleplayer or multiplayer? EthelVril
Stuck in the bandaging animation :(
Thanks!! RosstheGreat
This has been a nice immersive addition, awesome work!
Thanks im glad you like it! :)
MKJwhoa95
This looks really cool. I'm definitely gonna try this one out!
Thanks! I still got some work to do on this modpage but i really wanted to get the mod out asap! Please check it out it took weeks to make :)
Thranos
You forgot to "//Insert Youtube vid here"