Files
necromants-tome-7d2d-3-2/HarmonySrc/NecroBody.cs
T

557 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// Правка аниматора ТЕЛА игрока (вид от третьего лица, его же видят другие) прямо в игре (16.10: «Добавь все необходимые
/// команды для аниматора тела, чтобы можно было править прямо в игре»). Пара к necropose/necrobones, которые - для рук
/// первого лица. Всё держится до necrobody off или выхода из мира; итог переносится в код (necrobody save).
/// Параметры ставятся после Update аватара (иначе игра перезапишет их до расчёта аниматора), кости - в LateUpdate.
/// </summary>
public static class NecroBody
{
/// <summary>Параметры аниматора тела, которые держим своими (имя -> значение; bool - 0/1).</summary>
public static readonly Dictionary<string, float> Params = new Dictionary<string, float>();
/// <summary>Локальные повороты костей тела поверх анимации.</summary>
public static readonly Dictionary<string, Vector3> Pose = new Dictionary<string, Vector3>();
public static bool Freeze;
public static float Speed = 1f;
/// <summary>Место предмета в руке тела (поверх смещения игры по хвату), если задано.</summary>
public static Vector3? ItemPos, ItemRot;
static Animator _anim;
static readonly Dictionary<string, Transform> _bones = new Dictionary<string, Transform>();
static readonly List<Transform> _frozen = new List<Transform>();
static readonly List<Vector3> _frozenPos = new List<Vector3>();
static readonly List<Quaternion> _frozenRot = new List<Quaternion>();
static int _playerId = -1;
/// <summary>Любая загруженная анимация на теле (16.10: «Может и танцевать сможешь заставить?.. сильно бы помогло в
/// создании контента»): клип играется поверх контроллера через Playables, по кругу. Танцы есть у зомби (twitch_dance) -
/// их клипы в памяти, пока в мире есть зомби с таким аниматором.</summary>
static PlayableGraph _graph;
static AnimationClipPlayable _clip;
public static string Playing;
public static List<AnimationClip> FindClips(string _filter)
{
var list = new List<AnimationClip>();
var seen = new HashSet<string>();
foreach (AnimationClip c in Resources.FindObjectsOfTypeAll<AnimationClip>())
{
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);
}
public static void StopPlay()
{
if (_graph.IsValid())
{
_graph.Destroy();
}
Playing = null;
}
/// <summary>Петля для клипов без своей петли.</summary>
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<Transform>(true))
{
if (!_bones.ContainsKey(t.name))
{
_bones[t.name] = t;
}
}
}
return _bones.TryGetValue(_name, out Transform b) ? b : null;
}
public static void Reset()
{
StopPlay();
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();
}
/// <summary>Параметр по имени: тип берётся из аниматора. false - нет такого.</summary>
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;
}
/// <summary>После Update аватара (заплатка ниже) - до расчёта аниматора в этом кадре.</summary>
public static void ApplyParams()
{
if (Params.Count == 0 && Speed == 1f)
{
return;
}
Animator a = Anim();
if (a == null)
{
return;
}
foreach (KeyValuePair<string, float> kv in Params)
{
SetParam(a, kv.Key, kv.Value);
}
a.speed = Speed;
}
/// <summary>Из HandPoseDriver.LateUpdate.</summary>
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();
if (!Active || Bone("") == null && _anim == null)
{
return;
}
if (Freeze)
{
if (_frozen.Count == 0 || _frozen[0] == null)
{
Unfreeze();
foreach (Transform t in _anim.GetComponentsInChildren<Transform>(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<string, Vector3> 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();
}
}
}
}
/// <summary>
/// necrobody - аниматор тела игрока (вид от третьего лица):
/// params [фильтр] - параметры аниматора и значения
/// clips - что играет в каждом слое (клип, вес слоя)
/// set &lt;параметр&gt; &lt;число&gt; - держать значение (bool 0/1); unset &lt;параметр&gt;; trigger &lt;параметр&gt;
/// speed &lt;k&gt; - скорость аниматора (0 - пауза)
/// freeze [off] - замереть в текущем кадре (все кости)
/// bones [фильтр] - кости тела с локальными поворотами в лог
/// show &lt;кость&gt;; pose &lt;кость&gt; x y z; add &lt;кость&gt; dx dy dz; unpose &lt;кость&gt;
/// item pos x y z (см) | rot x y z | off | show - место предмета в руке тела
/// holdoffset [N] - смещение игры для хвата N (по умолчанию - предмета в руке)
/// save - всё заданное строками в лог; off - всё снять
/// </summary>
[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|save|off";
static bool Num(List<string> _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<string> _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<string> _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<AnimationClip> 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 "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);
}
}
}
}