using System.Collections.Generic; using System.Globalization; using System.Text; using HarmonyLib; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Правка аниматора ТЕЛА игрока (вид от третьего лица, его же видят другие) прямо в игре (16.10: «Добавь все необходимые /// команды для аниматора тела, чтобы можно было править прямо в игре»). Пара к necropose/necrobones, которые - для рук /// первого лица. Всё держится до necrobody off или выхода из мира; итог переносится в код (necrobody save). /// Параметры ставятся после Update аватара (иначе игра перезапишет их до расчёта аниматора), кости - в LateUpdate. /// public static class NecroBody { /// Параметры аниматора тела, которые держим своими (имя -> значение; bool - 0/1). public static readonly Dictionary Params = new Dictionary(); /// Локальные повороты костей тела поверх анимации. public static readonly Dictionary Pose = new Dictionary(); public static bool Freeze; public static float Speed = 1f; /// Место предмета в руке тела (поверх смещения игры по хвату), если задано. public static Vector3? ItemPos, ItemRot; static Animator _anim; static readonly Dictionary _bones = new Dictionary(); static readonly List _frozen = new List(); static readonly List _frozenPos = new List(); static readonly List _frozenRot = new List(); static int _playerId = -1; /// Любая загруженная анимация на теле (16.10: «Может и танцевать сможешь заставить?.. сильно бы помогло в /// создании контента»): клип играется поверх контроллера через Playables, по кругу. Танцы есть у зомби (twitch_dance) - /// их клипы в памяти, пока в мире есть зомби с таким аниматором. static PlayableGraph _graph; static AnimationClipPlayable _clip; public static string Playing; public static List FindClips(string _filter) { var list = new List(); var seen = new HashSet(); foreach (AnimationClip c in Resources.FindObjectsOfTypeAll()) { if (c != null && (_filter.Length == 0 || c.name.IndexOf(_filter, System.StringComparison.OrdinalIgnoreCase) >= 0) && seen.Add(c.name)) { list.Add(c); } } list.Sort((x, y) => string.CompareOrdinal(x.name, y.name)); return list; } public static string Play(string _name, float _speed) { Animator a = Anim(); if (a == null) { return "нет аниматора тела"; } AnimationClip clip = null; foreach (AnimationClip c in FindClips(_name)) { if (c.name == _name) { clip = c; break; } clip = clip ?? c; } if (clip == null) { return "нет загруженной анимации «" + _name + "» (necrobody anims слово)"; } StopPlay(); _graph = PlayableGraph.Create("NecroBodyPlay"); _graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime); var output = AnimationPlayableOutput.Create(_graph, "body", a); _clip = AnimationClipPlayable.Create(_graph, clip); _clip.SetSpeed(_speed); output.SetSourcePlayable(_clip); _graph.Play(); Playing = clip.name; return "играет " + clip.name + " (" + NecroBody.F(clip.length) + " с" + (clip.isLooping ? ", петля" : "") + (clip.humanMotion ? ", гуманоид" : ", не гуманоид - может лечь криво") + "), скорость " + NecroBody.F(_speed); } /// Тело повторяет движения другого существа (16.10: танец зомби не лёг на тело игрока через Playables - клип /// гуманоидный, а тело игрока - нет). Каждый кадр локальные повороты костей источника копируются на кости тела с теми же /// именами (Hips, Spine, LeftArm...); у бёдер - ещё и сдвиг. Источник - зомби, которого necrobody zdance заставляет танцевать. static EntityAlive _mimic; static readonly List _mimicSrc = new List(); static readonly List _mimicDst = new List(); public static EntityAlive Nearest(float _radius, bool _zombiesOnly) { EntityPlayerLocal pl = Player(); if (pl == null) { return null; } EntityAlive best = null; float bestD = _radius * _radius; foreach (Entity e in GameManager.Instance.World.Entities.list) { if (e is EntityAlive a && a != pl && !a.IsDead() && (!_zombiesOnly || a is EntityZombie)) { float d = (a.position - pl.position).sqrMagnitude; if (d < bestD) { bestD = d; best = a; } } } return best; } static Transform ModelRoot(EntityAlive _e) { if (_e == null || _e.emodel == null) { return null; } if (_e.emodel.avatarController is AvatarCharacterController ac && ac.CharacterBody?.Animator != null) { return ac.CharacterBody.Animator.transform; } return _e.emodel.GetModelTransform(); } public static string Mimic(EntityAlive _src) { StopMimic(); Animator a = Anim(); Transform src = ModelRoot(_src); if (a == null || src == null) { return "нет тела игрока или источника"; } var byName = new Dictionary(); foreach (Transform t in src.GetComponentsInChildren(true)) { if (!byName.ContainsKey(t.name)) { byName[t.name] = t; } } foreach (Transform t in a.GetComponentsInChildren(true)) { if (t != a.transform && byName.TryGetValue(t.name, out Transform s2) && s2 != src) { _mimicDst.Add(t); _mimicSrc.Add(s2); } } _mimic = _src; return "тело повторяет за " + _src.EntityName + " (id " + _src.entityId + "): общих костей " + _mimicDst.Count; } public static void StopMimic() { _mimic = null; _mimicSrc.Clear(); _mimicDst.Clear(); } static void ApplyMimic() { if (_mimic == null) { return; } if (_mimic.IsDead() || _mimicSrc.Count == 0 || _mimicSrc[0] == null) { StopMimic(); return; } for (int i = 0; i < _mimicDst.Count; i++) { Transform s = _mimicSrc[i], d = _mimicDst[i]; if (s == null || d == null) { continue; } d.localRotation = s.localRotation; if (d.name == "Hips") { d.localPosition = s.localPosition; } } } public static void StopPlay() { if (_graph.IsValid()) { _graph.Destroy(); } Playing = null; } /// Петля для клипов без своей петли. static void LoopPlay() { if (!_graph.IsValid()) { return; } AnimationClip c = _clip.GetAnimationClip(); if (c != null && !c.isLooping && _clip.GetTime() >= c.length) { _clip.SetTime(0); } } public static EntityPlayerLocal Player() => GameManager.Instance?.World?.GetPrimaryPlayer(); public static BodyAnimator Body() { return (Player()?.emodel?.avatarController as AvatarCharacterController)?.CharacterBody; } public static Animator Anim() => Body()?.Animator; public static bool Active => Params.Count > 0 || Pose.Count > 0 || Freeze || Speed != 1f || ItemPos.HasValue || ItemRot.HasValue; public static Transform Bone(string _name) { Animator a = Anim(); if (a == null) { return null; } if (a != _anim) { _anim = a; _bones.Clear(); _frozen.Clear(); foreach (Transform t in a.GetComponentsInChildren(true)) { if (!_bones.ContainsKey(t.name)) { _bones[t.name] = t; } } } return _bones.TryGetValue(_name, out Transform b) ? b : null; } public static void Reset() { StopPlay(); StopMimic(); Params.Clear(); Pose.Clear(); Freeze = false; _frozen.Clear(); ItemPos = null; ItemRot = null; if (Speed != 1f) { Speed = 1f; Animator a = Anim(); if (a != null) { a.speed = 1f; } } } public static void Unfreeze() { _frozen.Clear(); _frozenPos.Clear(); _frozenRot.Clear(); } /// Параметр по имени: тип берётся из аниматора. false - нет такого. public static bool SetParam(Animator _a, string _name, float _v) { foreach (AnimatorControllerParameter p in _a.parameters) { if (p.name != _name) { continue; } switch (p.type) { case AnimatorControllerParameterType.Float: _a.SetFloat(p.nameHash, _v); break; case AnimatorControllerParameterType.Int: _a.SetInteger(p.nameHash, (int)_v); break; case AnimatorControllerParameterType.Bool: _a.SetBool(p.nameHash, _v != 0f); break; case AnimatorControllerParameterType.Trigger: _a.SetTrigger(p.nameHash); break; } return true; } return false; } /// После Update аватара (заплатка ниже) - до расчёта аниматора в этом кадре. public static void ApplyParams() { if (Params.Count == 0 && Speed == 1f) { return; } Animator a = Anim(); if (a == null) { return; } foreach (KeyValuePair kv in Params) { SetParam(a, kv.Key, kv.Value); } a.speed = Speed; } /// Из HandPoseDriver.LateUpdate. public static void LateUpdate() { EntityPlayerLocal pl = Player(); int id = pl != null ? pl.entityId : -1; if (id != _playerId) { _playerId = id; Reset(); _anim = null; _bones.Clear(); return; } LoopPlay(); ApplyMimic(); if (!Active || Bone("") == null && _anim == null) { return; } if (Freeze) { if (_frozen.Count == 0 || _frozen[0] == null) { Unfreeze(); foreach (Transform t in _anim.GetComponentsInChildren(true)) { _frozen.Add(t); _frozenPos.Add(t.localPosition); _frozenRot.Add(t.localRotation); } Debug.Log("[NecromancerTome] necrobody: заморожено костей " + _frozen.Count); } else { for (int i = 0; i < _frozen.Count; i++) { if (_frozen[i] != null) { _frozen[i].localPosition = _frozenPos[i]; _frozen[i].localRotation = _frozenRot[i]; } } } } foreach (KeyValuePair kv in Pose) { Transform b = Bone(kv.Key); if (b != null) { b.localRotation = Quaternion.Euler(kv.Value); } } if ((ItemPos.HasValue || ItemRot.HasValue) && pl.emodel != null && !pl.emodel.IsFPV) { Transform item = (pl.emodel.avatarController as AvatarMultiBodyController)?.heldItemTransform; if (item != null) { if (ItemPos.HasValue) { item.localPosition = ItemPos.Value; } if (ItemRot.HasValue) { item.localEulerAngles = ItemRot.Value; } } } } public static string F(float _v) => _v.ToString("0.###", CultureInfo.InvariantCulture); public static string F(Vector3 _v) => F(_v.x) + " " + F(_v.y) + " " + F(_v.z); } [HarmonyPatch(typeof(AvatarMultiBodyController), "Update")] public static class Patch_AvatarMultiBodyController_Update_NecroBody { public static void Postfix(AvatarMultiBodyController __instance) { if (NecroBody.Params.Count > 0 || NecroBody.Speed != 1f) { EntityPlayerLocal pl = NecroBody.Player(); if (pl != null && __instance.Entity == pl) { NecroBody.ApplyParams(); } } } } /// /// necrobody - аниматор тела игрока (вид от третьего лица): /// params [фильтр] - параметры аниматора и значения /// clips - что играет в каждом слое (клип, вес слоя) /// set <параметр> <число> - держать значение (bool 0/1); unset <параметр>; trigger <параметр> /// speed <k> - скорость аниматора (0 - пауза) /// freeze [off] - замереть в текущем кадре (все кости) /// bones [фильтр] - кости тела с локальными поворотами в лог /// show <кость>; pose <кость> x y z; add <кость> dx dy dz; unpose <кость> /// item pos x y z (см) | rot x y z | off | show - место предмета в руке тела /// holdoffset [N] - смещение игры для хвата N (по умолчанию - предмета в руке) /// save - всё заданное строками в лог; off - всё снять /// [Preserve] public class ConsoleCmdNecroBody : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necrobody" }; public override string getDescription() => "NecromancerTome: аниматор тела - params [ф]|clips|set п v|unset п|trigger п|speed k|freeze [off]|bones [ф]|show к|pose к x y z|add к dx dy dz|unpose к|item pos|rot|off|show|holdoffset [N]|anims [слово]|play имя [скорость]|play off|zdance [off]|mimic [id]|mimic off|save|off"; static bool Num(List _p, int _i, out float _v) { _v = 0f; return _p.Count > _i && float.TryParse(_p[_i].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out _v); } static bool Vec(List _p, int _i, out Vector3 _v) { _v = Vector3.zero; if (Num(_p, _i, out float x) && Num(_p, _i + 1, out float y) && Num(_p, _i + 2, out float z)) { _v = new Vector3(x, y, z); return true; } return false; } static void Out(string _s) { SdtdConsole.Instance.Output(_s); foreach (string line in _s.Split('\n')) { Debug.Log("[NecromancerTome] necrobody: " + line); } } public override void Execute(List _params, CommandSenderInfo _senderInfo) { Animator a = NecroBody.Anim(); if (a == null) { Out("нет аниматора тела (игрок не в мире?)"); return; } string cmd = _params.Count > 0 ? _params[0] : ""; string arg = _params.Count > 1 ? _params[1] : ""; switch (cmd) { case "params": { var sb = new StringBuilder("аниматор " + a.name + ", контроллер " + (a.runtimeAnimatorController != null ? a.runtimeAnimatorController.name : "нет")); foreach (AnimatorControllerParameter p in a.parameters) { if (arg.Length > 0 && p.name.IndexOf(arg, System.StringComparison.OrdinalIgnoreCase) < 0) { continue; } string v = p.type == AnimatorControllerParameterType.Float ? NecroBody.F(a.GetFloat(p.nameHash)) : p.type == AnimatorControllerParameterType.Int ? a.GetInteger(p.nameHash).ToString() : p.type == AnimatorControllerParameterType.Bool ? (a.GetBool(p.nameHash) ? "1" : "0") : "trigger"; sb.Append("\n ").Append(p.name).Append(" (").Append(p.type).Append(") = ").Append(v) .Append(NecroBody.Params.ContainsKey(p.name) ? " [держим]" : ""); } Out(sb.ToString()); return; } case "clips": { var sb = new StringBuilder("слоёв " + a.layerCount); for (int l = 0; l < a.layerCount; l++) { string names = ""; foreach (AnimatorClipInfo c in a.GetCurrentAnimatorClipInfo(l)) { names += (names.Length > 0 ? ", " : "") + (c.clip != null ? c.clip.name : "?") + " x" + c.weight.ToString("0.##"); } sb.Append("\n ").Append(l).Append(' ').Append(a.GetLayerName(l)).Append(" вес ").Append(a.GetLayerWeight(l).ToString("0.##")).Append(": ").Append(names); } Out(sb.ToString()); return; } case "set" when Num(_params, 2, out float v): if (!NecroBody.SetParam(a, arg, v)) { Out("нет параметра " + arg + " (necrobody params)"); return; } NecroBody.Params[arg] = v; Out(arg + " = " + NecroBody.F(v) + " (держим; unset - отпустить)"); return; case "unset": Out(NecroBody.Params.Remove(arg) ? arg + " отпущен" : arg + " не был задан"); return; case "trigger": Out(NecroBody.SetParam(a, arg, 1f) ? "сработал " + arg : "нет параметра " + arg); return; case "speed" when Num(_params, 1, out float k): NecroBody.Speed = k; a.speed = k; Out("скорость аниматора тела " + NecroBody.F(k)); return; case "freeze": NecroBody.Freeze = arg != "off"; NecroBody.Unfreeze(); Out("заморозка тела " + (NecroBody.Freeze ? "включена (снимок - в этом кадре)" : "выключена")); return; case "bones": { var sb = new StringBuilder(); int n = 0; Dump(a.transform, 0, arg, sb, ref n); Out("костей " + n + (arg.Length > 0 ? " (фильтр " + arg + ")" : "") + sb); return; } case "show": { Transform b = NecroBody.Bone(arg); Out(b == null ? "нет кости " + arg : arg + " = " + NecroBody.F(b.localEulerAngles) + (NecroBody.Pose.ContainsKey(arg) ? " (задана)" : " (от аниматора)") + ", поз. " + NecroBody.F(b.localPosition)); return; } case "pose" when Vec(_params, 2, out Vector3 r): if (NecroBody.Bone(arg) == null) { Out("нет кости " + arg); return; } NecroBody.Pose[arg] = r; Out(arg + " = " + NecroBody.F(r)); return; case "add" when Vec(_params, 2, out Vector3 d): { Transform b = NecroBody.Bone(arg); if (b == null) { Out("нет кости " + arg); return; } Vector3 cur = NecroBody.Pose.TryGetValue(arg, out Vector3 was) ? was : b.localEulerAngles; NecroBody.Pose[arg] = cur + d; Out(arg + " = " + NecroBody.F(NecroBody.Pose[arg])); return; } case "unpose": Out(NecroBody.Pose.Remove(arg) ? arg + " отпущена" : arg + " не была задана"); return; case "item": { Transform it = (NecroBody.Player()?.emodel?.avatarController as AvatarMultiBodyController)?.heldItemTransform; if (arg == "pos" && Vec(_params, 2, out Vector3 p)) { NecroBody.ItemPos = p / 100f; } else if (arg == "rot" && Vec(_params, 2, out Vector3 r2)) { NecroBody.ItemRot = r2; } else if (arg == "off") { NecroBody.ItemPos = null; NecroBody.ItemRot = null; Out("место предмета - как у игры (при следующем доставании)"); return; } Out("предмет в руке тела " + (it != null ? it.name + ": поз. (см) " + NecroBody.F(it.localPosition * 100f) + ", пов. " + NecroBody.F(it.localEulerAngles) : "нет") + (NecroBody.ItemPos.HasValue || NecroBody.ItemRot.HasValue ? " [держим]" : "")); return; } case "holdoffset": { int h = Num(_params, 1, out float hv) ? (int)hv : NecroBody.Player()?.inventory?.holdingItem?.HoldType.Value ?? 0; var o = AnimationGunjointOffsetData.AnimationGunjointOffset[h]; Out("хват " + h + ": смещение игры поз. (см) " + NecroBody.F(o.position * 100f) + ", пов. " + NecroBody.F(o.rotation)); return; } case "save": { var sb = new StringBuilder("заданное:"); foreach (var kv in NecroBody.Params) { sb.Append("\n param ").Append(kv.Key).Append(" = ").Append(NecroBody.F(kv.Value)); } foreach (var kv in NecroBody.Pose) { sb.Append("\n Pose[\"").Append(kv.Key).Append("\"] = new Vector3(").Append(NecroBody.F(kv.Value.x)).Append("f, ") .Append(NecroBody.F(kv.Value.y)).Append("f, ").Append(NecroBody.F(kv.Value.z)).Append("f);"); } if (NecroBody.ItemPos.HasValue) { sb.Append("\n item pos (см) ").Append(NecroBody.F(NecroBody.ItemPos.Value * 100f)); } if (NecroBody.ItemRot.HasValue) { sb.Append("\n item rot ").Append(NecroBody.F(NecroBody.ItemRot.Value)); } sb.Append("\n speed ").Append(NecroBody.F(NecroBody.Speed)).Append(", freeze ").Append(NecroBody.Freeze); Out(sb.ToString()); return; } case "anims": { List l = NecroBody.FindClips(arg); var sb = new StringBuilder("анимаций в памяти" + (arg.Length > 0 ? " со словом " + arg : "") + ": " + l.Count); int shown = 0; foreach (AnimationClip c in l) { if (shown++ >= 200) { sb.Append("\n ... (уточните слово)"); break; } sb.Append("\n ").Append(c.name).Append(" ").Append(NecroBody.F(c.length)).Append(" с").Append(c.humanMotion ? "" : " [не гуманоид]"); } Out(sb.ToString()); return; } case "zdance": { EntityAlive z = NecroBody.Nearest(30f, true); if (z == null) { Out("рядом (30 м) нет зомби"); return; } z.SetDancing(arg != "off"); Out(z.EntityName + " (id " + z.entityId + ") " + (arg != "off" ? "танцует" : "перестал танцевать")); return; } case "mimic" when arg == "off": NecroBody.StopMimic(); Out("тело больше не повторяет"); return; case "mimic": { EntityAlive src = null; if (Num(_params, 1, out float mid)) { src = GameManager.Instance.World.GetEntity((int)mid) as EntityAlive; } else { src = NecroBody.Nearest(30f, false); } Out(src == null ? "нет источника (id или существо в 30 м)" : NecroBody.Mimic(src)); return; } case "play" when arg == "off": NecroBody.StopPlay(); Out("анимация снята"); return; case "play" when arg.Length > 0: Out(NecroBody.Play(arg, Num(_params, 2, out float ps) ? ps : 1f)); return; case "off": NecroBody.Reset(); Out("тело - как у игры"); return; } Out(getDescription()); } static void Dump(Transform _t, int _depth, string _filter, StringBuilder _sb, ref int _n) { if (_filter.Length == 0 || _t.name.IndexOf(_filter, System.StringComparison.OrdinalIgnoreCase) >= 0) { _n++; _sb.Append('\n').Append(new string(' ', _depth * 2)).Append(_t.name).Append(_t.gameObject.activeSelf ? "" : " [выкл]") .Append(" пов. ").Append(NecroBody.F(_t.localEulerAngles)).Append(" поз. ").Append(NecroBody.F(_t.localPosition)); } for (int i = 0; i < _t.childCount; i++) { Dump(_t.GetChild(i), _depth + 1, _filter, _sb, ref _n); } } } }