Editor/Effigy/VmdlAnimation.cs
using System.Text;
namespace Effigy;
/// <summary>
/// The AnimationList node a skinned .vmdl needs, and the bind pose inside it.
///
/// WHAT THIS CLOSES. ModelDoc's own documentation says a model that is not fully static needs at
/// least an `AnimBindPose` node or morph targets and IK data silently break — and this project's
/// skinned export has never had one, because nothing here had seen the node's real KV3 and a
/// guessed one risks breaking a compile that currently works. Copied from a real file, not a guess.
///
/// COPIED, NOT GUESSED. `first_person_arms_preview.vmdl` ships as source at
/// `sbox\addons\citizen\Assets\models\first_person\` and carries exactly this: an `AnimationList`
/// with `default_root_bone_name`, holding one `AnimBindPose` named `bindPose`. Citizen's own
/// `citizen_animationlist.vmdl_prefab` has the same node with the same fields, under the name
/// `bindPose_internal`, alongside a note of its own worth knowing: the compiled bind pose picks up
/// a little inaccuracy in bone orientations unless compression is disabled, and the INTERNAL one is
/// exact. Nothing here needs that distinction — one bind pose and no clips — so this writes the
/// plain one under the plain name.
///
/// EVERY FIELD IS WRITTEN, including the ones that look like defaults. A KV3 node with fields
/// missing is not the same thing as a node with them at their defaults: the compiler's defaults are
/// not documented anywhere this project can read, and the file that is known to work has all of
/// them. Copying it whole costs sixteen lines and removes the question.
/// </summary>
public static class VmdlAnimation
{
/// <summary>
/// The AnimationList node, indented to sit among a RootNode's children.
///
/// It is only ever wanted on a SKINNED model. A static export has no bones for a bind pose to be
/// about, and adding one there would be a node that means nothing.
/// </summary>
public static string BindPoseList() =>
"\t\t\t{\n"
+ "\t\t\t\t_class = \"AnimationList\"\n"
+ "\t\t\t\tchildren = \n"
+ "\t\t\t\t[\n"
+ "\t\t\t\t\t{\n"
+ "\t\t\t\t\t\t_class = \"AnimBindPose\"\n"
+ "\t\t\t\t\t\tname = \"bindPose\"\n"
+ "\t\t\t\t\t\tactivity_name = \"\"\n"
+ "\t\t\t\t\t\tactivity_weight = 1\n"
+ "\t\t\t\t\t\tweight_list_name = \"\"\n"
+ "\t\t\t\t\t\tfade_in_time = 0.2\n"
+ "\t\t\t\t\t\tfade_out_time = 0.2\n"
+ "\t\t\t\t\t\tlooping = false\n"
+ "\t\t\t\t\t\tdelta = false\n"
+ "\t\t\t\t\t\tworldSpace = false\n"
+ "\t\t\t\t\t\thidden = false\n"
+ "\t\t\t\t\t\tanim_markup_ordered = false\n"
+ "\t\t\t\t\t\tdisable_compression = false\n"
+ "\t\t\t\t\t\tdisable_interpolation = false\n"
+ "\t\t\t\t\t\tenable_scale = false\n"
+ "\t\t\t\t\t\tframe_count = 1\n"
+ "\t\t\t\t\t\tframe_rate = 30\n"
+ "\t\t\t\t\t},\n"
+ "\t\t\t\t]\n"
+ "\t\t\t\tdefault_root_bone_name = \"\"\n"
+ "\t\t\t},\n";
/// <summary>
/// Every bone marked do_not_discard, so ModelDoc keeps bones this export gives it no other
/// reason to keep.
///
/// CONFIRMED TWICE, and the second time is the one worth recording. ModelDoc prunes any bone that
/// is neither weighted by the mesh nor animated by anything imported — first seen on
/// `Assets/models/first_person/fp_arms.vmdl`, where arm_root and the two upper arms kept vanishing
/// (27 bones in the FBX, 24 in the compiled model) until a BoneMarkupList fixed it. Then measured
/// again from the other end: a two-bone sample .vmdl written WITHOUT this node compiled fine, and
/// the loaded model reported exactly one bone. `root` survived, `child` did not.
///
/// So this is not belt and braces. Effigy's export has no AnimationList of clips, so nothing is
/// ever "animated by anything imported", and every bone leans entirely on being weighted.
/// SkinBinder's nearest-vertex fallback gives most bones at least one vertex, and a short helper
/// joint that never ends up nearest to anything has nothing else keeping it alive.
///
/// Marking every bone rather than working out which ones are at risk costs a line each and
/// removes the question. `bone_cull_type = "None"` on the list says the same thing again at the
/// list level, which is what fp_arms does.
///
/// IN THE KERNEL RATHER THAN THE EDITOR because the sample .vmdl the suite writes has to carry
/// the same node the editor writes, or compiling the sample answers a question about the sample.
/// </summary>
public static string BoneMarkupList( Skeleton skeleton )
{
if ( skeleton is null || skeleton.Count == 0 )
return "";
var sb = new StringBuilder();
sb.Append( "\t\t\t{\n" );
sb.Append( "\t\t\t\t_class = \"BoneMarkupList\"\n" );
sb.Append( "\t\t\t\tchildren = \n" );
sb.Append( "\t\t\t\t[\n" );
foreach ( var bone in skeleton.Bones )
{
sb.Append( "\t\t\t\t\t{\n" );
sb.Append( "\t\t\t\t\t\t_class = \"BoneMarkup\"\n" );
sb.Append( $"\t\t\t\t\t\ttarget_bone = \"{bone.Name}\"\n" );
sb.Append( "\t\t\t\t\t\tignore_Translation = false\n" );
sb.Append( "\t\t\t\t\t\tignore_rotation = false\n" );
sb.Append( "\t\t\t\t\t\tdo_not_discard = true\n" );
sb.Append( "\t\t\t\t\t},\n" );
}
sb.Append( "\t\t\t\t]\n" );
sb.Append( "\t\t\t\tbone_cull_type = \"None\"\n" );
sb.Append( "\t\t\t},\n" );
return sb.ToString();
}
}