#if UNITY_EDITOR using System; using System.IO; using System.Linq; using System.Text; using System.Collections.Generic; using System.Threading.Tasks; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using Object=UnityEngine.Object; /// Actual four-paw support, body orientation and rigid shell/armor/horn QA. public static class VerifyQuadrupedMotions { [Serializable] public sealed class Recipe { public string id,assetRoot,staticPrefab,rootBone,shellBone,chestBone,headBone,outputDir,attackEvent,profile; public string frontRightFootTip,frontLeftFootTip,hindRightFootTip,hindLeftFootTip; public float height,walkStride,walkLift,maxRootTilt,groundTolerance,shellRigidityTolerance,attackHeadReach,attackImpact,attackSeconds; public float[] shellRegionMin,shellRegionMax; public float[] armorRegionMin,armorRegionMax,hornRegionMin,hornRegionMax; public float armorRigidityTolerance,hornRigidityTolerance,attackLunge,attackHeadPitchDegrees; public int maxTriangles; } static string Repo=>Path.GetFullPath(Path.Combine(Application.dataPath,"../..")); static Recipe Read(string path)=>JsonUtility.FromJson(File.ReadAllText(Path.Combine(Repo,path))); static void Require(bool ok,string message){if(!ok)throw new Exception(message);} static Vector3[] Vertices(GameObject go)=>go.GetComponentsInChildren().SelectMany(s=> { var m=s.sharedMesh;var w=m.boneWeights;var bind=m.bindposes; var matrices=s.bones.Select((b,i)=>b.localToWorldMatrix*bind[i]).ToArray(); return m.vertices.Select((v,i)=>matrices[w[i].boneIndex0].MultiplyPoint3x4(v)*w[i].weight0+ matrices[w[i].boneIndex1].MultiplyPoint3x4(v)*w[i].weight1+ matrices[w[i].boneIndex2].MultiplyPoint3x4(v)*w[i].weight2+ matrices[w[i].boneIndex3].MultiplyPoint3x4(v)*w[i].weight3).ToArray(); }).ToArray(); static Bounds BoundsOf(Vector3[] p){var b=new Bounds(p[0],Vector3.zero);foreach(var v in p)b.Encapsulate(v);return b;} static float Difference(Vector3[] a,Vector3[] b)=>a.Select((v,i)=>Vector3.Distance(v,b[i])).Max(); static bool Inside(Vector3 p,float[] lo,float[] hi)=>Enumerable.Range(0,3).All(c=>p[c]>=lo[c] && p[c]<=hi[c]); public static string Walk(string path)=>RunCore(path,true); public static string Run(string path)=>RunCore(path,false); static string RunCore(string path,bool walkOnly) { var r=Read(path);var sb=new StringBuilder();var scene=EditorSceneManager.NewPreviewScene(); try { var prefab=AssetDatabase.LoadAssetAtPath(r.assetRoot+"/"+r.id+"_Rigged.prefab"); Require(prefab!=null,"Missing quadruped prefab"); var go=(GameObject)PrefabUtility.InstantiatePrefab(prefab,scene);var anim=go.GetComponentInChildren();anim.enabled=false; var model=anim.gameObject;var bones=go.GetComponentsInChildren(); bool heavy=r.profile=="quadruped_heavy"; var root=bones.First(b=>b.name==r.rootBone);var shell=bones.First(b=>b.name==(heavy ? r.chestBone : r.shellBone));var head=bones.First(b=>b.name==r.headBone); var feet=new[]{r.frontRightFootTip,r.frontLeftFootTip,r.hindRightFootTip,r.hindLeftFootTip}.Select(n=>bones.First(b=>b.name==n)).ToArray(); Require(feet.Distinct().Count()==4,"Four distinct paw tips required"); Require(head.position.z().Sum(s=>s.sharedMesh.triangles.Length/3); Require(Mathf.Abs(bb.size.y-r.height)<.002f && Mathf.Abs(bb.min.y)<.002f && tris<=r.maxTriangles,"Bind dimensions/triangle budget invalid"); Require(go.GetComponentsInChildren().All(s=>s.sharedMaterial.shader.name=="Test/XPbr"),"XPbr required"); var approved=(GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(r.staticPrefab),scene); var staticPoints=approved.GetComponentsInChildren().SelectMany(m=>m.sharedMesh.vertices.Select(v=>m.transform.TransformPoint(v))).ToArray(); float shape=bind.Max(v=>staticPoints.Min(p=>Vector3.Distance(v,p)));Object.DestroyImmediate(approved); Require(shape<.002f,"Approved quadruped geometry changed"); var shellIndices=Enumerable.Range(0,bind.Length).Where(i=>Inside(bind[i]/r.height,heavy ? r.armorRegionMin : r.shellRegionMin,heavy ? r.armorRegionMax : r.shellRegionMax)).ToArray(); var hornIndices=heavy ? Enumerable.Range(0,bind.Length).Where(i=>Inside(bind[i]/r.height,r.hornRegionMin,r.hornRegionMax)).ToArray() : new int[0]; Require(shellIndices.Length>=20,"Shell/armor audit region too small"); if(heavy)Require(hornIndices.Length>=5,"Horn audit region too small"); var footIndices=Enumerable.Range(0,4).Select(f=>Enumerable.Range(0,bind.Length).Where(i=> { var distances=feet.Select(t=>Vector2.Distance(new Vector2(bind[i].x,bind[i].z),new Vector2(t.position.x,t.position.z))).ToArray(); return bind[i].yp.Length>=5),"Each paw needs its own sampled geometry"); sb.AppendLine($"BIND height={bb.size.y:F6} bottom={bb.min.y:F6} tris={tris} staticVertexError={shape:F6} bodyVertices={shellIndices.Length} hornVertices={hornIndices.Length} pawVertices={string.Join("/",footIndices.Select(p=>p.Length))}"); var clips=new Dictionary(); foreach(string name in walkOnly ? new[]{"Walk"} : new[]{"Idle","Walk","Attack"}) { var clip=AssetDatabase.LoadAssetAtPath(r.assetRoot+"/Clips/"+name+".anim");Require(clip!=null,"Missing "+name);clips[name]=clip; foreach(var b in AnimationUtility.GetCurveBindings(clip))Require(string.IsNullOrEmpty(b.path)||model.transform.Find(b.path)!=null,"Unbound "+b.path); Require(AnimationUtility.GetAnimationClipSettings(clip).loopTime==(name!="Attack"),"Wrong loop setting"); var traces=Enumerable.Range(0,4).Select(_=>new List()).ToArray();var lifts=new float[4]; Vector3[] start=null,end=null;Vector3 rootStart=Vector3.zero;float headStart=0,bodyStart=0,bodyForward=0,headPitch=0,hornLowering=0,forward=0,retract=0,minY=1e9f,maxY=-1e9f,movement=0,tilt=0,rigidity=0,hornError=0,drift=0,minHeight=1e9f;int minSupport=4; for(int f=0;f<=120;f++) { clip.SampleAnimation(model,clip.length*f/120f);var p=Vertices(go);var bounds=BoundsOf(p); if(f==0){start=p;rootStart=root.position;headStart=(head.position-root.position).z;bodyStart=(shell.position-root.position).z;}if(f==120)end=p; minY=Mathf.Min(minY,bounds.min.y);maxY=Mathf.Max(maxY,bounds.min.y);minHeight=Mathf.Min(minHeight,bounds.size.y);movement=Mathf.Max(movement,Difference(start,p)); // Measure the visible body, not only the deliberately pinned root bone. tilt=Mathf.Max(tilt,Vector3.Angle(shell.rotation*Quaternion.Inverse(shellBind)*Vector3.up,Vector3.up)); drift=Mathf.Max(drift,Vector2.Distance(new Vector2(root.position.x,root.position.z),new Vector2(rootStart.x,rootStart.z))); var inverse=Quaternion.Inverse(shell.rotation*Quaternion.Inverse(shellBind)); rigidity=Mathf.Max(rigidity,shellIndices.Max(i=>Vector3.Distance(bind[i],inverse*(p[i]-shell.position)+shellPosition))); if(heavy) { var inverseHead=Quaternion.Inverse(head.rotation*Quaternion.Inverse(headBind)); hornError=Mathf.Max(hornError,hornIndices.Max(i=>Vector3.Distance(bind[i],inverseHead*(p[i]-head.position)+headPosition))); bodyForward=Mathf.Max(bodyForward,bodyStart-(shell.position-root.position).z); headPitch=Mathf.Max(headPitch,Quaternion.Angle(headBind,head.rotation)); // Lowering is measured from visible horn geometry relative to // the head pivot, so an unrelated yaw cannot pass as a nod. hornLowering=Mathf.Max(hornLowering,hornIndices.Average(i=>bind[i].y-headPosition.y)-hornIndices.Average(i=>p[i].y-head.position.y)); } int support=0; for(int foot=0;foot<4;foot++) { float bottom=footIndices[foot].Min(i=>p[i].y);Require(bottom>=-r.groundTolerance,"Paw penetrates floor"); if(bottom<=r.groundTolerance)support++;lifts[foot]=Mathf.Max(lifts[foot],bottom); traces[foot].Add(footIndices[foot].Average(i=>p[i].z-root.position.z)); } minSupport=Mathf.Min(minSupport,support); float headZ=(head.position-root.position).z;forward=Mathf.Max(forward,headStart-headZ);retract=Mathf.Max(retract,headZ-headStart); } float seam=Difference(start,end);var strides=traces.Select(v=>v.Max()-v.Min()).ToArray(); sb.AppendLine($"{name} len={clip.length:F3} bottom=[{minY:F6},{maxY:F6}] minHeight={minHeight:F5} bindUpTilt={tilt:F3} movement={movement:F5} seam={seam:F6} rootXZ={drift:F5} bodyError={rigidity:F6} hornError={hornError:F6} support={minSupport} strides={string.Join("/",strides.Select(v=>v.ToString("F5")))} lifts={string.Join("/",lifts.Select(v=>v.ToString("F5")))} headForward={forward:F5} retract={retract:F5} shoulderForward={bodyForward:F5} headPitch={headPitch:F3} hornLowering={hornLowering:F5}"); Require(minY>=-r.groundTolerance && maxY<=r.groundTolerance && tilt<=r.maxRootTilt && minHeight>.8f*r.height,"Orientation/ground failed\n"+sb); Require(seam<.001f && drift<.001f && rigidity<=(heavy ? r.armorRigidityTolerance : r.shellRigidityTolerance) && (!heavy || hornError<=r.hornRigidityTolerance),"Seam/root/rigid geometry failed\n"+sb); if(name=="Walk")Require(strides.All(v=>v>r.walkStride*.5f) && lifts.All(v=>v>r.walkLift*.4f) && minSupport>=3,"Four-paw gait failed\n"+sb); else Require(minSupport==4,"Standing/attack must brace on four paws\n"+sb); if(name=="Idle")Require(movement>.0002f && movement<.03f,"Idle motion invalid\n"+sb); if(name=="Attack") { Require(movement>.02f && forward>r.attackHeadReach*.5f && retract>r.attackHeadReach*.1f,"Head retract/release unreadable\n"+sb); if(heavy)Require(bodyForward>r.attackLunge*.5f && headPitch>r.attackHeadPitchDegrees*.5f && hornLowering>r.height*.005f,"Heavy bash must drive shoulder and lower head\n"+sb); var events=AnimationUtility.GetAnimationEvents(clip);Require(events.Length==1 && events[0].functionName==r.attackEvent && Mathf.Abs(events[0].time-r.attackImpact*r.attackSeconds)<.001f,"Release event invalid"); } } if(!walkOnly) { clips["Idle"].SampleAnimation(model,0);var idle=Vertices(go);clips["Attack"].SampleAnimation(model,clips["Attack"].length);Require(Difference(idle,Vertices(go))<.001f,"Attack recovery mismatch"); anim.enabled=true;anim.Rebind();anim.Update(0); Action step=seconds=>{for(int f=0;f-.01f,"Transition penetrates ground");}}; Action state=n=>Require(anim.GetCurrentAnimatorStateInfo(0).IsName(n),"Expected "+n); state("Idle");anim.SetInteger("Motion",1);step(.25f);state("Walk");anim.SetTrigger("Attack");step(.2f);state("Attack");step(r.attackSeconds+.3f);state("Walk");step(r.attackSeconds*2);state("Walk"); anim.SetInteger("Motion",0);step(.25f);state("Idle");anim.SetTrigger("Attack");step(.2f);state("Attack");step(r.attackSeconds+.3f);state("Idle"); sb.AppendLine("CONTROLLER PASS four-legged Idle<->Walk, single Attack and requested-state recovery"); } string output=Path.Combine(Repo,r.outputDir,walkOnly ? "walk_verification.txt" : "verification.txt");Directory.CreateDirectory(Path.GetDirectoryName(output));File.WriteAllText(output,sb.ToString());return "PASS quadruped\n"+sb; } catch(Exception e) { // Keep the actual QA failure even when Pipeline loses the exception // response during its main-thread dispatch timeout. string output=Path.Combine(Repo,r.outputDir,"last_failure.txt");Directory.CreateDirectory(Path.GetDirectoryName(output));File.WriteAllText(output,DateTime.UtcNow.ToString("O")+"\n"+e); throw; } finally{EditorSceneManager.ClosePreviewScene(scene);} } public static async Task Live(string path) { Require(EditorApplication.isPlaying,"Use Play Mode");var r=Read(path);var go=GameObject.Find("CompanionReview_"+r.id+"_Cycle");Require(go!=null,"Missing review cycle"); string body=r.profile=="quadruped_heavy" ? r.chestBone : r.shellBone; var anim=go.GetComponentInChildren();var shell=go.GetComponentsInChildren().First(t=>t.name==body); var prefab=AssetDatabase.LoadAssetAtPath(r.assetRoot+"/"+r.id+"_Rigged.prefab");var bind=prefab.GetComponentsInChildren(true).First(t=>t.name==body).rotation; var seen=new HashSet();var origin=go.transform.position;float tilt=0;var deadline=DateTime.UtcNow.AddSeconds(10); while(DateTime.UtcNow