#!/usr/bin/env python3 """\ This module contains tools for manipulating and validating the avatar skeleton file. $LicenseInfo:firstyear=2016&license=viewerlgpl$ Second Life Viewer Source Code Copyright (C) 2016, Linden Research, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License only. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA $/LicenseInfo$ """ import argparse from lxml import etree def get_joint_names(tree): joints = [element.get('name') for element in tree.getroot().iter() if element.tag in ['bone','collision_volume']] print("joints:",joints) return joints def get_aliases(tree): aliases = {} alroot = tree.getroot() for element in alroot.iter(): for key in list(element.keys()): if key == 'aliases': name = element.get('name') val = element.get('aliases') aliases[name] = val return aliases def fix_name(element): pass def enforce_precision_rules(element): pass def float_tuple(str, n=3): try: result = tuple(float(e) for e in str.split()) if len(result)==n: return result else: print("tuple length wrong:", str,"gave",result,"wanted len",n,"got len",len(result)) raise Exception() except: print("convert failed for:",str) raise def check_symmetry(name, field, vec1, vec2): if vec1[0] != vec2[0]: print(name,field,"x match fail") if vec1[1] != -vec2[1]: print(name,field,"y mirror image fail") if vec1[2] != vec2[2]: print(name,field,"z match fail") def enforce_alias_rules(tree, element, fix=False): if element.tag != "bone": return alias_lis = [] aliases = element.get("aliases") if aliases: alias_lis = aliases.split(" ") name = element.get("name") if name: std_alias = "avatar_" + name if not std_alias in alias_lis: print "missing expected alias",name,std_alias for alias in alias_lis: if alias.startswith("avatar_") and alias != std_alias: print "invalid avatar_ alias",name,alias def enforce_symmetry(tree, element, field, fix=False): name = element.get("name") if not name: return if "Right" in name: left_name = name.replace("Right","Left") left_element = get_element_by_name(tree, left_name) pos = element.get(field) left_pos = left_element.get(field) pos_tuple = float_tuple(pos) left_pos_tuple = float_tuple(left_pos) check_symmetry(name,field,pos_tuple,left_pos_tuple) def get_element_by_name(tree,name): if tree is None: return None matches = [elt for elt in tree.getroot().iter() if elt.get("name")==name] if len(matches)==1: return matches[0] elif len(matches)>1: print("multiple matches for name",name) return None else: return None def list_skel_tree(tree): for element in tree.getroot().iter(): if element.tag == "bone": print(element.get("name"),"-",element.get("support")) def validate_child_order(tree, ogtree, fix=False): unfixable = 0 #print "validate_child_order am failing for NO RAISIN!" #unfixable += 1 tofix = set() for element in tree.getroot().iter(): if element.tag != "bone": continue og_element = get_element_by_name(ogtree,element.get("name")) if og_element is not None: for echild,ochild in zip(list(element),list(og_element)): if echild.get("name") != ochild.get("name"): print("Child ordering error, parent",element.get("name"),echild.get("name"),"vs",ochild.get("name")) if fix: tofix.add(element.get("name")) children = {} for name in tofix: print("FIX",name) element = get_element_by_name(tree,name) og_element = get_element_by_name(ogtree,name) children = [] # add children matching the original joints first, in the same order for og_elt in list(og_element): elt = get_element_by_name(tree,og_elt.get("name")) if elt is not None: children.append(elt) print("b:",elt.get("name")) else: print("b missing:",og_elt.get("name")) # then add children that are not present in the original joints for elt in list(element): og_elt = get_element_by_name(ogtree,elt.get("name")) if og_elt is None: children.append(elt) print("e:",elt.get("name")) # if we've done this right, we have a rearranged list of the same length if len(children)!=len(element): print("children",[e.get("name") for e in children]) print("element",[e.get("name") for e in element]) print("children changes for",name,", cannot reconcile") else: element[:] = children return unfixable # Checklist for the final file, started from SL-276: # - new "end" attribute on all bones # - new "connected" attribute on all bones # - new "support" tag on all bones and CVs # - aliases where appropriate for backward compatibility. rFoot and lFoot associated with mAnkle bones (not mFoot bones) # - correct counts of bones and collision volumes in header # - check all comments # - old fields of old bones and CVs should be identical to their previous values. # - old bones and CVs should retain their previous ordering under their parent, with new joints going later in any given child list # - corresponding right and left joints should be mirror symmetric. # - childless elements should be in short form ( instead of ) # - digits of precision should be consistent (again, except for old joints) # - new bones should have pos, pivot the same def validate_skel_tree(tree, ogtree, reftree, fix=False): print("validate_skel_tree") (num_bones,num_cvs) = (0,0) unfixable = 0 defaults = {"connected": "false", "group": "Face" } for element in tree.getroot().iter(): og_element = get_element_by_name(ogtree,element.get("name")) ref_element = get_element_by_name(reftree,element.get("name")) # Preserve values from og_file: for f in ["pos","rot","scale","pivot"]: if og_element is not None and og_element.get(f) and (str(element.get(f)) != str(og_element.get(f))): print(element.get("name"),"field",f,"has changed:",og_element.get(f),"!=",element.get(f)) if fix: element.set(f, og_element.get(f)) # Pick up any other fields that we can from ogtree and reftree fields = [] if element.tag in ["bone","collision_volume"]: fields = ["support","group"] if element.tag == 'bone': fields.extend(["end","connected"]) for f in fields: if not element.get(f): print(element.get("name"),"missing required field",f) if fix: if og_element is not None and og_element.get(f): print("fix from ogtree") element.set(f,og_element.get(f)) elif ref_element is not None and ref_element.get(f): print("fix from reftree") element.set(f,ref_element.get(f)) else: if f in defaults: print("fix by using default value",f,"=",defaults[f]) element.set(f,defaults[f]) elif f == "support": if og_element is not None: element.set(f,"base") else: element.set(f,"extended") else: print("unfixable:",element.get("name"),"no value for field",f) unfixable += 1 fix_name(element) enforce_alias_rules(tree, element, fix) enforce_precision_rules(element) for field in ["pos","pivot"]: enforce_symmetry(tree, element, field, fix) if element.get("support")=="extended": if element.get("pos") != element.get("pivot"): print("extended joint",element.get("name"),"has mismatched pos, pivot") if element.tag == "linden_skeleton": num_bones = int(element.get("num_bones")) num_cvs = int(element.get("num_collision_volumes")) all_bones = [e for e in tree.getroot().iter() if e.tag=="bone"] all_cvs = [e for e in tree.getroot().iter() if e.tag=="collision_volume"] if num_bones != len(all_bones): print("wrong bone count, expected",len(all_bones),"got",num_bones) if fix: element.set("num_bones", str(len(all_bones))) if num_cvs != len(all_cvs): print("wrong cv count, expected",len(all_cvs),"got",num_cvs) if fix: element.set("num_collision_volumes", str(len(all_cvs))) print("skipping child order code") #unfixable += validate_child_order(tree, ogtree, fix) if fix and (unfixable > 0): print("BAD FILE:", unfixable,"errs could not be fixed") def slider_info(ladtree,skeltree): for param in ladtree.iter("param"): for skel_param in param.iter("param_skeleton"): bones = [b for b in skel_param.iter("bone")] if bones: print("param",param.get("name"),"id",param.get("id")) value_min = float(param.get("value_min")) value_max = float(param.get("value_max")) neutral = 100.0 * (0.0-value_min)/(value_max-value_min) print(" neutral",neutral) for b in bones: scale = float_tuple(b.get("scale","0 0 0")) offset = float_tuple(b.get("offset","0 0 0")) print(" bone", b.get("name"), "scale", scale, "offset", offset) scale_min = [value_min * s for s in scale] scale_max = [value_max * s for s in scale] offset_min = [value_min * t for t in offset] offset_max = [value_max * t for t in offset] if (scale_min != scale_max): print(" Scale MinX", scale_min[0]) print(" Scale MinY", scale_min[1]) print(" Scale MinZ", scale_min[2]) print(" Scale MaxX", scale_max[0]) print(" Scale MaxY", scale_max[1]) print(" Scale MaxZ", scale_max[2]) if (offset_min != offset_max): print(" Offset MinX", offset_min[0]) print(" Offset MinY", offset_min[1]) print(" Offset MinZ", offset_min[2]) print(" Offset MaxX", offset_max[0]) print(" Offset MaxY", offset_max[1]) print(" Offset MaxZ", offset_max[2]) # Check contents of avatar_lad file relative to a specified skeleton def validate_lad_tree(ladtree,skeltree,orig_ladtree): print("validate_lad_tree") bone_names = [elt.get("name") for elt in skeltree.iter("bone")] bone_names.append("mScreen") bone_names.append("mRoot") cv_names = [elt.get("name") for elt in skeltree.iter("collision_volume")] #print "bones\n ","\n ".join(sorted(bone_names)) #print "cvs\n ","\n ".join(sorted(cv_names)) for att in ladtree.iter("attachment_point"): att_name = att.get("name") #print "attachment",att_name joint_name = att.get("joint") if not joint_name in bone_names: print("att",att_name,"linked to invalid joint",joint_name) for skel_param in ladtree.iter("param_skeleton"): skel_param_id = skel_param.get("id") skel_param_name = skel_param.get("name") #if not skel_param_name and not skel_param_id: # print "strange skel_param" # print etree.tostring(skel_param) # for k,v in skel_param.attrib.iteritems(): # print k,"->",v for bone in skel_param.iter("bone"): bone_name = bone.get("name") if not bone_name in bone_names: print("skel param references invalid bone",bone_name) print(etree.tostring(bone)) bone_scale = float_tuple(bone.get("scale","0 0 0")) bone_offset = float_tuple(bone.get("offset","0 0 0")) param = bone.getparent().getparent() if bone_scale==(0, 0, 0) and bone_offset==(0, 0, 0): print("no-op bone",bone_name,"in param",param.get("id","-1")) # check symmetry of sliders if "Right" in bone.get("name"): left_name = bone_name.replace("Right","Left") left_bone = None for b in skel_param.iter("bone"): if b.get("name")==left_name: left_bone = b if left_bone is None: print("left_bone not found",left_name,"in",param.get("id","-1")) else: left_scale = float_tuple(left_bone.get("scale","0 0 0")) left_offset = float_tuple(left_bone.get("offset","0 0 0")) if left_scale != bone_scale: print("scale mismatch between",bone_name,"and",left_name,"in param",param.get("id","-1")) param_id = int(param.get("id","-1")) if param_id in [661]: # shear expected_offset = tuple([bone_offset[0],bone_offset[1],-bone_offset[2]]) elif param_id in [30656, 31663, 32663]: # shift expected_offset = bone_offset else: expected_offset = tuple([bone_offset[0],-bone_offset[1],bone_offset[2]]) if left_offset != expected_offset: print("offset mismatch between",bone_name,"and",left_name,"in param",param.get("id","-1")) drivers = {} for driven_param in ladtree.iter("driven"): driver = driven_param.getparent().getparent() driven_id = driven_param.get("id") driver_id = driver.get("id") actual_param = next(param for param in ladtree.iter("param") if param.get("id")==driven_id) if not driven_id in drivers: drivers[driven_id] = set() drivers[driven_id].add(driver_id) if (actual_param.get("value_min") != driver.get("value_min") or \ actual_param.get("value_max") != driver.get("value_max")): if args.verbose: print("MISMATCH min max:",driver.get("id"),"drives",driven_param.get("id"),"min",driver.get("value_min"),actual_param.get("value_min"),"max",driver.get("value_max"),actual_param.get("value_max")) for driven_id in drivers: dset = drivers[driven_id] if len(dset) != 1: print("driven_id",driven_id,"has multiple drivers",dset) else: if args.verbose: print("driven_id",driven_id,"has one driver",dset) if orig_ladtree: # make sure expected message format is unchanged orig_message_params_by_id = dict((int(param.get("id")),param) for param in orig_ladtree.iter("param") if param.get("group") in ["0","3"]) orig_message_ids = sorted(orig_message_params_by_id.keys()) #print "orig_message_ids",orig_message_ids message_params_by_id = dict((int(param.get("id")),param) for param in ladtree.iter("param") if param.get("group") in ["0","3"]) message_ids = sorted(message_params_by_id.keys()) #print "message_ids",message_ids if (set(message_ids) != set(orig_message_ids)): print("mismatch in message ids!") print("added",set(message_ids) - set(orig_message_ids)) print("removed",set(orig_message_ids) - set(message_ids)) else: print("message ids OK") def remove_joint_by_name(tree, name): print("remove joint:",name) elt = get_element_by_name(tree,name) while elt is not None: children = list(elt) parent = elt.getparent() print("graft",[e.get("name") for e in children],"into",parent.get("name")) print("remove",elt.get("name")) #parent_children = list(parent) loc = parent.index(elt) parent[loc:loc+1] = children elt[:] = [] print("parent now:",[e.get("name") for e in list(parent)]) elt = get_element_by_name(tree,name) def compare_skel_trees(atree,btree): diffs = {} realdiffs = {} a_missing = set() b_missing = set() a_names = set(e.get("name") for e in atree.getroot().iter() if e.get("name")) b_names = set(e.get("name") for e in btree.getroot().iter() if e.get("name")) print("a_names\n ",str("\n ").join(sorted(list(a_names)))) print() print("b_names\n ","\n ".join(sorted(list(b_names)))) all_names = set.union(a_names,b_names) for name in all_names: if not name: continue a_element = get_element_by_name(atree,name) b_element = get_element_by_name(btree,name) if a_element is None or b_element is None: print("something not found for",name,a_element,b_element) if a_element is not None and b_element is not None: all_attrib = set.union(set(a_element.attrib.keys()),set(b_element.attrib.keys())) print(name,all_attrib) for att in all_attrib: if a_element.get(att) != b_element.get(att): if not att in diffs: diffs[att] = set() diffs[att].add(name) print("tuples",name,att,float_tuple(a_element.get(att)),float_tuple(b_element.get(att))) if float_tuple(a_element.get(att)) != float_tuple(b_element.get(att)): print("diff in",name,att) if not att in realdiffs: realdiffs[att] = set() realdiffs[att].add(name) for att in diffs: print("Differences in",att) for name in sorted(diffs[att]): print(" ",name) for att in realdiffs: print("Real differences in",att) for name in sorted(diffs[att]): print(" ",name) a_missing = b_names.difference(a_names) b_missing = a_names.difference(b_names) if len(a_missing) or len(b_missing): print("Missing from comparison") for name in a_missing: print(" ",name) print("Missing from infile") for name in b_missing: print(" ",name) if __name__ == "__main__": parser = argparse.ArgumentParser(description="process SL avatar_skeleton/avatar_lad files") parser.add_argument("--verbose", action="store_true",help="verbose flag") parser.add_argument("--ogfile", help="specify file containing base bones", default="avatar_skeleton_orig.xml") parser.add_argument("--ref_file", help="specify another file containing replacements for missing fields") parser.add_argument("--lad_file", help="specify avatar_lad file to check", default="avatar_lad.xml") parser.add_argument("--orig_lad_file", help="specify avatar_lad file to compare to", default="avatar_lad_orig.xml") parser.add_argument("--aliases", help="specify file containing bone aliases") parser.add_argument("--validate", action="store_true", help="check specified input file for validity") parser.add_argument("--fix", action="store_true", help="try to correct errors") parser.add_argument("--remove", nargs="+", help="remove specified joints") parser.add_argument("--list", action="store_true", help="list joint names") parser.add_argument("--compare", help="alternate skeleton file to compare") parser.add_argument("--slider_info", help="information about the lad file sliders and affected bones", action="store_true") parser.add_argument("infilename", nargs="?", help="name of a skel .xml file to input", default="avatar_skeleton.xml") parser.add_argument("outfilename", nargs="?", help="name of a skel .xml file to output") args = parser.parse_args() tree = etree.parse(args.infilename) aliases = {} if args.aliases: altree = etree.parse(args.aliases) aliases = get_aliases(altree) # Parse input files ogtree = None reftree = None ladtree = None orig_ladtree = None if args.ogfile: ogtree = etree.parse(args.ogfile) if args.ref_file: reftree = etree.parse(args.ref_file) if args.lad_file: ladtree = etree.parse(args.lad_file) if args.orig_lad_file: orig_ladtree = etree.parse(args.orig_lad_file) if args.remove: for name in args.remove: remove_joint_by_name(tree,name) # Do processing if args.validate and ogtree: validate_skel_tree(tree, ogtree, reftree) if args.validate and ladtree: validate_lad_tree(ladtree, tree, orig_ladtree) if args.fix and ogtree: validate_skel_tree(tree, ogtree, reftree, True) if args.list and tree: list_skel_tree(tree) if args.compare and tree: compare_tree = etree.parse(args.compare) compare_skel_trees(compare_tree,tree) if ladtree and tree and args.slider_info: slider_info(ladtree,tree) if args.outfilename: f = open(args.outfilename,"w") print(etree.tostring(tree, pretty_print=True), file=f) #need update to get: , short_empty_elements=True)