Code/HuesosManipulados.cs

Component that applies per-bone manual transforms (rotation/position/scale) on top of the evaluated animation for a SkinnedModelRenderer. Stores a dictionary of adjustments by bone name, applies them each frame by reading the animated bone pose and setting SceneModel bone overrides, and clears overrides when disabled, empty, or when the GameObject has the 'sbh_muerto' tag.

Native Interop
namespace SBH;

using Sandbox;
using System.Collections.Generic;

/// <summary>
/// [SBHBase] Huesos manipulados a mano (los ManipulateBone* de GMod, para el
/// Advanced Bone Tool): guarda un ajuste por hueso (ángulos/posición/escala,
/// offsets en el MARCO LOCAL del hueso) y lo aplica cada frame POR ENCIMA de
/// la animación ya evaluada — patrón BaileProcedural: TryGetBoneTransformAnimation
/// lee la pose SIN overrides (sin retroalimentación) y SetBoneOverride pisa.
/// Con la lista vacía no toca nada (la animación queda pura). En un bot
/// muerto (tag sbh_muerto) se apaga: el ragdoll manda.
/// OJO: comparte SceneModel.ClearBoneOverrides con otros drivers de huesos
/// (ej. BaileProcedural) — dos drivers vivos en el mismo bot se pisan entre sí.
/// </summary>
public sealed class HuesosManipulados : Component
{
	public struct Ajuste
	{
		public Angles Angulos;
		public Vector3 Posicion;
		public float Escala;

		public static readonly Ajuste Identidad = new() { Escala = 1f };

		public bool EsIdentidad =>
			Angulos == Angles.Zero && Posicion == Vector3.Zero && Escala == 1f;
	}

	readonly Dictionary<string, Ajuste> _ajustes = new();
	SkinnedModelRenderer _renderer;
	bool _aplicado;

	public IReadOnlyDictionary<string, Ajuste> Ajustes => _ajustes;

	public Ajuste De( string hueso )
		=> hueso != null && _ajustes.TryGetValue( hueso, out var a ) ? a : Ajuste.Identidad;

	public void Fijar( string hueso, Ajuste ajuste )
	{
		if ( string.IsNullOrEmpty( hueso ) ) return;

		if ( ajuste.EsIdentidad )
			_ajustes.Remove( hueso );
		else
			_ajustes[hueso] = ajuste;
	}

	public void Reiniciar() => _ajustes.Clear();

	protected override void OnEnabled()
	{
		_renderer = Components.Get<SkinnedModelRenderer>();
	}

	protected override void OnDisabled() => Limpiar();

	protected override void OnUpdate()
	{
		if ( !_renderer.IsValid() || _renderer.SceneModel == null ) return;

		if ( _ajustes.Count == 0 || GameObject.Tags.Has( "sbh_muerto" ) )
		{
			Limpiar();
			return;
		}

		var sm = _renderer.SceneModel;
		sm.ClearBoneOverrides();

		foreach ( var (nombre, ajuste) in _ajustes )
		{
			var hueso = _renderer.Model?.Bones?.GetBone( nombre );
			if ( hueso == null ) continue;

			// Pose de la ANIMACIÓN (mundo), sin nuestros overrides
			if ( !_renderer.TryGetBoneTransformAnimation( hueso, out var mundo ) ) continue;

			var local = sm.Transform.ToLocal( mundo );

			// Offsets en el marco local del hueso (como ManipulateBone* de GMod)
			local.Position += local.Rotation * ajuste.Posicion;
			local.Rotation *= Rotation.From( ajuste.Angulos );
			local.Scale *= ajuste.Escala;

			sm.SetBoneOverride( hueso.Index, local );
		}

		_aplicado = true;
	}

	void Limpiar()
	{
		if ( !_aplicado ) return;
		_aplicado = false;

		if ( _renderer.IsValid() && _renderer.SceneModel != null )
			_renderer.SceneModel.ClearBoneOverrides();
	}
}