Skip to content

Generic animations - #1067

Merged
DVOniksWyvern merged 12 commits into
WWhiteDreamProject:masterfrom
Spatison:animations
Feb 13, 2026
Merged

Generic animations#1067
DVOniksWyvern merged 12 commits into
WWhiteDreamProject:masterfrom
Spatison:animations

Conversation

@Spatison

@Spatison Spatison commented Feb 5, 2026

Copy link
Copy Markdown
Member

Описание PR

Необходим для красивых частичек крови в ньюмеде.

ПР добавляет прототип анимаций. Он не требует обязательного проигрывания на клиенте и анимация автоматически кэшируется на клиенте.

По хорошему это следует перенести в движок, а лучше вообще переписать то как работают анимации.

FlipOnHit был заменен на AnimateOnHit.
AnimatedEmotes был удален и заменен на не щиткодную версию губов.
Добавлен AnimateOnStartup. Название говорит само за себя, да?


Изменения

🆑

  • tweak: Конструкты кровавого культа теперь светятся в темноте
  • fix: Меню выбора конструкта культа теперь корректно отображается

@github-actions

github-actions Bot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

RSI Diff Bot; head commit 4bb8bc8 merging into 2c511bb
This PR makes changes to 1 or more RSIs. Here is a summary of all changes:

Resources/Textures/_White/Mobs/Construct/artificer.rsi

State Old New Status
make_cult Removed
make_holy Removed
spawn_construct Added
spawn_glow_cult Added
spawn_glow_holy Added

Resources/Textures/_White/Mobs/Construct/juggernaut.rsi

State Old New Status
make_cult Removed
make_holy Removed
spawn_construct Added
spawn_glow_cult Added
spawn_glow_holy Added

Resources/Textures/_White/Mobs/Construct/wraith.rsi

State Old New Status
make_cult Removed
make_holy Removed
spawn_construct Added
spawn_glow_cult Added
spawn_glow_holy Added

Edit: diff updated after 4bb8bc8

@coderabbitai

coderabbitai Bot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Заменены устаревшие клиентско/серверные системы анимаций (animated emotes, FlipOnHit) на новую сериализуемую инфраструктуру WhiteAnimationPlayerSystem с прототипами анимаций, новыми компонентами-триггерами и интеграцией в чат/прототипы.

Changes

Cohort / File(s) Summary
Удалённые старые эмот/flip системы
Content.Client/Emoting/AnimatedEmotesSystem.cs, Content.Server/Emoting/AnimatedEmotesSystem.cs, Content.Client/_White/Animations/FlipOnHitSystem.cs, Content.Server/_White/Animations/FlipOnHitSystem.cs, Content.Shared/Emoting/SharedAnimatedEmotesSystem.cs, Content.Shared/_White/Animations/SharedFlipOnHitSystem.cs
Полное удаление старых систем и связанных обработчиков/событий, убраны самодельные проигрыватели анимаций и подписки.
Удалены компоненты/сетевые типы
Content.Client/_White/Animations/FlippingComponent.cs, Content.Shared/Emoting/AnimatedEmotesComponent.cs, Content.Shared/_White/Animations/FlipOnHitComponent.cs
Удалены компоненты и сопряжённые сетевые события/const (FlippingComponent, AnimatedEmotesComponent, FlipOnHitComponent).
Новый WhiteAnimationPlayerSystem (shared/server/client)
Content.Shared/_White/Animations/Systems/SharedWhiteAnimationPlayerSystem.cs, Content.Server/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs, Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs
Добавлен абстрактный shared API и сервер/клиент реализации с методами Play/Stop, сетевыми сообщениями PlayAnimationMessage/StopAnimationMessage и логикой доставки/фильтрации.
Сериализуемые типы анимаций и keyframes
Content.Shared/_White/Animations/Prototypes/AnimationPrototype.cs, Content.Shared/_White/Animations/AnimationTrackData.cs, Content.Shared/_White/Animations/KeyFrameData.cs
Введена иерархия данных для анимаций: AnimationPrototype, треки и keyframe-типизация для конфигурации анимаций.
Новые компоненты-триггеры и системы
Content.Shared/_White/Animations/Components/AnimateOnHitComponent.cs, Content.Shared/_White/Animations/Components/AnimateOnStartupComponent.cs, Content.Shared/_White/Animations/Systems/AnimateOnHitSystem.cs, Content.Client/_White/Animations/Systems/AnimateOnStartupSystem.cs
Добавлены AnimateOnHit и AnimateOnStartup с системами: обработка MeleeHit и ComponentStartup, валидации (whitelist, standing) и вызовы WhiteAnimationPlayerSystem.
Интеграция с эмотами и чат-системой
Content.Shared/Chat/Prototypes/EmotePrototype.cs, Content.Server/Chat/Systems/ChatSystem.cs, Content.Server/Chat/Systems/ChatSystem.Emote.cs
Поле Event в EmotePrototype заменено на типизированное Animation; серверный ChatSystem добавляет зависимость на WhiteAnimationPlayerSystem и условно запускает анимацию при эмоте.
Команда консоли и локализация
Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs, Resources/Locale/en-US/_white/animations/commands/play-animation.ftl, Resources/Locale/ru-RU/_white/animations/commands/play-animation.ftl, Resources/Locale/en-US/_white/shell.ftl, Resources/Locale/ru-RU/_white/shell.ftl
Добавлена admin-команда playanimation с автодополнением и локализацией подсказок аргументов.
YAML-прототипы анимаций и данные
Resources/Prototypes/_White/Animations/emote.yml, Resources/Prototypes/_White/Animations/mob_spawn.yml
Добавлены прототипы анимаций (Flip, Spin, Jump, mob spawn) с треками и ключевыми кадрами.
Обновления прототипов сущностей / оружия / эффектов
Resources/Prototypes/Entities/Mobs/base.yml, Resources/Prototypes/_White/Entities/Mobs/Construct/*.yml, Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml, Resources/Prototypes/Entities/Objects/Weapons/Melee/hosbat.yml, Resources/Prototypes/_Lavaland/Entities/Objects/Weapons/crushers.yml, Resources/Prototypes/_White/Entities/Effects/Spawn/*
Удалён компонент AnimatedEmotes у base mob; для конструкта добавлены AnimateOnStartup; у оружия FlipOnHit заменён на AnimateOnHit (часто с animation: Flip и whitelist); удалены старые SpawnMobConstruct* эффекты и обновлены связки прототипов.
Текстуры / метаданные и слои
Resources/Textures/_White/Mobs/Construct/*.rsi/meta.json, Content.Server/_White/BloodCult/Constructs/Shell/ConstructShellComponent.cs, Content.Shared/_White/BloodCult/Construct/ConstructComponent.cs
Переименованы состояния текстур (например make_cultspawn_construct), обновлены delays; в ConstructShellComponent обновлены ссылки на прототипы; добавлен enum ConstructLayer для слоёв спрайта.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • Remuchi
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Generic animations' clearly describes the main change: introducing a prototype animation system for the codebase.
Description check ✅ Passed The description is related to the changeset, explaining the purpose of animations for blood particles and detailing system replacements (FlipOnHit→AnimateOnHit, AnimatedEmotes removal).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Content.Server/Chat/Systems/ChatSystem.Emote.cs (1)

224-232: ⚠️ Potential issue | 🟡 Minor

Проверьте удаление сущности перед проигрыванием анимации.

Если обработчик события удалит сущность после RaiseLocalEvent (Line 227), вызов _whiteAnimationPlayer.Play может работать по удалённому uid. Добавьте проверку на удаление перед проигрыванием.

🛡️ Предложение фикса
        RaiseLocalEvent(uid, ref ev, true); // goob edit
+       if (Deleted(uid))
+           return;

        // WD EDIT START
        if (proto.Animation.HasValue)
            _whiteAnimationPlayer.Play(uid, proto.Animation.Value);
🤖 Fix all issues with AI agents
In `@Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs`:
- Around line 43-76: The CachePrototypes method currently aborts all caching
when an unknown track type yields a null AnimationTrack because of `if
(animationTrack == null) return;`; change this to skip that single track instead
(e.g. `if (animationTrack == null) { /* log warning */ continue; }`) so the loop
over animationPrototype.AnimationTracksData continues and the rest of the
prototype still gets cached; update the code around the null check in
CachePrototypes (referencing animationTrack, animationTrackData,
GetComponentProperty/GetControlProperty/GetPlaySound/GetSpriteFlick and
AnimationPrototype.Animation) to log a warning about the unsupported track type
and continue rather than returning, leaving _animations population intact.
- Around line 78-90: GetComponentProperty currently returns an empty
AnimationTrackComponentProperty when _component.TryGetRegistration fails, which
produces tracks without ComponentType; change GetComponentProperty so that when
_component.TryGetRegistration(animationTrackData.ComponentType, out var
registration, true) returns false it returns null (or an optional) instead of an
empty AnimationTrackComponentProperty, and update callers to skip tracks where
GetComponentProperty returns null; ensure references to
AnimationTrackComponentProperty.ComponentType and SetProperty are only used when
registration is present.

In `@Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs`:
- Around line 16-44: The command treats arguments in the wrong order: when two
args are provided the code parses args[0] as the entity but also uses args[0] as
the animation id; fix Execute in PlayAnimationCommand so that when args.Length
== 2 you parse the second argument as the NetEntity (use
NetEntity.TryParse(args[1], out var netEntity) and set entity =
_entityManager.GetEntity(netEntity)) and keep the animation id as args[0] when
calling _entityManager.System<WhiteAnimationPlayerSystem>().Play(entity.Value,
args[0]); ensure the single-arg fallback still uses the attached entity and
animation id from args[0].

In `@Content.Shared/_White/Animations/AnimationTrackData.cs`:
- Around line 33-39: The DataField on the Property member is missing required:
true which allows null/missing YAML prototypes; update the DataField attributes
for the Property field in AnimationTrackControlPropertyData (class
AnimationTrackControlPropertyData -> Property) and also in
AnimationTrackComponentPropertyData (class AnimationTrackComponentPropertyData
-> Property) to include required: true so the parser will throw if the field is
absent.
- Around line 21-31: Add parser-level enforcement by marking the fields in
AnimationTrackComponentPropertyData as required: set the DataField attributes
for ComponentType and Property to include required: true, and initialize both
string fields with = default! (e.g., public string ComponentType = default!;
public string Property = default!;) to match existing style and avoid
nullable/empty values during YAML parsing; leave the class inheriting
AnimationTrackPropertyData unchanged.

In `@Content.Shared/_White/Animations/KeyFrameData.cs`:
- Around line 30-35: В классе KeyFrameSpriteFlickData добавьте атрибуту
[DataField] параметр required: true и инициализируйте поле State значением по
умолчанию (например пустой строкой), чтобы State не могло быть null; измените
объявление поля State внутри KeyFrameSpriteFlickData (параметры атрибута
DataField и значение по умолчанию) — это предотвратит NRE при использовании поля
в WhiteAnimationPlayerSystem, которое ожидает ненулевое значение.
- Around line 14-20: Добавьте в описание поля Value атрибут DataField с required
= true, чтобы гарантировать, что KeyFramePropertyData.Value всегда
инициализирована и исключить NRE при обращении к keyFrameProperty.Value.Value в
методе WhiteAnimationPlayerSystem.SetProperty; отредактируйте класс
KeyFramePropertyData (поле Value) и пересоберите, чтобы прототипы без этого поля
не проходили валидацию.
- Around line 22-28: KeyFrameSoundData.Sound can be null and is used without
null-check in WhiteAnimationPlayerSystem.ResolveSound; mark it required to avoid
NRE by adding the data attribute and non-null initializer: add
[DataField(required: true)] to the Sound field on class KeyFrameSoundData and
make the declaration non-nullable (e.g. public SoundSpecifier Sound = default!;)
so deserialization fails on missing data and callers can assume Sound is present
when calling ResolveSound.

In `@Content.Shared/_White/Animations/Systems/AnimateOnStartupSystem.cs`:
- Around line 5-17: The AnimateOnStartupSystem in Content.Shared currently
subscribes to ComponentStartup and calls _whiteAnimationPlayer.Play from
OnStartup, causing it to run on both client and server; either move the
AnimateOnStartupSystem class into Content.Server or guard the subscription with
if (IsServer) in Initialize so the
SubscribeLocalEvent<AnimateOnStartupComponent, ComponentStartup>(OnStartup) is
only registered on the server; ensure you still reference the same
AnimateOnStartupComponent, OnStartup handler, and _whiteAnimationPlayer.Play
usage so behavior remains identical but only executes server-side.

In `@Content.Shared/_White/Helpers/DynamicValueSerializer.cs`:
- Around line 40-41: The code uses the null-forgiving operator on the result of
serializationManager.Read, which can produce a NullReferenceException; in
DynamicValueSerializer (the code that calls node.Get<ValueDataNode>("type"),
GetType(serializationManager, type) and serializationManager.Read(...,
node.Get("value"), context)), remove the trailing '!' and explicitly handle a
null return from serializationManager.Read — either throw a clear exception with
context (including the 'type' and node info) or return/assign a safe default
value so callers won't get an unexpected NRE; ensure the null-handling is
applied right after the call that returns 'value'.

In `@Resources/Prototypes/_White/Animations/emote.yml`:
- Around line 83-107: В анимации с id Jump последний KeyFramePropertyData (в
блоке animationTracksData → componentType: Sprite → property: Offset) имеет
keyframe: 0.125, из‑за чего анимация длиной length: 0.25 содержит пустую вторую
половину; переместите/измените значение keyframe у последнего keyframe на 0.25
чтобы финальный кадр совпадал с длиной анимации.
- Around line 1-25: В анимации с id Flip ключи в animationTracksData → keyFrames
для компонента Sprite (property Rotation) имеют два последних keyframe с
значением 0.25 при общей length: 0.5, из‑за чего вторая половина пустая;
исправьте это, переместив финальный KeyFramePropertyData (значение 360) на
keyframe: 0.5 (либо альтернативно уменьшите length до 0.25) чтобы ключи
покрывали весь диапазон времени.
- Around line 27-81: В анимации type: animation с id: Spin (key: spin) все
keyFrames после первого имеют одинаковый keyframe 0.075, из‑за чего таймкоды не
возрастают и вращение «схлопывается»; исправьте значения keyframe в блоке
animationTracksData → keyFrames так, чтобы они были возрастающими и в сумме
давали length 0.6 (например: 0, 0.075, 0.15, 0.225, 0.3, 0.375, 0.45, 0.525,
0.6), оставив остальные поля (value/type/value) без изменений.
🧹 Nitpick comments (9)
Content.Shared/_White/BloodCult/Construct/ConstructComponent.cs (1)

13-18: Зафиксируйте численные значения enum для сетевой стабильности.

Так вы снизите риск случайного изменения значений при будущих правках.

♻️ Возможная правка
 public enum ConstructLayer : byte
 {
-    Base,
-    Unshaded
+    Base = 0,
+    Unshaded = 1
 }
Content.Shared/_White/Helpers/DynamicValueSerializer.cs (2)

70-71: Улучшить сообщение об ошибке.

Сообщение "NO TYPE " неинформативно. Рекомендуется использовать более описательное сообщение для упрощения отладки.

♻️ Предлагаемое исправление
-            throw new InvalidMappingException("NO TYPE " + typeValue.Value);
+            throw new InvalidMappingException($"Unable to resolve type '{typeValue.Value}'. Ensure the type name is fully qualified or registered.");

52-62: Валидация не проверяет разрешимость типа.

Метод Validate проверяет только наличие полей "type" и "value", но не валидирует, что строка типа может быть успешно разрешена. Это может привести к ошибкам на этапе Read, а не на этапе валидации.

Content.Shared/_White/Animations/Systems/SharedWhiteAnimationPlayerSystem.cs (1)

12-17: Используйте ProtoId<AnimationPrototype> для типобезопасности и консистентности.

Метод Play принимает ProtoId<AnimationPrototype>, но PlayAnimationMessage.AnimationId объявлен как string. Это вызывает неявные преобразования в обе стороны (ProtoId → string на сервере, string → ProtoId на клиенте), что снижает типобезопасность. В кодовой базе уже используются ProtoId в сетевых сообщениях (например, RuneDrawerSelectedMessage), поэтому рекомендуется использовать ProtoId<AnimationPrototype> напрямую.

♻️ Предлагаемое исправление
 [Serializable, NetSerializable]
-public sealed class PlayAnimationMessage(NetEntity animatedEntity, string animationId) : EntityEventArgs
+public sealed class PlayAnimationMessage(NetEntity animatedEntity, ProtoId<AnimationPrototype> animationId) : EntityEventArgs
 {
     public NetEntity AnimatedEntity = animatedEntity;
-    public string AnimationId = animationId;
+    public ProtoId<AnimationPrototype> AnimationId = animationId;
 }
Content.Server/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs (1)

7-10: LGTM!

Серверная реализация корректно переопределяет абстрактный метод Play и отправляет сетевое сообщение клиенту.

Опционально: можно добавить проверку существования сущности перед отправкой сетевого события, чтобы избежать лишнего сетевого трафика для удалённых сущностей.

♻️ Опциональная проверка валидности сущности
 public sealed class WhiteAnimationPlayerSystem : SharedWhiteAnimationPlayerSystem
 {
-    public override void Play(EntityUid uid, ProtoId<AnimationPrototype> animationId) =>
-        RaiseNetworkEvent(new PlayAnimationMessage(GetNetEntity(uid), animationId));
+    public override void Play(EntityUid uid, ProtoId<AnimationPrototype> animationId)
+    {
+        if (!Exists(uid))
+            return;
+
+        RaiseNetworkEvent(new PlayAnimationMessage(GetNetEntity(uid), animationId));
+    }
 }
Content.Shared/_White/Animations/KeyFrameData.cs (1)

6-12: Проверьте, что базовый KeyFrameData помечен DataDefinition.
Если Robust.Serialization требует [DataDefinition] на базовом типе, поле Keyframe может не сериализоваться и останется 0. Сверьте с документацией и при необходимости добавьте атрибут.

💡 Возможная правка
-[Serializable]
+[Serializable, DataDefinition]
 public abstract class KeyFrameData
Content.Shared/_White/Animations/AnimationTrackData.cs (2)

5-11: Проверьте необходимость [DataDefinition] на базовом AnimationTrackData.
Если базовый тип без [DataDefinition] не участвует в сериализации, KeyFrames может не загрузиться. Сверьте с документацией Robust.Serialization.

💡 Возможная правка
-[Serializable]
+[Serializable, DataDefinition]
 public abstract class AnimationTrackData

45-50: Для SpriteFlick лучше требовать LayerKey явно.
Иначе можно получить дефолтный enum‑значок, который не соответствует реальному слою.

💡 Возможная правка
-    [DataField]
-    [AlwaysPushInheritance]
-    public Enum LayerKey;
+    [DataField(required: true)]
+    [AlwaysPushInheritance]
+    public Enum LayerKey;
Content.Shared/_White/Animations/Prototypes/AnimationPrototype.cs (1)

28-29: Сделайте Animation безопасным к null и явно инициализируйте.

Поле не сериализуется и сейчас форсируется null!; если его читают до заполнения, возможен NRE. Предлагаю либо инициализировать его в пост‑обработке прототипа, либо сделать nullable и добавить проверки. По возможности лучше использовать конкретный тип вместо object.

♻️ Возможное смягчение null‑риска
-    [ViewVariables]
-    public object Animation = null!;
+    [ViewVariables]
+    public object? Animation;

Comment thread Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs Outdated
Comment thread Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs
Comment thread Content.Shared/_White/Animations/AnimationTrackData.cs Outdated
Comment thread Content.Shared/_White/Animations/AnimationTrackData.cs Outdated
Comment thread Content.Shared/_White/Animations/KeyFrameData.cs
Comment thread Content.Client/_White/Animations/Systems/AnimateOnStartupSystem.cs
Comment thread Content.Shared/_White/Helpers/DynamicValueSerializer.cs Outdated
Comment thread Resources/Prototypes/_White/Animations/emote.yml
Comment thread Resources/Prototypes/_White/Animations/emote.yml
Comment thread Resources/Prototypes/_White/Animations/emote.yml

@RedFoxIV RedFoxIV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В целом круто, но

  1. Предикт анимаций
    На данный момент в шареде доступен только один Play() метод, который играет анимацию на клиенте и отправляет с сервера сообщение об анимации. Да, оно скипается проверкой на уже играющую анимацию. Нет, мне похуй, всё равно хуйня получается, надо чинить, особенно если добавится возможность принудительно прерывать текущую анимацию и заменять её новой - тогда этот баг себя покажет.
    А ещё просто лишний трафик гоняется.

Проблема в том, что в 95% случаев, если анимация запускается из шареда - она по умолчанию будет запускаться у всех клиентов, в чей ПВС попадает анимируемая сущность, а у всех остальных - этой энтити просто не будет в игре или она будет засуспенжена. Повторный синхрон через серверное сообщение тут не требуется.

  1. чтобы при запуске длинной анимации она у нового игруна начиналась не сначала - тебе нужно будет дополнительно отслеживать время начала проигрывания каждой анимации и уведомлять об этом клиент, чтобы он сам мог посчитать оффсет. Например, новый компонент, который держит в себе словарь ключ - timestamp. На клиентском стартапе компонента все анимации, покрываемые этим словарём, будут сдвигаться в соответствии с тем, как давно началась анимация.

т.е. если у предмета начинает играть 10-секундная анимация, и через 5 секунд этот предмет попадает в ПВС какого-то игруна (допустим, телепортировался с лавы на станцию) - у игруна вызовется стартап этого компонента, компонент увидит, что анимация по ключу "ass_blast_usa" была запущена в 13 минут 37 секунд, а сейчас 13 минут 42 секунды - компонент сдвигает анимацию вперёд на 5 секунд и засыпает до следующего стартапа.

К сожалению, у меня недостаточно знаний о том, как работают РТшные анимации с ПВСом, а конкретно - если во время анимации энтити выпадает из ПВСа, а потом возвращается -

  • анимация ломается?
  • приостанавливается и по возвращению начинает играть с той же точки
  • приостанавливается и по возвращению начинает играть с той точки, как если бы она не выпадала?
    из того, что я помню о коде плейбека анимаций - если не первый вариант, то, скорее всего, второй.
  1. DynamicValue
    Это какой-то пиздец и абсолютная киллер фича (в плохом смысле) для любого, кто будет пилить анимации через прототипы. Необходимо что-то с этим сделать.

  2. фокус системы WhiteAnimationPlayerSystem
    Либо эта система играет роль более крутой системы анимации, либо эта система отвечает исключительно за проигрывание анимаций из прототипов.
    В первом случае нужен более-менее полный паритет по фичам с обычной системой анимации, а во втором - более соответствующее название.

Я давно не трогал анимации, поэтому не смогу ответить на вопрос, какие ещё фичи могут понадобится. Предположи сам, что может делать код из Content'а с анимациями (помимо их запуска) и попробуй добавить соответствующие методы в свою систему, чтобы не нужно было добавлять в зависимости "проигрыватель анимаций" и "проигрыватель анимаций 2"

Comment thread Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs Outdated
Comment thread Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs Outdated
Comment thread Content.Shared/_White/Animations/Prototypes/AnimationPrototype.cs Outdated
Comment thread Content.Shared/_White/Animations/Systems/AnimateOnHitSystem.cs Outdated
Comment thread Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs Outdated
Comment thread Content.Shared/_White/Animations/Prototypes/AnimationPrototype.cs Outdated
Comment thread Content.Shared/_White/Animations/Prototypes/AnimationPrototype.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs`:
- Around line 33-43: The Initialize method in WhiteAnimationPlayerSystem
subscribes to _prototype.PrototypesReloaded with OnPrototypeReload but never
unsubscribes; add an override Shutdown (or override Shutdown if already present)
in WhiteAnimationPlayerSystem that removes the handler by calling
_prototype.PrototypesReloaded -= OnPrototypeReload and performs any necessary
cleanup (matching Initialize's subscribe/CachePrototypes usage) so the system
doesn't leak when stopped or recreated.
- Around line 59-93: В методе CachePrototypes переменная realLength сейчас
складывает все keyframe'ы всех треков; нужно вместо суммирования брать максимум
длины по трекам: для каждого animationTrackData вычислить длину трека (например
максимальное значение keyFrame.Keyframe converted в TimeSpan) и присвоить
realLength = TimeSpan.Max(realLength, trackLength); сохраните оставшуюся логику
(использование TimeSpan.FromSeconds при вычислении и присвоение animation.Length
= animationPrototype.Length ?? realLength), обновите места с
animationTrackData.KeyFrames и переменной
realLength/animationPrototype.Animation соответственно.

In
`@Content.Shared/_White/Animations/Systems/SharedWhiteAnimationPlayerSystem.cs`:
- Around line 77-81: The XML doc for StopPredicted incorrectly says "The local
client will play animation" (copied from PlayPredicted); update the
summary/comments for the abstract method StopPredicted (and any related XML
tags) to state that the local client will stop the animation for the recipient
while the server stops it for other players, mirroring the intended behavior and
wording from PlayPredicted but replacing "play" with "stop" and adjusting
tense/clarity accordingly.
- Around line 48-53: The XML doc comment for the abstract method Stop(EntityUid
uid, string animationKey) contains a typo ("stoped"); update the documentation
text to use the correct past participle "stopped" (e.g., change "the animation
that will be stoped." to "the animation that will be stopped.") so the
summary/param XML is spelled correctly for the Stop method.
🧹 Nitpick comments (5)
Content.Shared/_White/Animations/Systems/AnimateOnHitSystem.cs (1)

30-34: Необязательное наблюдение: проверка IsDown для пользователя при ApplyToUser.

Когда ApplyToUser == true, PlayAnimation вызывается с target = args.User, и _standingState.IsDown(target) будет проверять состояние самого атакующего. На практике лежащий игрок, как правило, не может наносить удары ближнего боя, поэтому эта проверка избыточна (но безвредна). Если это осознанный выбор для перестраховки — всё в порядке.

Content.Client/_White/Animations/Systems/AnimateOnStartupSystem.cs (1)

18-21: Рассмотрите проброс Force в метод Play.

ent.Comp.Force используется только для решения «проигрывать ли повторно» (строка 18), но не передаётся в _whiteAnimationPlayer.Play() как параметр force. Сигнатура Play принимает bool force = false, который управляет принудительной заменой уже воспроизводящейся анимации. Если Force = true означает «обязательно воспроизвести», логично передавать это и в сам плеер:

♻️ Предлагаемое изменение
-        _whiteAnimationPlayer.Play(ent, ent.Comp.Animation);
+        _whiteAnimationPlayer.Play(ent, ent.Comp.Animation, ent.Comp.Force);
Content.Server/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs (1)

12-16: Нет проверки валидности uid перед отправкой сетевого сообщения.

Методы Play и Stop вызывают GetNetEntity(uid) и отправляют сетевое событие без проверки, что uid — валидная сущность. Если передан невалидный EntityUid, сервер всё равно отправит сообщение всем клиентам в PVS, хотя клиент его потом проигнорирует. Рекомендуется добавить ранний выход для минимизации лишнего сетевого трафика.

♻️ Пример для основного перегруза Play
 public override void Play(EntityUid uid, ProtoId<AnimationPrototype> animationId, bool force = false)
 {
+    if (!uid.Valid)
+        return;
+
     var filter = Filter.Pvs(uid, entityManager: EntityManager);
     RaiseNetworkEvent(new PlayAnimationMessage(GetNetEntity(uid), animationId, force), filter);
 }

Аналогичную проверку стоит добавить и в остальные перегрузы Play/Stop.

Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs (1)

25-44: Нет проверки существования сущности после разрешения NetEntity.

_entityManager.GetEntity(netEntity) может вернуть EntityUid, который уже удалён или невалиден. Стоит добавить проверку существования перед вызовом Play, чтобы администратор получил понятную ошибку.

♻️ Предлагаемое исправление
         entity = _entityManager.GetEntity(netEntity);
+
+        if (!_entityManager.EntityExists(entity))
+        {
+            shell.WriteError(Loc.GetString("shell-invalid-entity-id"));
+            return;
+        }
     }
Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs (1)

202-216: PlayClient и PlayPredicted на клиенте идентичны.

Оба метода проверяют IsFirstTimePredicted и затем вызывают Play(uid, animationId, recipient, force). Разницы в поведении между ними на клиенте нет. Если это осознанное решение (разница только на сервере), стоит оставить комментарий для ясности. Если предполагается различное поведение — нужно доработать.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs`:
- Around line 170-184: In WhiteAnimationPlayerSystem.Play, avoid the direct cast
(Animation) animation.Animation which can throw if animation.Animation is null;
instead retrieve animation.Animation into a local (e.g. var animObj =
animation.Animation), perform a null check (or use as Animation and check
result) and return (optionally log a warning) if it's null before calling
_animationPlayer.Play; ensure you reference Play in WhiteAnimationPlayerSystem
and the animation variable/animation.Animation when adding the guard.
🧹 Nitpick comments (4)
Content.Shared/_White/Animations/Systems/SharedWhiteAnimationPlayerSystem.cs (2)

86-99: Поля сообщений — публичные мутабельные поля вместо свойств.

PlayAnimationMessage и StopAnimationMessage используют публичные поля (public NetEntity AnimatedEntity = ...). Для сериализуемых сетевых сообщений обычно предпочтительнее использовать свойства с { get; } или хотя бы { get; set; }, чтобы предотвратить случайную мутацию после создания.

♻️ Предлагаемое исправление
 public sealed class PlayAnimationMessage(NetEntity animatedEntity, string animationId, bool force) : EntityEventArgs
 {
-    public NetEntity AnimatedEntity = animatedEntity;
-    public string AnimationId = animationId;
-    public bool Force = force;
+    public NetEntity AnimatedEntity { get; } = animatedEntity;
+    public string AnimationId { get; } = animationId;
+    public bool Force { get; } = force;
 }

 public sealed class StopAnimationMessage(NetEntity animatedEntity, string animationKey) : EntityEventArgs
 {
-    public NetEntity AnimatedEntity = animatedEntity;
-    public string AnimationKey = animationKey;
+    public NetEntity AnimatedEntity { get; } = animatedEntity;
+    public string AnimationKey { get; } = animationKey;
 }

20-28: Мелкие грамматические ошибки в XML-документации.

В строках 21, 26, 56 и 61 написано "that play animation" / "that stop animation" — должно быть "that plays animation" / "that stops animation" (третье лицо единственного числа).

Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs (2)

65-101: Мутация объекта прототипа — архитектурный антипаттерн.

Строка 97 (animationPrototype.Animation = animation) записывает вычисленное значение прямо в объект прототипа. Прототипы обычно считаются immutable и принадлежат IPrototypeManager. Мутация прототипа:

  1. Нарушает принцип неизменяемости прототипов и может вызвать неожиданное поведение при hot-reload (старое значение Animation живёт до следующего CachePrototypes).
  2. Создаёт неявную связь — другие системы, читающие AnimationPrototype.Animation, зависят от того, что клиентская система уже инициализировалась и закэшировала данные.

Рекомендуется хранить вычисленные Animation в отдельном словаре внутри системы (например, FrozenDictionary<string, Animation>), а не на самом прототипе.

♻️ Эскиз предлагаемого рефакторинга
- private FrozenDictionary<string, AnimationPrototype> _animations = default!;
+ private FrozenDictionary<string, (AnimationPrototype Proto, Animation Anim)> _animations = default!;

  // В CachePrototypes:
- animationPrototype.Animation = animation;
+ // не мутируем прототип
  ...
- _animations = animationPrototypes.ToFrozenDictionary(x => x.ID);
+ _animations = animationPrototypes.ToFrozenDictionary(
+     x => x.ID,
+     x => (x, /* соответствующая animation */));

  // В Play:
- _animationPlayer.Play(uid, (Animation) animation.Animation, animation.Key);
+ _animationPlayer.Play(uid, cachedAnim, animation.Key);

106-134: GetComponentProperty аллоцирует компонент для определения типа свойства.

Строка 117: _component.GetComponent(registration) создаёт экземпляр компонента только для того, чтобы получить тип свойства через рефлексию. Это происходит при кэшировании, так что не критично для рантайма, но стоит отметить — при большом количестве прототипов это может замедлить инициализацию.

@RedFoxIV RedFoxIV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не вижу смысла в обилии методов Play и почему нельзя сделать получение объекта типа Animation через метод в системе, а не какой-то непонятный костыль в прототипе.

Comment on lines +16 to +23
private void OnStartup(Entity<AnimateOnStartupComponent> ent, ref ComponentStartup args)
{
if (ent.Comp is { Played: true, Force: false, })
return;

_whiteAnimationPlayer.Play(ent, ent.Comp.Animation);
ent.Comp.Played = true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

анимация, которая должна играть непосредственно после создания энтити, будет играть сразу после первого попадания в ПВС клиента
устранение этого недочёта потребует либо проигрывание анимации по отдельному сообщению с сервера, либо сверяться с временем создания энтити (что то, что это - хуита)

в целом на это можно пока что забить, но лучше иметь это в виду

Comment thread Content.Shared/_White/Animations/Prototypes/AnimationPrototype.cs Outdated
Comment on lines +12 to +18
/// <summary>
/// Play animation on entity for every player in pvs range.
/// </summary>
/// <param name="uid">The UID of the entity.</param>
/// <param name="animationId">The ID of the animation that will be played.</param>
/// <param name="force">Determines whether this animation should play if an animation with the same key is already playing.</param>
public abstract void Play(EntityUid uid, ProtoId<AnimationPrototype> animationId, bool force = false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

большая часть кода с шареда захочет вызывать PlayClient, потому что она будет исполняться у всех на клиенте, а Play/PlayPredicted будут вызывать нежелательный повтор анимации в её начале

код, вызываемый на шареде, априори вызывается на всех клиентах, на которых он должен вызываться - дополнительный синхрон через сервер не нужен. Либо анимация запускается сразу на клиенте, либо сообщение о ней приходит с сервера. Всё.

Затрудняюсь в целом подобрать применение серверным методам по проигрыванию анимаций.

Нужно два метода:

  1. метод, который на клиенте играет анимацию на указанной энтити и ничего не делает на сервере
  2. метод, который на клиенте ничего не делает и на сервере отправляет всем клиентам, в чей ПВС попадает указанная энтити, сообщение об анимации.

@Spatison Spatison Feb 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не весь код в шараде априори вызывается на вcех клиентах на которых надо. Условно при ударе по объекту ивент MeleeHitEvent будет вызван только на клиенте того кто ударил. Все остальные могут получить его только через сервер.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как может пригодится 2-ой метод? Если тебе нужно проиграть что то только на сервере, то скорее всего и код у тебя находится на серверной части и тебе ничего не мешает использовать Play. В противном случае какая может быть причина не давать клиентам предугадывать проигрывание анимации?

Comment on lines +25 to +52
keyFrames:
- !type:KeyFramePropertyData
value: 0
keyframe: 0
- !type:KeyFramePropertyData
value: 90
keyframe: 0.075
- !type:KeyFramePropertyData
value: 180
keyframe: 0.075
- !type:KeyFramePropertyData
value: 270
keyframe: 0.075
- !type:KeyFramePropertyData
value: 0
keyframe: 0.075
- !type:KeyFramePropertyData
value: 90
keyframe: 0.075
- !type:KeyFramePropertyData
value: 180
keyframe: 0.075
- !type:KeyFramePropertyData
value: 270
keyframe: 0.075
- !type:KeyFramePropertyData
value: 0
keyframe: 0.075

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

все кейфреймы на 0.075 секунд
Если это относительно предыдущего кейфрейма - мб переименовать?

@Spatison Spatison Feb 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А смысл?

Comment thread Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs`:
- Around line 202-208: The null-check for _player.LocalSession is missing in the
Play(EntityUid uid, ProtoId<AnimationPrototype> animationId, Filter filter, bool
force = false) and Stop(EntityUid uid, ProtoId<AnimationPrototype> animationId,
Filter filter) methods: before calling
filter.Recipients.Contains(_player.LocalSession) add a guard to return early (or
skip the Contains call) when _player.LocalSession is null to avoid a possible
NRE; update both Play and Stop to check _player.LocalSession == null and only
call filter.Recipients.Contains(...) when it is non-null.
- Around line 210-211: PlayClient(EntityUid uid, ProtoId<AnimationPrototype>
animationId, bool force = false) is missing the prediction guard and can replay
animations during replays; add the same check used in the PlayClient overload
with recipient and in PlayPredicted: call _gameTiming.IsFirstTimePredicted and
return early (do not call Play) when it is false so the zero-recipient
PlayClient respects prediction and avoids duplicate plays.
- Around line 229-230: The Stop override should check HasRunningAnimation(uid,
animationkey) before calling _animationPlayer.Stop to match the existing pattern
used in Play (see Play at line ~175); modify the method so it only calls
_animationPlayer.Stop(uid, animationkey) when HasRunningAnimation(uid,
animationkey) returns true, preserving the current method signature and behavior
otherwise.

In `@Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs`:
- Around line 18-26: The error message is incorrect when
shell.Player?.AttachedEntity is null: change the logic in the
PlayAnimationCommand handling (the args.Length == 1 branch that checks
shell.Player?.AttachedEntity) to log a clear "no attached entity / command must
be run by a player with an attached entity" message instead of using the
shell-wrong-arguments-number key; update the Loc.GetString key (e.g.,
shell-no-attached-entity) and call
shell.WriteError(Loc.GetString("shell-no-attached-entity")) (optionally still
call shell.WriteLine(Help)) so the error accurately reflects the missing
attached entity rather than a wrong argument count.
- Around line 40-41: The entity resolved via _entityManager.GetEntity(netEntity)
can be EntityUid.Invalid; before calling
_entityManager.System<WhiteAnimationPlayerSystem>().Play(entity, args[0]) check
that the resolved entity is valid (e.g. compare against EntityUid.Invalid or use
_entityManager.Exists(entity)) and bail out (or log) when invalid to avoid
sending a Play for a non-existent entity; update the code around the entity
variable and the Play call accordingly.
🧹 Nitpick comments (2)
Content.Server/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs (1)

48-48: Несогласованное именование параметра: animationkeyanimationKey.

В абстрактном классе SharedWhiteAnimationPlayerSystem параметр назван animationKey (camelCase), а здесь — animationkey. Хотя C# допускает это при переопределении, стоит привести к единому стилю.

♻️ Предлагаемое исправление
-    public override void Stop(EntityUid uid, string animationkey)
+    public override void Stop(EntityUid uid, string animationKey)
     {
         var filter = Filter.Pvs(uid, entityManager: EntityManager);
-        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationkey), filter);
+        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationKey), filter);
     }

-    public override void Stop(EntityUid uid, string animationkey, EntityUid recipient)
+    public override void Stop(EntityUid uid, string animationKey, EntityUid recipient)
     {
         if (!TryComp<ActorComponent>(recipient, out var actor))
             return;

-        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationkey), actor.PlayerSession);
+        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationKey), actor.PlayerSession);
     }

-    public override void Stop(EntityUid uid, string animationkey, ICommonSession recipient) =>
-        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationkey), recipient);
+    public override void Stop(EntityUid uid, string animationKey, ICommonSession recipient) =>
+        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationKey), recipient);

-    public override void Stop(EntityUid uid, string animationkey, Filter filter) =>
-        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationkey), filter);
+    public override void Stop(EntityUid uid, string animationKey, Filter filter) =>
+        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationKey), filter);

-    public override void StopClient(EntityUid uid, string animationkey, EntityUid recipient)
+    public override void StopClient(EntityUid uid, string animationKey, EntityUid recipient)
     {
         // This is for the client
     }

-    public override void StopPredicted(EntityUid uid, string animationkey, EntityUid recipient)
+    public override void StopPredicted(EntityUid uid, string animationKey, EntityUid recipient)
     {
         var filter = Filter.PvsExcept(recipient, entityManager: EntityManager);
-        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationkey), filter);
+        RaiseNetworkEvent(new StopAnimationMessage(GetNetEntity(uid), animationKey), filter);
     }

Also applies to: 54-54, 62-62, 65-65, 68-68, 73-73

Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs (1)

136-148: GetPlaySound и GetSpriteFlick могут вернуть трек без ключевых кадров.

Если ни один keyFrame не соответствует ожидаемому типу (KeyFrameSoundData/KeyFrameSpriteFlickData), метод вернёт трек с пустым списком KeyFrames. В отличие от GetComponentProperty, который возвращает null при проблемах, эти методы всегда возвращают объект, который затем добавляется в анимацию. Стоит рассмотреть возврат null при пустых KeyFrames или хотя бы логирование предупреждения.

Also applies to: 150-162

Comment thread Content.Client/_White/Animations/Systems/WhiteAnimationPlayerSystem.cs Outdated
Comment thread Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs
Comment thread Content.Server/_White/Animations/Commands/PlayAnimationCommand.cs

@RedFoxIV RedFoxIV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm за исключением object? Animation в прототипе, см. комментарий из предыдущего ревью

@DVOniksWyvern
DVOniksWyvern merged commit 8e5d669 into WWhiteDreamProject:master Feb 13, 2026
11 of 12 checks passed
nekosich pushed a commit to nekosich/wwdpublic-for-ports that referenced this pull request Feb 15, 2026
@Spatison
Spatison deleted the animations branch February 23, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants