diff --git a/HarmonySrc/HandPosePose.cs b/HarmonySrc/HandPosePose.cs
index 3f0f874..07ee59c 100644
--- a/HarmonySrc/HandPosePose.cs
+++ b/HarmonySrc/HandPosePose.cs
@@ -448,6 +448,7 @@ namespace NecromancerTome
HandPose.Apply();
HeldTweak.Apply();
NecroView.Update();
+ NecroBody.LateUpdate();
}
}
diff --git a/HarmonySrc/NecroBody.cs b/HarmonySrc/NecroBody.cs
new file mode 100644
index 0000000..1687c43
--- /dev/null
+++ b/HarmonySrc/NecroBody.cs
@@ -0,0 +1,450 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using HarmonyLib;
+using UnityEngine;
+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;
+
+ 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()
+ {
+ 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;
+ }
+ 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]|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 "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);
+ }
+ }
+ }
+}