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); + } + } + } +}