diff --git a/indra/llmessage/lliosocket.cpp b/indra/llmessage/lliosocket.cpp
index b7460df508caec0cdabfd22556c76c1786e35d39..b15b98db806b04ae7204cd2a6c4482a5d0f7b13a 100644
--- a/indra/llmessage/lliosocket.cpp
+++ b/indra/llmessage/lliosocket.cpp
@@ -101,7 +101,7 @@ void ll_debug_socket(const char* msg, apr_socket_t* apr_sock)
 ///
 
 // static
-LLSocket::ptr_t LLSocket::create(apr_pool_t* pool, EType type, U16 port)
+LLSocket::ptr_t LLSocket::create(apr_pool_t* pool, EType type, U16 port, const char *hostname)
 {
 	LLSocket::ptr_t rv;
 	apr_socket_t* socket = NULL;
@@ -150,7 +150,7 @@ LLSocket::ptr_t LLSocket::create(apr_pool_t* pool, EType type, U16 port)
 		apr_sockaddr_t* sa = NULL;
 		status = apr_sockaddr_info_get(
 			&sa,
-			APR_ANYADDR,
+			hostname,
 			APR_UNSPEC,
 			port,
 			0,
diff --git a/indra/llmessage/lliosocket.h b/indra/llmessage/lliosocket.h
index f840f0275c12388e0eff23b8ff26c33b990f74bc..303d80eb142982c40daa1592d865fc06b52e19cd 100644
--- a/indra/llmessage/lliosocket.h
+++ b/indra/llmessage/lliosocket.h
@@ -96,12 +96,14 @@ class LLSocket
 	 * and associated with the socket.
 	 * @param type The type of socket to create
 	 * @param port The port for the socket
+	 * @param hostname e.g. APR_ANYADDR to listen openly, or "127.0.0.1"
 	 * @return A valid socket shared pointer if the call worked.
 	 */
 	static ptr_t create(
 		apr_pool_t* pool,
 		EType type,
-		U16 port = PORT_EPHEMERAL);
+		U16 port = PORT_EPHEMERAL,
+		const char *hostname = APR_ANYADDR);
 
 	/** 
 	 * @brief Create a LLSocket when you already have an apr socket.
diff --git a/indra/mac_crash_logger/CMakeLists.txt b/indra/mac_crash_logger/CMakeLists.txt
index ab2038826101bf3e1bb1f1fce3fbe9267e711c8e..f6c4dfb59da8ef3723a2b1ada85821cb45bf1e0a 100644
--- a/indra/mac_crash_logger/CMakeLists.txt
+++ b/indra/mac_crash_logger/CMakeLists.txt
@@ -85,7 +85,7 @@ add_custom_command(
   COMMAND ${CMAKE_COMMAND}
   ARGS
     -E
-    copy_directory
+    copy_if_different
     ${CMAKE_CURRENT_SOURCE_DIR}/CrashReporter.nib
     ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/mac-crash-logger.app/Contents/Resources/CrashReporter.nib
   )
diff --git a/indra/test/io.cpp b/indra/test/io.cpp
index ff900ab96bea3840cf497417a7a4114007920597..40243a8ad6ef6505f9f2988db9b2c8dea9d3c6bc 100644
--- a/indra/test/io.cpp
+++ b/indra/test/io.cpp
@@ -914,7 +914,8 @@ namespace tut
 			mSocket = LLSocket::create(
 				mPool,
 				LLSocket::STREAM_TCP,
-				SERVER_LISTEN_PORT);
+				SERVER_LISTEN_PORT,
+				"127.0.0.1");
 		}
 
 		~pipe_and_pump_fitness()
diff --git a/scripts/content_tools/anim_tool.py b/scripts/content_tools/anim_tool.py
index 77bf731ae60e8be463565eba3bbcc5bcb463304d..3496617b21404b7a06dff73dbe546e5b42582abe 100644
--- a/scripts/content_tools/anim_tool.py
+++ b/scripts/content_tools/anim_tool.py
@@ -1,14 +1,22 @@
-#!runpy.sh
-
+#!/usr/bin/python
 """\
-
-This module contains tools for manipulating the .anim files supported
-for Second Life animation upload. Note that this format is unrelated
-to any non-Second Life formats of the same name.
-
-$LicenseInfo:firstyear=2016&license=viewerlgpl$
+@file   anim_tool.py
+@author Brad Payne, Nat Goodspeed
+@date   2015-09-15
+@brief  This module contains tools for manipulating the .anim files supported
+        for Second Life animation upload. Note that this format is unrelated
+        to any non-Second Life formats of the same name.
+
+        This code is a Python translation of the logic in
+        LLKeyframeMotion::serialize() and deserialize():
+        https://bitbucket.org/lindenlab/viewer-release/src/827a910542a9af0a39b0ca03663c02e5c83869ea/indra/llcharacter/llkeyframemotion.cpp?at=default&fileviewer=file-view-default#llkeyframemotion.cpp-1864
+        https://bitbucket.org/lindenlab/viewer-release/src/827a910542a9af0a39b0ca03663c02e5c83869ea/indra/llcharacter/llkeyframemotion.cpp?at=default&fileviewer=file-view-default#llkeyframemotion.cpp-1220
+        save that there is no support for old-style .anim files, permitting
+        simpler code.
+
+$LicenseInfo:firstyear=2015&license=viewerlgpl$
 Second Life Viewer Source Code
-Copyright (C) 2016, Linden Research, Inc.
+Copyright (C) 2015, 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
@@ -28,63 +36,85 @@
 $/LicenseInfo$
 """
 
-import sys
-import os
-import struct
-import StringIO
 import math
-import argparse
+import os
 import random
-from lxml import etree
+from cStringIO import StringIO
+import struct
+import sys
+from xml.etree import ElementTree
+
+class Error(Exception):
+    pass
+
+class BadFormat(Error):
+    """
+    Something went wrong trying to read the specified .anim file.
+    """
+    pass
+
+class ExtraneousData(BadFormat):
+    """
+    Specifically, the .anim file in question contains more data than needed.
+    This could happen if the file isn't a .anim at all, and it 'just happens'
+    to read properly otherwise -- e.g. a block of all zero bytes could look
+    like empty name strings, empty arrays etc. That could be a legitimate
+    error -- or it could be due to a sloppy tool. Break this exception out
+    separately so caller can distinguish if desired.
+    """
+    pass
 
 U16MAX = 65535
-OOU16MAX = 1.0/(float)(U16MAX)
+# One Over U16MAX, for scaling
+OOU16MAX = 1.0/float(U16MAX)
 
 LL_MAX_PELVIS_OFFSET = 5.0
 
 class FilePacker(object):
     def __init__(self):
-        self.data = StringIO.StringIO()
-        self.offset = 0
+        self.buffer = StringIO()
 
     def write(self,filename):
-        f = open(filename,"wb")
-        f.write(self.data.getvalue())
-        f.close()
+        with open(filename,"wb") as f:
+            f.write(self.buffer.getvalue())
 
     def pack(self,fmt,*args):
         buf = struct.pack(fmt, *args)
-        self.offset += struct.calcsize(fmt)
-        self.data.write(buf)
+        self.buffer.write(buf)
 
     def pack_string(self,str,size=0):
-        buf = str + "\000"
-        if size and (len(buf) < size):
-            buf += "\000" * (size-len(buf))
-        self.data.write(buf)
+        # If size == 0, caller doesn't care, just wants a terminating nul byte
+        size = size or (len(str) + 1)
+        # Nonzero size means a fixed-length field. If the passed string (plus
+        # its terminating nul) exceeds that fixed length, we'll have to
+        # truncate. But make sure we still leave room for the final nul byte!
+        str = str[:size-1]
+        # Now pad what's left of str out to 'size' with nul bytes.
+        buf = str + ("\000" * (size-len(str)))
+        self.buffer.write(buf)
         
 class FileUnpacker(object):
     def __init__(self, filename):
-        f = open(filename,"rb")
-        self.data = f.read()
+        with open(filename,"rb") as f:
+            self.buffer = f.read()
         self.offset = 0
 
     def unpack(self,fmt):
-        result = struct.unpack_from(fmt, self.data, self.offset)
+        result = struct.unpack_from(fmt, self.buffer, self.offset)
         self.offset += struct.calcsize(fmt)
         return result
     
     def unpack_string(self, size=0):
-        result = ""
-        i = 0
-        while (self.data[self.offset+i] != "\000"):
-            result += self.data[self.offset+i]
-            i += 1
-        i += 1
+        # Nonzero size means we must consider exactly the next 'size'
+        # characters in self.buffer.
         if size:
-            # fixed-size field for the string
-            i = size
-        self.offset += i
+            self.offset += size
+            # but stop at the first nul byte
+            return self.buffer[self.offset-size:self.offset].split("\000", 1)[0]
+        # Zero size means consider everything until the next nul character.
+        result = self.buffer[self.offset:].split("\000", 1)[0]
+        # don't forget to skip the nul byte too
+        self.offset += len(result) + 1
         return result
 
 # translated from the C++ version in lldefs.h
@@ -108,7 +138,7 @@ def F32_to_U16(val, lower, upper):
 # translated from the C++ version in llquantize.h
 def U16_to_F32(ival, lower, upper):
     if ival < 0 or ival > U16MAX:
-        raise Exception("U16 out of range: "+ival)
+        raise ValueError("U16 out of range: %s" % ival)
     val = ival*OOU16MAX
     delta = (upper - lower)
     val *= delta
@@ -121,71 +151,100 @@ def U16_to_F32(ival, lower, upper):
         val = 0.0
     return val; 
 
-class BadFormat(Exception):
-    pass
-
 class RotKey(object):
-    def __init__(self):
-        pass
-
-    def unpack(self, anim, fup):
-        (self.time_short, ) = fup.unpack("<H")
-        self.time = U16_to_F32(self.time_short, 0.0, anim.duration)
+    def __init__(self, time, duration, rot):
+        """
+        This constructor instantiates a RotKey object from scratch, as it
+        were, converting from float time to time_short.
+        """
+        self.time = time
+        self.time_short = F32_to_U16(time, 0.0, duration) \
+                          if time is not None else None
+        self.rotation = rot
+
+    @staticmethod
+    def unpack(duration, fup):
+        """
+        This staticmethod constructs a RotKey by loadingfrom a FileUnpacker.
+        """
+        # cheat the other constructor
+        this = RotKey(None, None, None)
+        # load time_short directly from the file
+        (this.time_short, ) = fup.unpack("<H")
+        # then convert to float time
+        this.time = U16_to_F32(this.time_short, 0.0, duration)
+        # convert each coordinate of the rotation from short to float
         (x,y,z) = fup.unpack("<HHH")
-        self.rotation = [U16_to_F32(i, -1.0, 1.0) for i in (x,y,z)]
+        this.rotation = [U16_to_F32(i, -1.0, 1.0) for i in (x,y,z)]
+        return this
 
     def dump(self, f):
-        print >>f, "    rot_key: t",self.time,"st",self.time_short,"rot",",".join([str(f) for f in self.rotation])
+        print >>f, "    rot_key: t %.3f" % self.time,"st",self.time_short,"rot",",".join("%.3f" % f for f in self.rotation)
 
-    def pack(self, anim, fp):
-        if not hasattr(self,"time_short"):
-            self.time_short = F32_to_U16(self.time, 0.0, anim.duration)
+    def pack(self, fp):
         fp.pack("<H",self.time_short)
         (x,y,z) = [F32_to_U16(v, -1.0, 1.0) for v in self.rotation]
         fp.pack("<HHH",x,y,z)
         
 class PosKey(object):
-    def __init__(self):
-        pass
-
-    def unpack(self, anim, fup):
-        (self.time_short, ) = fup.unpack("<H")
-        self.time = U16_to_F32(self.time_short, 0.0, anim.duration)
+    def __init__(self, time, duration, pos):
+        """
+        This constructor instantiates a PosKey object from scratch, as it
+        were, converting from float time to time_short.
+        """
+        self.time = time
+        self.time_short = F32_to_U16(time, 0.0, duration) \
+                          if time is not None else None
+        self.position = pos
+
+    @staticmethod
+    def unpack(duration, fup):
+        """
+        This staticmethod constructs a PosKey by loadingfrom a FileUnpacker.
+        """
+        # cheat the other constructor
+        this = PosKey(None, None, None)
+        # load time_short directly from the file
+        (this.time_short, ) = fup.unpack("<H")
+        # then convert to float time
+        this.time = U16_to_F32(this.time_short, 0.0, duration)
+        # convert each coordinate of the rotation from short to float
         (x,y,z) = fup.unpack("<HHH")
-        self.position = [U16_to_F32(i, -LL_MAX_PELVIS_OFFSET, LL_MAX_PELVIS_OFFSET) for i in (x,y,z)]
+        this.position = [U16_to_F32(i, -LL_MAX_PELVIS_OFFSET, LL_MAX_PELVIS_OFFSET)
+                         for i in (x,y,z)]
+        return this
 
     def dump(self, f):
-        print >>f, "    pos_key: t",self.time,"pos ",",".join([str(f) for f in self.position])
+        print >>f, "    pos_key: t %.3f" % self.time,"pos ",",".join("%.3f" % f for f in self.position)
         
-    def pack(self, anim, fp):
-        if not hasattr(self,"time_short"):
-            self.time_short = F32_to_U16(self.time, 0.0, anim.duration)
+    def pack(self, fp):
         fp.pack("<H",self.time_short)
         (x,y,z) = [F32_to_U16(v, -LL_MAX_PELVIS_OFFSET, LL_MAX_PELVIS_OFFSET) for v in self.position]
         fp.pack("<HHH",x,y,z)
 
 class Constraint(object):
-    def __init__(self):
-        pass
-
-    def unpack(self, anim, fup):
-        (self.chain_length, self.constraint_type) = fup.unpack("<BB")
-        self.source_volume = fup.unpack_string(16)
-        self.source_offset = fup.unpack("<fff")
-        self.target_volume = fup.unpack_string(16)
-        self.target_offset = fup.unpack("<fff")
-        self.target_dir = fup.unpack("<fff")
-        fmt = "<ffff"
-        (self.ease_in_start, self.ease_in_stop, self.ease_out_start, self.ease_out_stop) = fup.unpack("<ffff")
-
-    def pack(self, anim, fp):
+    @staticmethod
+    def unpack(duration, fup):
+        this = Constraint()
+        (this.chain_length, this.constraint_type) = fup.unpack("<BB")
+        this.source_volume = fup.unpack_string(16)
+        this.source_offset = fup.unpack("<fff")
+        this.target_volume = fup.unpack_string(16)
+        this.target_offset = fup.unpack("<fff")
+        this.target_dir = fup.unpack("<fff")
+        (this.ease_in_start, this.ease_in_stop, this.ease_out_start, this.ease_out_stop) = \
+                             fup.unpack("<ffff")
+        return this
+
+    def pack(self, fp):
         fp.pack("<BB", self.chain_length, self.constraint_type)
         fp.pack_string(self.source_volume, 16)
         fp.pack("<fff", *self.source_offset)
         fp.pack_string(self.target_volume, 16)
         fp.pack("<fff", *self.target_offset)
         fp.pack("<fff", *self.target_dir)
-        fp.pack("<ffff", self.ease_in_start, self.ease_in_stop, self.ease_out_start, self.ease_out_stop)
+        fp.pack("<ffff", self.ease_in_start, self.ease_in_stop,
+                self.ease_out_start, self.ease_out_stop)
 
     def dump(self, f):
         print >>f, "  constraint:"
@@ -202,30 +261,26 @@ def dump(self, f):
         print >>f, "    ease_out_stop",self.ease_out_stop
         
 class Constraints(object):
-    def __init__(self):
-        pass
-
-    def unpack(self, anim, fup):
-        (self.num_constraints, ) = fup.unpack("<i")
-        self.constraints = []
-        for i in xrange(self.num_constraints):
-            constraint = Constraint()
-            constraint.unpack(anim, fup)
-            self.constraints.append(constraint)
-
-    def pack(self, anim, fp):
-        fp.pack("<i",self.num_constraints)
+    @staticmethod
+    def unpack(duration, fup):
+        this = Constraints()
+        (num_constraints, ) = fup.unpack("<i")
+        this.constraints = [Constraint.unpack(duration, fup)
+                            for i in xrange(num_constraints)]
+        return this
+
+    def pack(self, fp):
+        fp.pack("<i",len(self.constraints))
         for c in self.constraints:
-            c.pack(anim,fp)
+            c.pack(fp)
 
     def dump(self, f):
-        print >>f, "constraints:",self.num_constraints
+        print >>f, "constraints:",len(self.constraints)
         for c in self.constraints:
             c.dump(f)
 
 class PositionCurve(object):
     def __init__(self):
-        self.num_pos_keys = 0
         self.keys = []
 
     def is_static(self):
@@ -236,28 +291,27 @@ def is_static(self):
                     return False
         return True
 
-    def unpack(self, anim, fup):
-        (self.num_pos_keys, ) = fup.unpack("<i")
-        self.keys = []
-        for k in xrange(0,self.num_pos_keys):
-            pos_key = PosKey()
-            pos_key.unpack(anim, fup)
-            self.keys.append(pos_key)
+    @staticmethod
+    def unpack(duration, fup):
+        this = PositionCurve()
+        (num_pos_keys, ) = fup.unpack("<i")
+        this.keys = [PosKey.unpack(duration, fup)
+                     for k in xrange(num_pos_keys)]
+        return this
 
-    def pack(self, anim, fp):
-        fp.pack("<i",self.num_pos_keys)
+    def pack(self, fp):
+        fp.pack("<i",len(self.keys))
         for k in self.keys:
-            k.pack(anim, fp)
+            k.pack(fp)
 
     def dump(self, f):
         print >>f, "  position_curve:"
-        print >>f, "    num_pos_keys", self.num_pos_keys
-        for k in xrange(0,self.num_pos_keys):
-            self.keys[k].dump(f)
+        print >>f, "    num_pos_keys", len(self.keys)
+        for k in self.keys:
+            k.dump(f)
 
 class RotationCurve(object):
     def __init__(self):
-        self.num_rot_keys = 0
         self.keys = []
 
     def is_static(self):
@@ -268,42 +322,46 @@ def is_static(self):
                     return False
         return True
 
-    def unpack(self, anim, fup):
-        (self.num_rot_keys, ) = fup.unpack("<i")
-        self.keys = []
-        for k in xrange(0,self.num_rot_keys):
-            rot_key = RotKey()
-            rot_key.unpack(anim, fup)
-            self.keys.append(rot_key)
+    @staticmethod
+    def unpack(duration, fup):
+        this = RotationCurve()
+        (num_rot_keys, ) = fup.unpack("<i")
+        this.keys = [RotKey.unpack(duration, fup)
+                     for k in xrange(num_rot_keys)]
+        return this
 
-    def pack(self, anim, fp):
-        fp.pack("<i",self.num_rot_keys)
+    def pack(self, fp):
+        fp.pack("<i",len(self.keys))
         for k in self.keys:
-            k.pack(anim, fp)
+            k.pack(fp)
 
     def dump(self, f):
         print >>f, "  rotation_curve:"
-        print >>f, "    num_rot_keys", self.num_rot_keys
-        for k in xrange(0,self.num_rot_keys):
-            self.keys[k].dump(f)
+        print >>f, "    num_rot_keys", len(self.keys)
+        for k in self.keys:
+            k.dump(f)
             
 class JointInfo(object):
-    def __init__(self):
-        pass
-
-    def unpack(self, anim, fup):
-        self.joint_name = fup.unpack_string()
-        (self.joint_priority, ) = fup.unpack("<i")
+    def __init__(self, name, priority):
+        self.joint_name = name
+        self.joint_priority = priority
         self.rotation_curve = RotationCurve()
-        self.rotation_curve.unpack(anim, fup)
         self.position_curve = PositionCurve()
-        self.position_curve.unpack(anim, fup)
 
-    def pack(self, anim, fp):
+    @staticmethod
+    def unpack(duration, fup):
+        this = JointInfo(None, None)
+        this.joint_name = fup.unpack_string()
+        (this.joint_priority, ) = fup.unpack("<i")
+        this.rotation_curve = RotationCurve.unpack(duration, fup)
+        this.position_curve = PositionCurve.unpack(duration, fup)
+        return this
+
+    def pack(self, fp):
         fp.pack_string(self.joint_name)
         fp.pack("<i", self.joint_priority)
-        self.rotation_curve.pack(anim, fp)
-        self.position_curve.pack(anim, fp)
+        self.rotation_curve.pack(fp)
+        self.position_curve.pack(fp)
 
     def dump(self, f):
         print >>f, "joint:"
@@ -313,13 +371,26 @@ def dump(self, f):
         self.position_curve.dump(f)
 
 class Anim(object):
-    def __init__(self, filename=None):
+    def __init__(self, filename=None, verbose=False):
+        # set this FIRST as it's consulted by read() and unpack()
+        self.verbose = verbose
         if filename:
             self.read(filename)
 
     def read(self, filename):
         fup = FileUnpacker(filename)
-        self.unpack(fup)
+        try:
+            self.unpack(fup)
+        except struct.error as err:
+            raise BadFormat("error reading %s: %s" % (filename, err))
+        # By the end of streaming data in from our FileUnpacker, we should
+        # have consumed the entire thing. If there's excess data, it's
+        # entirely possible that this is a garbage file that happens to
+        # resemble a valid degenerate .anim file, e.g. with zero counts of
+        # things.
+        if fup.offset != len(fup.buffer):
+            raise ExtraneousData("extraneous data in %s; is it really a Linden .anim file?" %
+                                 filename)
 
     # various validity checks could be added - see LLKeyframeMotion::deserialize()
     def unpack(self,fup):
@@ -333,27 +404,57 @@ def unpack(self,fup):
         else:
             raise BadFormat("Bad combination of version, sub_version: %d %d" % (self.version, self.sub_version))
 
+        # Also consult BVH conversion code for stricter checks
+
+        # C++ deserialize() checks self.base_priority against
+        # LLJoint::ADDITIVE_PRIORITY and LLJoint::USE_MOTION_PRIORITY,
+        # possibly sets self.max_priority
+        # checks self.duration against MAX_ANIM_DURATION !!
+        # checks self.emote_name != str(self.ID)
+        # checks self.hand_pose against LLHandMotion::NUM_HAND_POSES !!
+        # checks 0 < num_joints <= LL_CHARACTER_MAX_JOINTS (no need --
+        # validate names)
+        # checks each joint_name neither "mScreen" nor "mRoot" ("attempted to
+        # animate special joint") !!
+        # checks each joint_name can be found in mCharacter
+        # checks each joint_priority >= LLJoint::USE_MOTION_PRIORITY
+        # tracks max observed joint_priority, excluding USE_MOTION_PRIORITY
+        # checks each 0 <= RotKey.time <= self.duration !!
+        # checks each RotKey.rotation.isFinite() !!
+        # checks each PosKey.position.isFinite() !!
+        # checks 0 <= num_constraints <= MAX_CONSTRAINTS  !!
+        # checks each Constraint.chain_length <= num_joints
+        # checks each Constraint.constraint_type < NUM_CONSTRAINT_TYPES !!
+        # checks each Constraint.source_offset.isFinite() !!
+        # checks each Constraint.target_offset.isFinite() !!
+        # checks each Constraint.target_dir.isFinite() !!
+        # from https://bitbucket.org/lindenlab/viewer-release/src/827a910542a9af0a39b0ca03663c02e5c83869ea/indra/llcharacter/llkeyframemotion.cpp?at=default&fileviewer=file-view-default#llkeyframemotion.cpp-1812 :
+        # find joint to which each Constraint's collision volume is attached;
+        # for each link in Constraint.chain_length, walk to joint's parent,
+        # find that parent in list of joints, set its index in index list
+
         self.emote_name = fup.unpack_string()
         
-        (self.loop_in_point, self.loop_out_point, self.loop, self.ease_in_duration, self.ease_out_duration, self.hand_pose, self.num_joints) = fup.unpack("@ffiffII")
+        (self.loop_in_point, self.loop_out_point, self.loop,
+         self.ease_in_duration, self.ease_out_duration, self.hand_pose, num_joints) = \
+            fup.unpack("@ffiffII")
         
-        self.joints = []
-        for j in xrange(0,self.num_joints):
-            joint_info = JointInfo()
-            joint_info.unpack(self, fup)
-            self.joints.append(joint_info)
-            print "unpacked joint",joint_info.joint_name
-        self.constraints = Constraints()
-        self.constraints.unpack(self, fup)
-        self.data = fup.data
+        self.joints = [JointInfo.unpack(self.duration, fup)
+                       for j in xrange(num_joints)]
+        if self.verbose:
+            for joint_info in self.joints:
+                print "unpacked joint",joint_info.joint_name
+        self.constraints = Constraints.unpack(self.duration, fup)
+        self.buffer = fup.buffer
         
     def pack(self, fp):
         fp.pack("@HHhf", self.version, self.sub_version, self.base_priority, self.duration)
         fp.pack_string(self.emote_name, 0)
-        fp.pack("@ffiffII", self.loop_in_point, self.loop_out_point, self.loop, self.ease_in_duration, self.ease_out_duration, self.hand_pose, self.num_joints)
+        fp.pack("@ffiffII", self.loop_in_point, self.loop_out_point, self.loop,
+                self.ease_in_duration, self.ease_out_duration, self.hand_pose, len(self.joints))
         for j in self.joints:
-            j.pack(anim, fp)
-        self.constraints.pack(anim, fp)
+            j.pack(fp)
+        self.constraints.pack(fp)
 
     def dump(self, filename="-"):
         if filename=="-":
@@ -370,7 +471,7 @@ def dump(self, filename="-"):
         print >>f, "ease_in_duration: ", self.ease_in_duration
         print >>f, "ease_out_duration: ", self.ease_out_duration
         print >>f, "hand_pose", self.hand_pose
-        print >>f, "num_joints", self.num_joints
+        print >>f, "num_joints", len(self.joints)
         for j in self.joints:
             j.dump(f)
         self.constraints.dump(f)
@@ -382,10 +483,9 @@ def write(self, filename):
 
     def write_src_data(self, filename):
         print "write file",filename
-        f = open(filename,"wb")
-        f.write(self.data)
-        f.close()
-        
+        with open(filename,"wb") as f:
+            f.write(self.buffer)
+
     def find_joint(self, name):
         joints = [j for j in self.joints if j.joint_name == name]
         if joints:
@@ -395,91 +495,71 @@ def find_joint(self, name):
 
     def add_joint(self, name, priority):
         if not self.find_joint(name):
-            j = JointInfo()
-            j.joint_name = name
-            j.joint_priority = priority
-            j.rotation_curve = RotationCurve()
-            j.position_curve = PositionCurve()
-            self.joints.append(j)
-            self.num_joints = len(self.joints)
+            self.joints.append(JointInfo(name, priority))
 
     def delete_joint(self, name):
         j = self.find_joint(name)
         if j:
-            if args.verbose:
+            if self.verbose:
                 print "removing joint", name
-            anim.joints.remove(j)
-            anim.num_joints = len(self.joints)
+            self.joints.remove(j)
         else:
-            if args.verbose:
+            if self.verbose:
                 print "joint not found to remove", name
 
     def summary(self):
         nj = len(self.joints)
         nz = len([j for j in self.joints if j.joint_priority > 0])
-        nstatic = len([j for j in self.joints if j.rotation_curve.is_static() and j.position_curve.is_static()])
+        nstatic = len([j for j in self.joints
+                       if j.rotation_curve.is_static()
+                       and j.position_curve.is_static()])
         print "summary: %d joints, non-zero priority %d, static %d" % (nj, nz, nstatic)
 
     def add_pos(self, joint_names, positions):
         js = [joint for joint in self.joints if joint.joint_name in joint_names]
         for j in js:
-            if args.verbose:
+            if self.verbose:
                 print "adding positions",j.joint_name,positions
             j.joint_priority = 4
-            j.position_curve.num_pos_keys = len(positions)
-            j.position_curve.keys = []
-            for i,pos in enumerate(positions):
-                key = PosKey()
-                key.time = self.duration * i / (len(positions) - 1)
-                key.time_short = F32_to_U16(key.time, 0.0, self.duration)
-                key.position = pos
-                j.position_curve.keys.append(key)
+            j.position_curve.keys = [PosKey(self.duration * i / (len(positions) - 1),
+                                            self.duration,
+                                            pos)
+                                     for i,pos in enumerate(positions)]
 
     def add_rot(self, joint_names, rotations):
         js = [joint for joint in self.joints if joint.joint_name in joint_names]
         for j in js:
             print "adding rotations",j.joint_name
             j.joint_priority = 4
-            j.rotation_curve.num_rot_keys = len(rotations)
-            j.rotation_curve.keys = []
-            for i,pos in enumerate(rotations):
-                key = RotKey()
-                key.time = self.duration * i / (len(rotations) - 1)
-                key.time_short = F32_to_U16(key.time, 0.0, self.duration)
-                key.rotation = pos
-                j.rotation_curve.keys.append(key)
+            j.rotation_curve.keys = [RotKey(self.duration * i / (len(rotations) - 1),
+                                            self.duration,
+                                            rot)
+                                     for i,rot in enumerate(rotations)]
 
 def twistify(anim, joint_names, rot1, rot2):
     js = [joint for joint in anim.joints if joint.joint_name in joint_names]
     for j in js:
         print "twisting",j.joint_name
-        print j.rotation_curve.num_rot_keys
+        print len(j.rotation_curve.keys)
         j.joint_priority = 4
-        j.rotation_curve.num_rot_keys = 2
-        j.rotation_curve.keys = []
-        key1 = RotKey()
-        key1.time_short = 0
-        key1.time = U16_to_F32(key1.time_short, 0.0, anim.duration)
-        key1.rotation = rot1
-        key2 = RotKey()
-        key2.time_short = U16MAX
-        key2.time = U16_to_F32(key2.time_short, 0.0, anim.duration)
-        key2.rotation = rot2
-        j.rotation_curve.keys.append(key1)
-        j.rotation_curve.keys.append(key2)
+        # Set the joint(s) to rot1 at time 0, rot2 at the full duration.
+        j.rotation_curve.keys = [
+            RotKey(0.0, anim.duration, rot1),
+            RotKey(anim.duration, anim.duration, rot2)]
 
 def float_triple(arg):
     vals = arg.split()
     if len(vals)==3:
         return [float(x) for x in vals]
     else:
-        raise Exception("arg %s does not resolve to a float triple" % arg)
+        raise ValueError("arg %s does not resolve to a float triple" % arg)
 
 def get_joint_by_name(tree,name):
     if tree is None:
         return None
-    matches = [elt for elt in tree.getroot().iter() if \
-                   elt.get("name")==name and elt.tag in ["bone", "collision_volume", "attachment_point"]]
+    matches = [elt for elt in tree.getroot().iter()
+               if elt.get("name")==name
+               and elt.tag in ["bone", "collision_volume", "attachment_point"]]
     if len(matches)==1:
         return matches[0]
     elif len(matches)>1:
@@ -496,121 +576,135 @@ def get_elt_pos(elt):
     else:
         return (0.0, 0.0, 0.0)
 
-def resolve_joints(names, skel_tree, lad_tree):
-    print "resolve joints, no_hud is",args.no_hud
+def resolve_joints(names, skel_tree, lad_tree, no_hud=False):
+    print "resolve joints, no_hud is",no_hud
     if skel_tree and lad_tree:
         all_elts = [elt for elt in skel_tree.getroot().iter()]
         all_elts.extend([elt for elt in lad_tree.getroot().iter()])
-        matches = []
+        matches = set()
         for elt in all_elts:
             if elt.get("name") is None:
                 continue
             #print elt.get("name"),"hud",elt.get("hud")
-            if args.no_hud and elt.get("hud"):
+            if no_hud and elt.get("hud"):
                 #print "skipping hud joint", elt.get("name")
                 continue
             if elt.get("name") in names or elt.tag in names:
-                matches.append(elt.get("name"))
-        return list(set(matches))
+                matches.add(elt.get("name"))
+        return list(matches)
     else:
         return names
 
-if __name__ == "__main__":
+def main(*argv):
+    import argparse
 
     # default search location for config files is defined relative to
     # the script location; assuming they live in the same viewer repo
+    # Use sys.argv[0] because (a) this script lives where it lives regardless
+    # of what our caller passes and (b) we don't expect our caller to pass the
+    # script name anyway.
     pathname = os.path.dirname(sys.argv[0])        
-    path_to_skel = os.path.join(os.path.abspath(pathname),"..","..","indra","newview","character")
+    # we're in scripts/content_tools; hop back to base of repository clone
+    path_to_skel = os.path.join(os.path.abspath(pathname),os.pardir,os.pardir,
+                                "indra","newview","character")
 
     parser = argparse.ArgumentParser(description="process SL animations")
     parser.add_argument("--verbose", help="verbose flag", action="store_true")
-    parser.add_argument("--dump", help="dump to specified file")
+    parser.add_argument("--dump", metavar="FILEPATH", help="dump to specified file")
     parser.add_argument("--rot", help="specify sequence of rotations", type=float_triple, nargs="+")
-    parser.add_argument("--rand_pos", help="request random positions", action="store_true")
+    parser.add_argument("--rand_pos", help="request NUM random positions (default %(default)s)",
+                        metavar="NUM", type=int, default=2)
     parser.add_argument("--reset_pos", help="request original positions", action="store_true")
     parser.add_argument("--pos", help="specify sequence of positions", type=float_triple, nargs="+")
-    parser.add_argument("--num_pos", help="number of positions to create", type=int, default=2)
-    parser.add_argument("--delete_joints", help="specify joints to be deleted", nargs="+")
-    parser.add_argument("--joints", help="specify joints to be added or modified", nargs="+")
+    parser.add_argument("--delete_joints", help="specify joints to be deleted", nargs="+",
+                        metavar="JOINT")
+    parser.add_argument("--joints", help="specify joints to be added or modified", nargs="+",
+                        metavar="JOINT")
     parser.add_argument("--summary", help="print summary of the output animation", action="store_true")
-    parser.add_argument("--skel", help="name of the avatar_skeleton file", default= os.path.join(path_to_skel,"avatar_skeleton.xml"))
-    parser.add_argument("--lad", help="name of the avatar_lad file", default= os.path.join(path_to_skel,"avatar_lad.xml"))
-    parser.add_argument("--set_version", nargs=2, type=int, help="set version and sub-version to specified values")
+    parser.add_argument("--skel", help="name of the avatar_skeleton file (default %(default)s)",
+                        default=os.path.join(path_to_skel,"avatar_skeleton.xml"),
+                        metavar="FILEPATH")
+    parser.add_argument("--lad", help="name of the avatar_lad file (default %(default)s)",
+                        default=os.path.join(path_to_skel,"avatar_lad.xml"),
+                        metavar="FILEPATH")
+    parser.add_argument("--set_version", nargs=2, type=int,
+                        help="set version and sub-version to specified values",
+                        metavar=("VERSION", "SUB-VERSION"))
     parser.add_argument("--no_hud", help="omit hud joints from list of attachments", action="store_true")
     parser.add_argument("--base_priority", help="set base priority", type=int)
     parser.add_argument("--joint_priority", help="set joint priority for all joints", type=int)
     parser.add_argument("infilename", help="name of a .anim file to input")
     parser.add_argument("outfilename", nargs="?", help="name of a .anim file to output")
-    args = parser.parse_args()
+    args = parser.parse_args(argv)
 
-    print "anim_tool.py: " + " ".join(sys.argv)
+    print "anim_tool.py: " + " ".join(argv)
     print "dump is", args.dump
     print "infilename",args.infilename,"outfilename",args.outfilename
     print "rot",args.rot
     print "pos",args.pos
     print "joints",args.joints
 
-    try:
-        anim = Anim(args.infilename)
-        skel_tree = None
-        lad_tree = None
-        joints = []
-        if args.skel:
-            skel_tree = etree.parse(args.skel)
-            if skel_tree is None:
-                print "failed to parse",args.skel
-                exit(1)
-        if args.lad:
-            lad_tree = etree.parse(args.lad)
-            if lad_tree is None:
-                print "failed to parse",args.lad
-                exit(1)
-        if args.joints:
-            joints = resolve_joints(args.joints, skel_tree, lad_tree)
-            if args.verbose:
-                print "joints resolved to",joints
-            for name in joints:
-                anim.add_joint(name,0)
-        if args.delete_joints:
-            for name in args.delete_joints:
-                anim.delete_joint(name)
-        if joints and args.rot:
-            anim.add_rot(joints, args.rot)
-        if joints and args.pos:
-            anim.add_pos(joints, args.pos)
-        if joints and args.rand_pos:
-            for joint in joints:
-                pos_array = list(tuple(random.uniform(-1,1) for i in xrange(3)) for j in xrange(args.num_pos))
-                pos_array.append(pos_array[0])
-                anim.add_pos([joint], pos_array)
-        if joints and args.reset_pos:
-            for joint in joints:
-                elt = get_joint_by_name(skel_tree,joint)
-                if elt is None:
-                    elt = get_joint_by_name(lad_tree,joint)
-                if elt is not None:
-                    pos_array = []
-                    pos_array.append(get_elt_pos(elt))
-                    pos_array.append(pos_array[0])
-                    anim.add_pos([joint], pos_array)
-                else:
-                    print "no elt or no pos data for",joint
-        if args.set_version:
-            anim.version = args.set_version[0]
-            anim.sub_version = args.set_version[1]
-        if args.base_priority is not None:
-            print "set base priority",args.base_priority
-            anim.base_priority = args.base_priority
-        if args.joint_priority is not None:
-            print "set joint priority",args.joint_priority
-            for joint in anim.joints:
-                joint.joint_priority = args.joint_priority
-        if args.dump:
-            anim.dump(args.dump)
-        if args.summary:
-            anim.summary()
-        if args.outfilename:
-            anim.write(args.outfilename)
-    except:
-        raise
+    anim = Anim(args.infilename, args.verbose)
+    skel_tree = None
+    lad_tree = None
+    joints = []
+    if args.skel:
+        skel_tree = ElementTree.parse(args.skel)
+        if skel_tree is None:
+            raise Error("failed to parse " + args.skel)
+    if args.lad:
+        lad_tree = ElementTree.parse(args.lad)
+        if lad_tree is None:
+            raise Error("failed to parse " + args.lad)
+    if args.joints:
+        joints = resolve_joints(args.joints, skel_tree, lad_tree, args.no_hud)
+        if args.verbose:
+            print "joints resolved to",joints
+        for name in joints:
+            anim.add_joint(name,0)
+    if args.delete_joints:
+        for name in args.delete_joints:
+            anim.delete_joint(name)
+    if joints and args.rot:
+        anim.add_rot(joints, args.rot)
+    if joints and args.pos:
+        anim.add_pos(joints, args.pos)
+    if joints and args.rand_pos:
+        # pick a random sequence of positions for each joint specified
+        for joint in joints:
+            # generate a list of rand_pos triples
+            pos_array = [tuple(random.uniform(-1,1) for i in xrange(3))
+                         for j in xrange(args.rand_pos)]
+            # close the loop by cycling back to the first entry
+            pos_array.append(pos_array[0])
+            anim.add_pos([joint], pos_array)
+    if joints and args.reset_pos:
+        for joint in joints:
+            elt = get_joint_by_name(skel_tree,joint) or get_joint_by_name(lad_tree,joint)
+            if elt is not None:
+                anim.add_pos([joint], 2*[get_elt_pos(elt)])
+            else:
+                print "no elt or no pos data for",joint
+    if args.set_version:
+        anim.version, anim.sub_version = args.set_version
+    if args.base_priority is not None:
+        print "set base priority",args.base_priority
+        anim.base_priority = args.base_priority
+    # --joint_priority sets priority for ALL joints, not just the explicitly-
+    # specified ones
+    if args.joint_priority is not None:
+        print "set joint priority",args.joint_priority
+        for joint in anim.joints:
+            joint.joint_priority = args.joint_priority
+    if args.dump:
+        anim.dump(args.dump)
+    if args.summary:
+        anim.summary()
+    if args.outfilename:
+        anim.write(args.outfilename)
 
+if __name__ == "__main__":
+    try:
+        sys.exit(main(*sys.argv[1:]))
+    except Error as err:
+        sys.exit("%s: %s" % (err.__class__.__name__, err))