Index: /OniSplit/Akira/AkiraDaeNodeProperties.cs
===================================================================
--- /OniSplit/Akira/AkiraDaeNodeProperties.cs	(revision 1114)
+++ /OniSplit/Akira/AkiraDaeNodeProperties.cs	(revision 1114)
@@ -0,0 +1,9 @@
+﻿namespace Oni.Akira
+{
+    internal abstract class AkiraDaeNodeProperties
+    {
+        public bool HasPhysics;
+        public int ScriptId;
+        public GunkFlags GunkFlags;
+    }
+}
Index: /OniSplit/Akira/AkiraDaeReader.cs
===================================================================
--- /OniSplit/Akira/AkiraDaeReader.cs	(revision 1114)
+++ /OniSplit/Akira/AkiraDaeReader.cs	(revision 1114)
@@ -0,0 +1,347 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class AkiraDaeReader
+    {
+        #region Private data
+        private readonly PolygonMesh mesh;
+        private readonly List<Vector3> positions;
+        private readonly Dictionary<Vector3, int> uniquePositions;
+        private readonly List<Vector3> normals;
+        private readonly List<Vector2> texCoords;
+        private readonly Dictionary<Dae.Material, Material> materialMap;
+        private readonly Dictionary<string, Material> materialFileMap;
+        private readonly Stack<Matrix> nodeTransformStack;
+
+        private Dae.Scene scene;
+        private Dictionary<string, AkiraDaeNodeProperties> properties;
+        private Matrix nodeTransform;
+        private string nodeName;
+        #endregion
+
+        public static PolygonMesh Read(IEnumerable<string> filePaths)
+        {
+            var reader = new AkiraDaeReader();
+            var properties = new Dictionary<string, AkiraDaeNodeProperties>();
+
+            foreach (var filePath in filePaths)
+                reader.ReadScene(Dae.Reader.ReadFile(filePath), properties);
+
+            return reader.mesh;
+        }
+
+        public AkiraDaeReader()
+        {
+            mesh = new PolygonMesh(new MaterialLibrary());
+
+            positions = mesh.Points;
+            uniquePositions = new Dictionary<Vector3, int>();
+            texCoords = mesh.TexCoords;
+            normals = mesh.Normals;
+
+            materialMap = new Dictionary<Dae.Material, Material>();
+            materialFileMap = new Dictionary<string, Material>(StringComparer.OrdinalIgnoreCase);
+
+            nodeTransformStack = new Stack<Matrix>();
+            nodeTransform = Matrix.Identity;
+        }
+
+        public PolygonMesh Mesh => mesh;
+
+        public void ReadScene(Dae.Scene scene, Dictionary<string, AkiraDaeNodeProperties> properties)
+        {
+            this.scene = scene;
+            this.properties = properties;
+
+            AkiraDaeNodeProperties sceneProperties;
+            properties.TryGetValue(scene.Id, out sceneProperties);
+
+            foreach (var node in scene.Nodes)
+                ReadNode(node, sceneProperties);
+        }
+
+        private void ReadNode(Dae.Node node, AkiraDaeNodeProperties parentNodeProperties)
+        {
+            AkiraDaeNodeProperties nodeProperties;
+
+            if (!properties.TryGetValue(node.Id, out nodeProperties))
+                nodeProperties = parentNodeProperties;
+            else if (nodeProperties.HasPhysics)
+                return;
+
+            nodeTransformStack.Push(nodeTransform);
+
+            foreach (var transform in node.Transforms)
+                nodeTransform = transform.ToMatrix() * nodeTransform;
+
+            nodeName = node.Name;
+
+            foreach (var geometryInstance in node.GeometryInstances)
+                ReadGeometryInstance(node, nodeProperties, geometryInstance);
+
+            foreach (var child in node.Nodes)
+                ReadNode(child, nodeProperties);
+
+            nodeTransform = nodeTransformStack.Pop();
+        }
+
+        private void ReadGeometryInstance(Dae.Node node, AkiraDaeNodeProperties nodeProperties, Dae.GeometryInstance instance)
+        {
+            foreach (var primitives in instance.Target.Primitives)
+            {
+                if (primitives.PrimitiveType != Dae.MeshPrimitiveType.Polygons)
+                {
+                    Console.Error.WriteLine("Unsupported primitive type '{0}' found in geometry '{1}', ignoring.", primitives.PrimitiveType, instance.Name);
+                    continue;
+                }
+
+                ReadPolygonPrimitives(node, nodeProperties, primitives, instance.Materials.Find(m => m.Symbol == primitives.MaterialSymbol));
+            }
+        }
+
+        private Material ReadMaterial(Dae.Material material)
+        {
+            if (material == null || material.Effect == null)
+                return null;
+
+            Material polygonMaterial;
+
+            if (materialMap.TryGetValue(material, out polygonMaterial))
+                return polygonMaterial;
+
+            Dae.EffectSampler diffuseSampler = null;
+            Dae.EffectSampler transparentSampler = null;
+
+            foreach (var texture in material.Effect.Textures)
+            {
+                if (texture.Channel == Dae.EffectTextureChannel.Diffuse)
+                    diffuseSampler = texture.Sampler;
+                else if (texture.Channel == Dae.EffectTextureChannel.Transparent)
+                    transparentSampler = texture.Sampler;
+            }
+
+            if (diffuseSampler == null || diffuseSampler.Surface == null || diffuseSampler.Surface.InitFrom == null)
+            {
+                //
+                // this material doesn't have a diffuse texture
+                //
+
+                return null;
+            }
+
+            var image = diffuseSampler.Surface.InitFrom;
+
+            if (materialFileMap.TryGetValue(image.FilePath, out polygonMaterial))
+                return polygonMaterial;
+
+            polygonMaterial = mesh.Materials.GetMaterial(Path.GetFileNameWithoutExtension(image.FilePath));
+            polygonMaterial.ImageFilePath = image.FilePath;
+
+            if (transparentSampler == diffuseSampler)
+                polygonMaterial.Flags |= GunkFlags.Transparent | GunkFlags.NoOcclusion | GunkFlags.TwoSided;
+
+            materialFileMap.Add(image.FilePath, polygonMaterial);
+            materialMap.Add(material, polygonMaterial);
+
+            return polygonMaterial;
+        }
+
+        private void ReadPolygonPrimitives(Dae.Node node, AkiraDaeNodeProperties nodeProperties, Dae.MeshPrimitives primitives, Dae.MaterialInstance materialInstance)
+        {
+            Material material = null;
+
+            if (materialInstance != null)
+                material = ReadMaterial(materialInstance.Target);
+
+            if (material == null)
+                material = mesh.Materials.NotFound;
+
+            int[] positionIndices = null;
+            int[] texCoordIndices = null;
+            int[] normalIndices = null;
+            Color[] colors = null;
+
+            foreach (var input in primitives.Inputs)
+            {
+                switch (input.Semantic)
+                {
+                    case Dae.Semantic.Position:
+                        positionIndices = ReadInputIndexed(input, positions, uniquePositions, PositionReader);
+                        break;
+
+                    case Dae.Semantic.TexCoord:
+                        texCoordIndices = ReadInputIndexed(input, texCoords, Dae.Source.ReadTexCoord);
+                        break;
+
+                    case Dae.Semantic.Normal:
+                        normalIndices = ReadInputIndexed(input, normals, Dae.Source.ReadVector3);
+                        break;
+
+                    case Dae.Semantic.Color:
+                        colors = ReadInput(input, Dae.Source.ReadColor);
+                        break;
+                }
+            }
+
+            if (texCoordIndices == null)
+                Console.Error.WriteLine("Geometry '{0}' does not contain texture coordinates.", nodeName);
+
+            int startIndex = 0;
+            int degeneratePolygonCount = 0;
+
+            foreach (int vertexCount in primitives.VertexCounts)
+            {
+                var polygonPointIndices = new int[vertexCount];
+                Array.Copy(positionIndices, startIndex, polygonPointIndices, 0, vertexCount);
+
+                if (CheckDegenerate(positions, polygonPointIndices))
+                {
+                    degeneratePolygonCount++;
+                    startIndex += vertexCount;
+                    continue;
+                }
+
+                var polygon = new Polygon(mesh, polygonPointIndices)
+                {
+                    FileName = node.FileName,
+                    ObjectName = node.Name,
+                    Material = material
+                };
+
+                if (texCoordIndices != null)
+                {
+                    polygon.TexCoordIndices = new int[vertexCount];
+                    Array.Copy(texCoordIndices, startIndex, polygon.TexCoordIndices, 0, vertexCount);
+                }
+                else
+                {
+                    polygon.TexCoordIndices = new int[vertexCount];
+                }
+
+                if (normalIndices != null)
+                {
+                    polygon.NormalIndices = new int[vertexCount];
+                    Array.Copy(normalIndices, startIndex, polygon.NormalIndices, 0, vertexCount);
+                }
+
+                if (colors != null)
+                {
+                    polygon.Colors = new Color[vertexCount];
+                    Array.Copy(colors, startIndex, polygon.Colors, 0, vertexCount);
+                }
+
+                startIndex += vertexCount;
+
+                if (nodeProperties != null)
+                {
+                    polygon.ScriptId = nodeProperties.ScriptId;
+                    polygon.Flags |= nodeProperties.GunkFlags;
+                }
+
+                if (material == mesh.Materials.Markers.Ghost)
+                    mesh.Ghosts.Add(polygon);
+                else if (material == mesh.Materials.Markers.DoorFrame)
+                    mesh.Doors.Add(polygon);
+                else if (material.Name.StartsWith("bnv_grid_", StringComparison.Ordinal))
+                    mesh.Floors.Add(polygon);
+                else
+                    mesh.Polygons.Add(polygon);
+            }
+
+            if (degeneratePolygonCount > 0)
+            {
+                Console.Error.WriteLine("Ignoring {0} degenerate polygons", degeneratePolygonCount);
+            }
+        }
+
+        private static bool CheckDegenerate(List<Vector3> positions, int[] positionIndices)
+        {
+            if (positionIndices.Length < 3)
+                return true;
+
+            var p0 = positions[positionIndices[0]];
+            var p1 = positions[positionIndices[1]];
+
+            for (int i = 2; i < positionIndices.Length; i++)
+            {
+                var p2 = positions[positionIndices[i]];
+
+                var s0 = p0 - p1;
+                var s1 = p2 - p1;
+
+                Vector3 c;
+                Vector3.Cross(ref s0, ref s1, out c);
+
+                if (Math.Abs(c.LengthSquared()) < 0.0001f && Vector3.Dot(s0, s1) > 0.0f)
+                    return true;
+
+                p0 = p1;
+                p1 = p2;
+            }
+
+            return false;
+        }
+
+        private static int[] ReadInputIndexed<T>(Dae.IndexedInput input, List<T> list, Func<Dae.Source, int, T> elementReader)
+            where T : struct
+        {
+            var indices = new int[input.Indices.Count];
+
+            for (int i = 0; i < input.Indices.Count; i++)
+            {
+                var v = elementReader(input.Source, input.Indices[i]);
+                indices[i] = list.Count;
+                list.Add(v);
+            }
+
+            return indices;
+        }
+
+        private static int[] ReadInputIndexed<T>(Dae.IndexedInput input, List<T> list, Dictionary<T, int> uniqueList, Func<Dae.Source, int, T> elementReader)
+            where T : struct
+        {
+            var indices = new int[input.Indices.Count];
+
+            for (int i = 0; i < input.Indices.Count; i++)
+            {
+                var v = elementReader(input.Source, input.Indices[i]);
+
+                int index;
+
+                if (!uniqueList.TryGetValue(v, out index))
+                {
+                    index = list.Count;
+                    list.Add(v);
+                    uniqueList.Add(v, index);
+                }
+
+                indices[i] = index;
+            }
+
+            return indices;
+        }
+
+        private static T[] ReadInput<T>(Dae.IndexedInput input, Func<Dae.Source, int, T> elementReader)
+            where T : struct
+        {
+            var values = new T[input.Indices.Count];
+
+            for (int i = 0; i < input.Indices.Count; i++)
+                values[i] = elementReader(input.Source, input.Indices[i]);
+
+            return values;
+        }
+
+        private Vector3 PositionReader(Dae.Source source, int index)
+        {
+            Vector3 p = Dae.Source.ReadVector3(source, index);
+            Vector3 r;
+            Vector3.Transform(ref p, ref nodeTransform, out r);
+            return r;
+        }
+    }
+}
Index: /OniSplit/Akira/AkiraDaeWriter.cs
===================================================================
--- /OniSplit/Akira/AkiraDaeWriter.cs	(revision 1114)
+++ /OniSplit/Akira/AkiraDaeWriter.cs	(revision 1114)
@@ -0,0 +1,726 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class AkiraDaeWriter
+    {
+        #region Private data
+        private readonly PolygonMesh source;
+        private DaeSceneBuilder world;
+        private DaeSceneBuilder worldMarkers;
+        private DaeSceneBuilder[] objects;
+        private DaeSceneBuilder rooms;
+        private Dictionary<int, DaeSceneBuilder> scripts;
+
+        private static readonly string[] objectTypeNames = new[] {
+            "",
+            "char",
+            "patr",
+            "door",
+            "flag",
+            "furn",
+            "",
+            "",
+            "part",
+            "pwru",
+            "sndg",
+            "trgv",
+            "weap",
+            "trig",
+            "turr",
+            "cons",
+            "cmbt",
+            "mele",
+            "neut"
+        };
+
+        #endregion
+
+        #region private class DaePolygon
+
+        private class DaePolygon
+        {
+            private readonly Polygon source;
+            private readonly Material material;
+            private readonly int[] pointIndices;
+            private readonly int[] texCoordIndices;
+            private readonly int[] colorIndices;
+
+            public DaePolygon(Polygon source, int[] pointIndices, int[] texCoordIndices, int[] colorIndices)
+            {
+                this.source = source;
+                material = source.Material;
+                this.pointIndices = pointIndices;
+                this.texCoordIndices = texCoordIndices;
+                this.colorIndices = colorIndices;
+            }
+
+            public DaePolygon(Material material, int[] pointIndices, int[] texCoordIndices)
+            {
+                this.material = material;
+                this.pointIndices = pointIndices;
+                this.texCoordIndices = texCoordIndices;
+            }
+
+            public Polygon Source => source;
+            public Material Material => material;
+            public int[] PointIndices => pointIndices;
+            public int[] TexCoordIndices => texCoordIndices;
+            public int[] ColorIndices => colorIndices;
+        }
+
+        #endregion
+        #region private class DaeMeshBuilder
+
+        private class DaeMeshBuilder
+        {
+            private readonly List<DaePolygon> polygons = new List<DaePolygon>();
+            private readonly List<Vector3> points = new List<Vector3>();
+            private readonly Dictionary<Vector3, int> uniquePoints = new Dictionary<Vector3, int>();
+            private readonly List<Vector2> texCoords = new List<Vector2>();
+            private readonly Dictionary<Vector2, int> uniqueTexCoords = new Dictionary<Vector2, int>();
+            private readonly List<Color> colors = new List<Color>();
+            private readonly Dictionary<Color, int> uniqueColors = new Dictionary<Color, int>();
+            private string name;
+            private Vector3 translation;
+            private Dae.Geometry geometry;
+
+            public DaeMeshBuilder(string name)
+            {
+                this.name = name;
+            }
+
+            public string Name
+            {
+                get { return name; }
+                set { name = value; }
+            }
+
+            public Vector3 Translation => translation;
+
+            public void ResetTransform()
+            {
+                //
+                // Attempt to un-bake the translation of the furniture
+                //
+
+                var center = BoundingSphere.CreateFromPoints(points).Center;
+                center.Y = BoundingBox.CreateFromPoints(points).Min.Y;
+
+                translation = center;
+
+                for (int i = 0; i < points.Count; i++)
+                    points[i] -= center;
+            }
+
+            public void AddPolygon(Material material, Vector3[] polygonPoints, Vector2[] polygonTexCoords)
+            {
+                polygons.Add(new DaePolygon(
+                    material,
+                    Remap(polygonPoints, points, uniquePoints),
+                    Remap(polygonTexCoords, texCoords, uniqueTexCoords)));
+            }
+
+            public void AddPolygon(Polygon polygon)
+            {
+                polygons.Add(new DaePolygon(
+                    polygon,
+                    Remap(polygon.Mesh.Points, polygon.PointIndices, points, uniquePoints),
+                    Remap(polygon.Mesh.TexCoords, polygon.TexCoordIndices, texCoords, uniqueTexCoords),
+                    Remap(polygon.Colors, colors, uniqueColors)));
+            }
+
+            public IEnumerable<Polygon> Polygons
+            {
+                get
+                {
+                    return from p in polygons
+                           where p.Source != null
+                           select p.Source;
+                }
+            }
+
+            private static int[] Remap<T>(IList<T> values, int[] indices, List<T> list, Dictionary<T, int> unique)
+                where T : struct
+            {
+                var result = new int[indices.Length];
+
+                for (int i = 0; i < indices.Length; i++)
+                    result[i] = AddUnique(list, unique, values[indices[i]]);
+
+                return result;
+            }
+
+            private static int[] Remap<T>(IList<T> values, List<T> list, Dictionary<T, int> unique)
+                where T : struct
+            {
+                var result = new int[values.Count];
+
+                for (int i = 0; i < values.Count; i++)
+                    result[i] = AddUnique(list, unique, values[i]);
+
+                return result;
+            }
+
+            private static int AddUnique<T>(List<T> list, Dictionary<T, int> unique, T value)
+                where T : struct
+            {
+                int index;
+
+                if (!unique.TryGetValue(value, out index))
+                {
+                    index = list.Count;
+                    unique.Add(value, index);
+                    list.Add(value);
+                }
+
+                return index;
+            }
+
+            public void Build()
+            {
+                geometry = new Dae.Geometry();
+                geometry.Name = Name + "_geo";
+
+                var positionSource = new Dae.Source(points);
+                var texCoordSource = new Dae.Source(texCoords);
+                var colorSource = new Dae.Source(ColorArrayToFloatArray(colors), 4);
+
+                geometry.Vertices.Add(new Dae.Input(Dae.Semantic.Position, positionSource));
+
+                Dae.IndexedInput posInput = null;
+                Dae.IndexedInput texCoordInput = null;
+                Dae.IndexedInput colorInput = null;
+
+                var materialPrimitives = new Dictionary<Material, Dae.MeshPrimitives>();
+
+                polygons.Sort((x, y) => string.Compare(x.Material.Name, y.Material.Name));
+
+                foreach (var poly in polygons)
+                {
+                    Dae.MeshPrimitives primitives;
+
+                    if (!materialPrimitives.TryGetValue(poly.Material, out primitives))
+                    {
+                        primitives = new Dae.MeshPrimitives(Dae.MeshPrimitiveType.Polygons);
+                        materialPrimitives.Add(poly.Material, primitives);
+
+                        posInput = new Dae.IndexedInput(Dae.Semantic.Position, positionSource);
+                        primitives.Inputs.Add(posInput);
+
+                        texCoordInput = new Dae.IndexedInput(Dae.Semantic.TexCoord, texCoordSource);
+                        primitives.Inputs.Add(texCoordInput);
+
+                        if (poly.ColorIndices != null /*&& !poly.Material.Image.HasAlpha*/)
+                        {
+                            colorInput = new Dae.IndexedInput(Dae.Semantic.Color, colorSource);
+                            primitives.Inputs.Add(colorInput);
+                        }
+
+                        primitives.MaterialSymbol = poly.Material.Name;
+                        geometry.Primitives.Add(primitives);
+                    }
+
+                    primitives.VertexCounts.Add(poly.PointIndices.Length);
+
+                    posInput.Indices.AddRange(poly.PointIndices);
+                    texCoordInput.Indices.AddRange(poly.TexCoordIndices);
+
+                    if (colorInput != null)
+                    {
+                        //
+                        // If the first polygon had color indices then the rest better have them too...
+                        //
+
+                        colorInput.Indices.AddRange(poly.ColorIndices);
+                    }
+                }
+            }
+
+            public Dae.Geometry Geometry => geometry;
+
+            public void InstantiateMaterials(Dae.GeometryInstance inst, DaeSceneBuilder sceneBuilder)
+            {
+                var matInstances = new Dictionary<Material, Dae.MaterialInstance>();
+
+                foreach (var poly in polygons)
+                {
+                    if (matInstances.ContainsKey(poly.Material))
+                        continue;
+
+                    string matSymbol = poly.Material.Name;
+
+                    var matInstance = new Dae.MaterialInstance(
+                        matSymbol,
+                        sceneBuilder.GetMaterial(poly.Material));
+
+                    matInstances.Add(poly.Material, matInstance);
+
+                    var primitives = geometry.Primitives.FirstOrDefault(p => p.MaterialSymbol == matSymbol);
+
+                    if (primitives == null)
+                        continue;
+
+                    var texCoordInput = primitives.Inputs.Find(i => i.Semantic == Dae.Semantic.TexCoord);
+
+                    if (texCoordInput == null)
+                        continue;
+
+                    matInstance.Bindings.Add(new Dae.MaterialBinding("diffuse_TEXCOORD", texCoordInput));
+                    inst.Materials.Add(matInstance);
+                }
+            }
+
+            private static float[] ColorArrayToFloatArray(IList<Color> array)
+            {
+                var result = new float[array.Count * 4];
+
+                for (int i = 0; i < array.Count; i++)
+                {
+                    var color = array[i].ToVector3();
+
+                    result[i * 4 + 0] = color.X;
+                    result[i * 4 + 1] = color.Y;
+                    result[i * 4 + 2] = color.Z;
+                }
+
+                return result;
+            }
+        }
+
+        #endregion
+        #region private class DaeSceneBuilder
+
+        private class DaeSceneBuilder
+        {
+            private readonly Dae.Scene scene;
+            private readonly Dictionary<string, DaeMeshBuilder> nameMeshBuilder;
+            private readonly List<DaeMeshBuilder> meshBuilders;
+            private readonly Dictionary<Material, Dae.Material> materials;
+            private string imagesFolder = "images";
+
+            public DaeSceneBuilder()
+            {
+                scene = new Dae.Scene();
+                nameMeshBuilder = new Dictionary<string, DaeMeshBuilder>(StringComparer.Ordinal);
+                meshBuilders = new List<DaeMeshBuilder>();
+                materials = new Dictionary<Material, Dae.Material>();
+            }
+
+            public string ImagesFolder
+            {
+                get { return imagesFolder; }
+                set { imagesFolder = value; }
+            }
+
+            public DaeMeshBuilder GetMeshBuilder(string name)
+            {
+                DaeMeshBuilder result;
+
+                if (!nameMeshBuilder.TryGetValue(name, out result))
+                {
+                    result = new DaeMeshBuilder(name);
+                    nameMeshBuilder.Add(name, result);
+                    meshBuilders.Add(result);
+                }
+
+                return result;
+            }
+
+            public IEnumerable<DaeMeshBuilder> MeshBuilders => meshBuilders;
+
+            public Dae.Material GetMaterial(Material material)
+            {
+                Dae.Material result;
+
+                if (!materials.TryGetValue(material, out result))
+                {
+                    result = new Dae.Material();
+                    materials.Add(material, result);
+                }
+
+                return result;
+            }
+
+            public void Build()
+            {
+                BuildNodes();
+                BuildMaterials();
+            }
+
+            private void BuildNodes()
+            {
+                foreach (var meshBuilder in meshBuilders)
+                {
+                    meshBuilder.Build();
+
+                    var instance = new Dae.GeometryInstance(meshBuilder.Geometry);
+
+                    meshBuilder.InstantiateMaterials(instance, this);
+
+                    var node = new Dae.Node();
+                    node.Name = meshBuilder.Name;
+                    node.Instances.Add(instance);
+
+                    if (meshBuilder.Translation != Vector3.Zero)
+                        node.Transforms.Add(new Dae.TransformTranslate(meshBuilder.Translation));
+
+                    scene.Nodes.Add(node);
+                }
+            }
+
+            private void BuildMaterials()
+            {
+                foreach (var pair in materials)
+                {
+                    var material = pair.Key;
+                    var daeMaterial = pair.Value;
+
+                    string imageFileName = GetImageFileName(material);
+
+                    var image = new Dae.Image
+                    {
+                        FilePath = "./" + imageFileName.Replace('\\', '/'),
+                        Name = material.Name + "_img"
+                    };
+
+                    var effectSurface = new Dae.EffectSurface(image);
+
+                    var effectSampler = new Dae.EffectSampler(effectSurface)
+                    {
+                        //    WrapS = texture.WrapU ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None,
+                        //    WrapT = texture.WrapV ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None
+                    };
+
+                    var effectTexture = new Dae.EffectTexture(effectSampler, "diffuse_TEXCOORD");
+
+                    var effect = new Dae.Effect
+                    {
+                        Name = material.Name + "_fx",
+                        AmbientValue = Vector4.One,
+                        SpecularValue = Vector4.Zero,
+                        DiffuseValue = effectTexture,
+                        TransparentValue = material.Image.HasAlpha ? effectTexture : null,
+                        Parameters = {
+                            new Dae.EffectParameter("surface", effectSurface),
+                            new Dae.EffectParameter("sampler", effectSampler)
+                        }
+                    };
+
+                    daeMaterial.Name = material.Name;
+                    daeMaterial.Effect = effect;
+                }
+            }
+
+            private string GetImageFileName(Material material)
+            {
+                string fileName = material.Name + ".tga";
+
+                if (material.IsMarker)
+                    return Path.Combine("markers", fileName);
+
+                return Path.Combine(imagesFolder, fileName);
+            }
+
+            public void Write(string filePath)
+            {
+                string outputDirPath = Path.GetDirectoryName(filePath);
+
+                foreach (var material in materials.Keys)
+                    TgaWriter.Write(material.Image, Path.Combine(outputDirPath, GetImageFileName(material)));
+
+                Dae.Writer.WriteFile(filePath, scene);
+            }
+        }
+
+        #endregion
+
+        public static void WriteRooms(PolygonMesh mesh, string name, string outputDirPath)
+        {
+            var writer = new AkiraDaeWriter(mesh);
+            writer.WriteRooms();
+            writer.rooms.Write(Path.Combine(outputDirPath, name + "_bnv.dae"));
+        }
+
+        public static void WriteRooms(PolygonMesh mesh, string filePath)
+        {
+            var writer = new AkiraDaeWriter(mesh);
+            writer.WriteRooms();
+            writer.rooms.Write(filePath);
+        }
+
+        public static void Write(PolygonMesh mesh, string name, string outputDirPath, string fileType)
+        {
+            var writer = new AkiraDaeWriter(mesh);
+
+            writer.WriteGeometry();
+            writer.WriteRooms();
+
+            writer.world.Write(Path.Combine(outputDirPath, name + "_env." + fileType));
+            writer.worldMarkers.Write(Path.Combine(outputDirPath, name + "_env_markers." + fileType));
+            writer.rooms.Write(Path.Combine(outputDirPath, name + "_bnv." + fileType));
+
+            for (int i = 0; i < writer.objects.Length; i++)
+            {
+                var builder = writer.objects[i];
+
+                if (builder != null)
+                    builder.Write(Path.Combine(outputDirPath, string.Format("{0}_{1}." + fileType, name, objectTypeNames[i])));
+            }
+
+            foreach (var pair in writer.scripts)
+            {
+                var scriptId = pair.Key;
+                var builder = pair.Value;
+
+                builder.Write(Path.Combine(outputDirPath, string.Format("{0}_script_{1}." + fileType, name, scriptId)));
+            }
+        }
+
+        private AkiraDaeWriter(PolygonMesh source)
+        {
+            this.source = source;
+        }
+
+        private void WriteGeometry()
+        {
+            world = new DaeSceneBuilder();
+            worldMarkers = new DaeSceneBuilder();
+            objects = new DaeSceneBuilder[objectTypeNames.Length];
+            scripts = new Dictionary<int, DaeSceneBuilder>();
+
+            foreach (var polygon in source.Polygons)
+            {
+                if (polygon.Material == null)
+                    continue;
+
+                int objectType = polygon.ObjectType;
+                int scriptId = polygon.ScriptId;
+
+                if (scriptId != 0)
+                {
+                    string name = string.Format(CultureInfo.InvariantCulture, "script_{0}", scriptId);
+
+                    DaeSceneBuilder sceneBuilder;
+
+                    if (!scripts.TryGetValue(scriptId, out sceneBuilder))
+                    {
+                        sceneBuilder = new DaeSceneBuilder();
+                        scripts.Add(scriptId, sceneBuilder);
+                    }
+
+                    var meshBuilder = sceneBuilder.GetMeshBuilder(name);
+
+                    meshBuilder.AddPolygon(polygon);
+                }
+                else if (objectType == -1)
+                {
+                    //
+                    // If it doesn't have an object type then it's probably an environment polygon.
+                    //
+
+                    string name;
+
+                    if (source.HasDebugInfo)
+                        name = polygon.FileName;
+                    else
+                        name = "world";
+
+                    DaeMeshBuilder meshBuilder;
+
+                    if (polygon.Material.IsMarker)
+                        meshBuilder = worldMarkers.GetMeshBuilder(name);
+                    else
+                        meshBuilder = world.GetMeshBuilder(name);
+
+                    meshBuilder.AddPolygon(polygon);
+                }
+                else
+                {
+                    //
+                    // This polygon belongs to a object. Export it to one of the object files.
+                    //
+
+                    string name = string.Format(CultureInfo.InvariantCulture,
+                        "{0}_{1}", objectTypeNames[objectType], polygon.ObjectId);
+
+                    var sceneBuilder = objects[objectType];
+
+                    if (sceneBuilder == null)
+                    {
+                        sceneBuilder = new DaeSceneBuilder();
+                        objects[objectType] = sceneBuilder;
+                    }
+
+                    var meshBuilder = sceneBuilder.GetMeshBuilder(name);
+
+                    meshBuilder.AddPolygon(polygon);
+                }
+            }
+
+            foreach (var sceneBuilder in objects)
+            {
+                if (sceneBuilder == null)
+                    continue;
+
+                foreach (var meshBuilder in sceneBuilder.MeshBuilders)
+                {
+                    meshBuilder.ResetTransform();
+
+                    if (source.HasDebugInfo)
+                    {
+                        //
+                        // Polygons that belong to an object have useful object names in the debug info.
+                        // Try to use them. 
+                        //
+
+                        var names = new List<string>();
+                        int objectId = 0;
+
+                        foreach (var polygon in meshBuilder.Polygons)
+                        {
+                            objectId = polygon.ObjectId;
+                            names.Add(polygon.ObjectName);
+                        }
+
+                        string name = Utils.CommonPrefix(names);
+
+                        if (!string.IsNullOrEmpty(name) && name.Length > 3)
+                        {
+                            if (!name.EndsWith("_", StringComparison.Ordinal))
+                                name += "_";
+
+                            meshBuilder.Name = string.Format(CultureInfo.InvariantCulture,
+                                "{0}{1}", name, objectId);
+                        }
+                    }
+                }
+            }
+
+            foreach (var sceneBuilder in scripts.Values)
+            {
+                foreach (var meshBuilder in sceneBuilder.MeshBuilders)
+                {
+                    meshBuilder.ResetTransform();
+
+                    if (source.HasDebugInfo)
+                    {
+                        //
+                        // Polygons that belong to an object have useful object names in the debug info.
+                        // Try to use them. 
+                        //
+
+                        var names = new List<string>();
+                        int scriptId = 0;
+
+                        foreach (var polygon in meshBuilder.Polygons)
+                        {
+                            scriptId = polygon.ScriptId;
+                            names.Add(polygon.ObjectName);
+                        }
+
+                        string name = Utils.CommonPrefix(names);
+
+                        if (!string.IsNullOrEmpty(name) && name.Length > 3)
+                        {
+                            if (!name.EndsWith("_", StringComparison.Ordinal))
+                                name += "_";
+
+                            meshBuilder.Name = string.Format(CultureInfo.InvariantCulture,
+                                "{0}{1}", name, scriptId);
+                        }
+                    }
+                }
+            }
+
+            world.Build();
+            worldMarkers.Build();
+
+            foreach (var sceneBuilder in objects)
+            {
+                if (sceneBuilder == null)
+                    continue;
+
+                sceneBuilder.Build();
+            }
+
+            foreach (var sceneBuilder in scripts.Values)
+            {
+                sceneBuilder.Build();
+            }
+        }
+
+        private void WriteRooms()
+        {
+            rooms = new DaeSceneBuilder();
+            rooms.ImagesFolder = "grids";
+
+            for (int i = 0; i < source.Rooms.Count; i++)
+            {
+                var room = source.Rooms[i];
+
+                var meshBuilder = rooms.GetMeshBuilder(
+                    string.Format(CultureInfo.InvariantCulture, "room_{0}", i));
+
+                var material = source.Materials.GetMaterial(
+                    string.Format(CultureInfo.InvariantCulture, "bnv_grid_{0:d3}", i));
+
+                material.Image = room.Grid.ToImage();
+
+                foreach (var polygonPoints in room.GetFloorPolygons())
+                {
+                    var texCoords = new Vector2[polygonPoints.Length];
+
+                    for (int j = 0; j < polygonPoints.Length; j++)
+                    {
+                        var point = polygonPoints[j];
+                        var min = room.BoundingBox.Min;
+                        var max = room.BoundingBox.Max;
+
+                        min += new Vector3(room.Grid.TileSize * room.Grid.XOrigin, 0.0f, room.Grid.TileSize * room.Grid.ZOrigin);
+                        max -= new Vector3(room.Grid.TileSize * room.Grid.XOrigin, 0.0f, room.Grid.TileSize * room.Grid.ZOrigin);
+
+                        var size = max - min;
+
+                        float x = (point.X - min.X) / size.X;
+                        float z = (point.Z - min.Z) / size.Z;
+
+                        texCoords[j] = new Vector2(x, z);
+                    }
+
+                    meshBuilder.AddPolygon(material, polygonPoints, texCoords);
+                    meshBuilder.Build();
+                }
+            }
+
+            var ghostTexCoords = new[] {
+                new Vector2(0.0f, 0.0f),
+                new Vector2(1.0f, 0.0f),
+                new Vector2(1.0f, 1.0f),
+                new Vector2(0.0f, 1.0f)
+            };
+
+            for (int i = 0; i < source.Ghosts.Count; i++)
+            {
+                var ghost = source.Ghosts[i];
+
+                var meshBuilder = rooms.GetMeshBuilder(
+                    string.Format(CultureInfo.InvariantCulture, "ghost_{0}", i));
+
+                meshBuilder.AddPolygon(
+                    source.Materials.Markers.Ghost,
+                    ghost.Points.ToArray(),
+                    ghostTexCoords);
+
+                meshBuilder.Build();
+                meshBuilder.ResetTransform();
+            }
+
+            rooms.Build();
+        }
+    }
+}
Index: /OniSplit/Akira/AkiraDatReader.cs
===================================================================
--- /OniSplit/Akira/AkiraDatReader.cs	(revision 1114)
+++ /OniSplit/Akira/AkiraDatReader.cs	(revision 1114)
@@ -0,0 +1,515 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+using Oni.Motoko;
+
+namespace Oni.Akira
+{
+    internal class AkiraDatReader
+    {
+        #region Private data
+        private InstanceDescriptor akev;
+        private InstanceDescriptor agdb;
+        private InstanceDescriptor pnta;
+        private InstanceDescriptor plea;
+        private InstanceDescriptor txca;
+        private InstanceDescriptor agqg;
+        private InstanceDescriptor agqc;
+        private InstanceDescriptor agqr;
+        private InstanceDescriptor txma;
+        private InstanceDescriptor akva;
+        private InstanceDescriptor akba;
+        private InstanceDescriptor idxa1;
+        private InstanceDescriptor idxa2;
+        private InstanceDescriptor akbp;
+        private InstanceDescriptor akaa;
+
+        private PolygonMesh mesh;
+        private Plane[] planes;
+        private Polygon[] polygons;
+        #endregion
+
+        #region private class DatRoom
+
+        private class DatRoom
+        {
+            public readonly int BspRootIndex;
+            public readonly int SideListStart;
+            public readonly int SideListEnd;
+            public readonly int ChildIndex;
+            public readonly int SiblingIndex;
+            public readonly int XTiles;
+            public readonly int ZTiles;
+            public readonly BoundingBox BoundingBox;
+            public readonly float TileSize;
+            public readonly int XOrigin;
+            public readonly int ZOrigin;
+            public readonly RoomFlags Flags;
+            public readonly Plane Floor;
+            public readonly float Height;
+            public readonly byte[] CompressedGridData;
+
+            public DatRoom(InstanceDescriptor descriptor, BinaryReader reader)
+            {
+                BspRootIndex = reader.ReadInt32();
+                reader.Skip(4);
+                SideListStart = reader.ReadInt32();
+                SideListEnd = reader.ReadInt32();
+                ChildIndex = reader.ReadInt32();
+                SiblingIndex = reader.ReadInt32();
+                reader.Skip(4);
+                XTiles = reader.ReadInt32();
+                ZTiles = reader.ReadInt32();
+                int ofsGridData = reader.ReadInt32();
+                int lenGridData = reader.ReadInt32();
+                TileSize = reader.ReadSingle();
+                BoundingBox = reader.ReadBoundingBox();
+                XOrigin = reader.ReadInt16();
+                ZOrigin = reader.ReadInt16();
+                reader.Skip(16);
+                Flags = (RoomFlags)reader.ReadInt32();
+                Floor = reader.ReadPlane();
+                Height = reader.ReadSingle();
+
+                if (ofsGridData != 0 && lenGridData != 0)
+                {
+                    using (BinaryReader rawReader = descriptor.GetRawReader(ofsGridData))
+                        CompressedGridData = rawReader.ReadBytes(lenGridData);
+                }
+            }
+        }
+
+        #endregion
+        #region private class DatRoomBspNode
+
+        private class DatRoomBspNode
+        {
+            public readonly int PlaneIndex;
+            public readonly int FrontChildIndex;
+            public readonly int BackChildIndex;
+
+            public DatRoomBspNode(BinaryReader reader)
+            {
+                PlaneIndex = reader.ReadInt32();
+                BackChildIndex = reader.ReadInt32();
+                FrontChildIndex = reader.ReadInt32();
+            }
+        }
+
+        #endregion
+        #region private class DatRoomSide
+
+        private class DatRoomSide
+        {
+            public readonly int SideListStart;
+            public readonly int SideListEnd;
+
+            public DatRoomSide(BinaryReader reader)
+            {
+                reader.Skip(4);
+                SideListStart = reader.ReadInt32();
+                SideListEnd = reader.ReadInt32();
+                reader.Skip(16);
+            }
+        }
+
+        #endregion
+        #region private class DatRoomAdjacency
+
+        private class DatRoomAdjacency
+        {
+            public readonly int RoomIndex;
+            public readonly int QuadIndex;
+
+            public DatRoomAdjacency(BinaryReader reader)
+            {
+                RoomIndex = reader.ReadInt32();
+                QuadIndex = reader.ReadInt32();
+                reader.Skip(4);
+            }
+        }
+
+        #endregion
+
+        public static PolygonMesh Read(InstanceDescriptor akev)
+        {
+            var reader = new AkiraDatReader
+            {
+                akev = akev,
+                mesh = new PolygonMesh(new MaterialLibrary())
+            };
+
+            reader.Read();
+            return reader.mesh;
+        }
+
+        private void Read()
+        {
+            using (var reader = akev.OpenRead())
+            {
+                pnta = reader.ReadInstance();
+                plea = reader.ReadInstance();
+                txca = reader.ReadInstance();
+                agqg = reader.ReadInstance();
+                agqr = reader.ReadInstance();
+                agqc = reader.ReadInstance();
+                agdb = reader.ReadInstance();
+                txma = reader.ReadInstance();
+                akva = reader.ReadInstance();
+                akba = reader.ReadInstance();
+                idxa1 = reader.ReadInstance();
+                idxa2 = reader.ReadInstance();
+                akbp = reader.ReadInstance();
+                reader.Skip(8);
+                akaa = reader.ReadInstance();
+            }
+
+            ReadGeometry();
+            ReadDebugInfo();
+            ReadMaterials();
+            ReadScriptIndices();
+            ReadRooms();
+        }
+
+        private void ReadGeometry()
+        {
+            int[] planeIndices;
+
+            using (var reader = pnta.OpenRead(52))
+                mesh.Points.AddRange(reader.ReadVector3VarArray());
+
+            using (var reader = txca.OpenRead(20))
+                mesh.TexCoords.AddRange(reader.ReadVector2VarArray());
+
+            using (var reader = plea.OpenRead(20))
+                planes = reader.ReadPlaneVarArray();
+
+            using (var reader = agqc.OpenRead(20))
+            {
+                planeIndices = new int[reader.ReadInt32()];
+
+                for (int i = 0; i < planeIndices.Length; i++)
+                {
+                    planeIndices[i] = reader.ReadInt32();
+
+                    //
+                    // Ignore bounding boxes, we don't need them
+                    //
+
+                    reader.Skip(24);
+                }
+            }
+
+            using (var reader = agqg.OpenRead(20))
+            {
+                polygons = new Polygon[reader.ReadInt32()];
+
+                for (int i = 0; i < polygons.Length; i++)
+                {
+                    var pointIndices = reader.ReadInt32Array(4);
+                    var texCoordIndices = reader.ReadInt32Array(4);
+                    var colors = reader.ReadColorArray(4);
+                    var flags = (GunkFlags)reader.ReadInt32();
+                    int objectId = reader.ReadInt32();
+
+                    if ((flags & GunkFlags.Triangle) != 0)
+                    {
+                        Array.Resize(ref pointIndices, 3);
+                        Array.Resize(ref texCoordIndices, 3);
+                        Array.Resize(ref colors, 3);
+
+                        flags &= ~GunkFlags.Triangle;
+                    }
+
+                    var polygon = new Polygon(mesh, pointIndices, PlaneFromIndex(planeIndices[i]))
+                    {
+                        Flags = flags & ~GunkFlags.Transparent,
+                        TexCoordIndices = texCoordIndices,
+                        Colors = colors
+                    };
+
+                    if (objectId == -1)
+                    {
+                        polygon.ObjectType = -1;
+                        polygon.ObjectId = -1;
+                    }
+                    else
+                    {
+                        polygon.ObjectType = (objectId >> 24) & 0xff;
+                        polygon.ObjectId = objectId & 0xffffff;
+                    }
+
+                    polygons[i] = polygon;
+                }
+            }
+
+            foreach (var polygon in polygons)
+            {
+                if ((polygon.Flags & (GunkFlags.Ghost | GunkFlags.StairsUp | GunkFlags.StairsDown)) != 0)
+                    mesh.Ghosts.Add(polygon);
+                else
+                    mesh.Polygons.Add(polygon);
+            }
+        }
+
+        private Plane PlaneFromIndex(int index)
+        {
+            var plane = planes[index & int.MaxValue];
+
+            if (index < 0)
+            {
+                plane.Normal = -plane.Normal;
+                plane.D = -plane.D;
+            }
+
+            return plane;
+        }
+
+        private void ReadMaterials()
+        {
+            //
+            // Read material list from TXMA
+            //
+
+            Material[] materials;
+
+            using (var reader = txma.OpenRead(20))
+            {
+                materials = new Material[reader.ReadInt32()];
+
+                for (int i = 0; i < materials.Length; i++)
+                {
+                    var texture = reader.ReadInstance();
+
+                    if (texture == null)
+                        continue;
+
+                    var material = mesh.Materials.GetMaterial(Utils.CleanupTextureName(texture.Name));
+                    material.Image = TextureDatReader.Read(texture).Surfaces[0];
+
+                    if (material.Image.HasAlpha)
+                        material.Flags |= GunkFlags.Transparent;
+
+                    materials[i] = material;
+                }
+            }
+
+            //
+            // Assign materials to polygons based on AGQR
+            //
+
+            using (var reader = agqr.OpenRead(20))
+            {
+                int count = reader.ReadInt32();
+
+                for (int i = 0; i < count; i++)
+                    polygons[i].Material = materials[reader.ReadInt32() & 0xffff];
+            }
+
+            //
+            // Assign special materials: danger, stairs etc.
+            //
+
+            foreach (var polygon in polygons)
+            {
+                var marker = mesh.Materials.Markers.GetMarker(polygon);
+
+                if (marker != null)
+                    polygon.Material = marker;
+            }
+        }
+
+        private void ReadScriptIndices()
+        {
+            if (idxa1 == null || idxa2 == null)
+                return;
+
+            int[] scriptQuadIndices;
+            int[] scriptIds;
+
+            using (var reader = idxa1.OpenRead(20))
+                scriptQuadIndices = reader.ReadInt32VarArray();
+
+            using (var reader = idxa2.OpenRead(20))
+                scriptIds = reader.ReadInt32VarArray();
+
+            for (int i = 0; i < scriptQuadIndices.Length; i++)
+                polygons[scriptQuadIndices[i]].ScriptId = scriptIds[i];
+        }
+
+        private void ReadDebugInfo()
+        {
+            if (agdb == null)
+            {
+                var debugFileName = "AGDB" + akev.Name + ".oni";
+                var debugFilePath = Path.Combine(Path.GetDirectoryName(akev.File.FilePath), debugFileName);
+
+                if (!File.Exists(debugFilePath))
+                    return;
+
+                Console.WriteLine(debugFilePath);
+
+                var debugFile = akev.File.FileManager.OpenFile(debugFilePath);
+
+                if (debugFile == null)
+                    return;
+
+                agdb = debugFile.Descriptors[0];
+            }
+
+            if (agdb == null || agdb.Template.Tag != TemplateTag.AGDB)
+                return;
+
+            using (var reader = agdb.OpenRead(20))
+            {
+                int count = reader.ReadInt32();
+
+                var fileNames = new Dictionary<int, string>();
+                var objectNames = new Dictionary<int, string>();
+
+                for (int i = 0; i < count; i++)
+                {
+                    int objectNameOffset = reader.ReadInt32();
+                    string objectName;
+
+                    if (!objectNames.TryGetValue(objectNameOffset, out objectName))
+                    {
+                        using (var rawReader = agdb.GetRawReader(objectNameOffset))
+                            objectName = rawReader.ReadString(256);
+
+                        objectName = objectName.Replace('.', '_');
+                        objectNames.Add(objectNameOffset, objectName);
+                    }
+
+                    int fileNameOffset = reader.ReadInt32();
+                    string fileName;
+
+                    if (!fileNames.TryGetValue(fileNameOffset, out fileName))
+                    {
+                        using (var rawReader = agdb.GetRawReader(fileNameOffset))
+                            fileName = rawReader.ReadString(256);
+
+                        fileName = Path.GetFileNameWithoutExtension(fileName);
+                        fileNames.Add(fileNameOffset, fileName);
+                    }
+
+                    if (!string.IsNullOrEmpty(objectName))
+                        mesh.HasDebugInfo = true;
+
+                    polygons[i].ObjectName = objectName;
+                    polygons[i].FileName = fileName;
+                }
+            }
+        }
+
+        private void ReadRooms()
+        {
+            DatRoomBspNode[] bspTrees;
+            DatRoomSide[] roomSides;
+            DatRoomAdjacency[] roomAdjacencies;
+            DatRoom[] roomsData;
+
+            using (var reader = akbp.OpenRead(22))
+            {
+                bspTrees = new DatRoomBspNode[reader.ReadUInt16()];
+
+                for (int i = 0; i < bspTrees.Length; i++)
+                    bspTrees[i] = new DatRoomBspNode(reader);
+            }
+
+            using (var reader = akba.OpenRead(20))
+            {
+                roomSides = new DatRoomSide[reader.ReadInt32()];
+
+                for (int i = 0; i < roomSides.Length; i++)
+                    roomSides[i] = new DatRoomSide(reader);
+            }
+
+            using (var reader = akaa.OpenRead(20))
+            {
+                roomAdjacencies = new DatRoomAdjacency[reader.ReadInt32()];
+
+                for (int i = 0; i < roomAdjacencies.Length; i++)
+                    roomAdjacencies[i] = new DatRoomAdjacency(reader);
+            }
+
+            using (var reader = akva.OpenRead(20))
+            {
+                roomsData = new DatRoom[reader.ReadInt32()];
+
+                for (int i = 0; i < roomsData.Length; i++)
+                    roomsData[i] = new DatRoom(akva, reader);
+            }
+
+            var rooms = new Room[roomsData.Length];
+
+            for (int i = 0; i < roomsData.Length; i++)
+            {
+                var data = roomsData[i];
+
+                var room = new Room
+                {
+                    BspTree = BspNodeDataToBspNode(bspTrees, data.BspRootIndex),
+                    BoundingBox = data.BoundingBox
+                };
+
+                if ((data.Flags & RoomFlags.Stairs) != 0)
+                {
+                    room.FloorPlane = data.Floor;
+                    room.Height = data.Height;
+                }
+                else
+                {
+                    room.FloorPlane = new Plane(Vector3.Up, -data.BoundingBox.Min.Y);
+                    room.Height = data.BoundingBox.Max.Y - data.BoundingBox.Min.Y;
+                }
+
+                room.Grid = RoomGrid.FromCompressedData(data.XTiles, data.ZTiles, data.CompressedGridData);
+                rooms[i] = room;
+            }
+
+            for (int i = 0; i < roomsData.Length; i++)
+            {
+                var data = roomsData[i];
+                var room = rooms[i];
+
+                //if (data.SiblingIndex != -1)
+                //    room.Sibling = rooms[data.SiblingIndex];
+
+                //if (data.ChildIndex != -1)
+                //    room.Child = rooms[data.ChildIndex];
+
+                for (int j = data.SideListStart; j < data.SideListEnd; j++)
+                {
+                    var sideData = roomSides[j];
+
+                    for (int k = sideData.SideListStart; k < sideData.SideListEnd; k++)
+                    {
+                        var adjData = roomAdjacencies[k];
+                        var adjacentRoom = rooms[adjData.RoomIndex];
+                        var ghost = polygons[adjData.QuadIndex];
+
+                        room.Ajacencies.Add(new RoomAdjacency(adjacentRoom, ghost));
+                    }
+                }
+            }
+
+            mesh.Rooms.AddRange(rooms);
+        }
+
+        private RoomBspNode BspNodeDataToBspNode(DatRoomBspNode[] data, int index)
+        {
+            var nodeData = data[index];
+            RoomBspNode front = null, back = null;
+
+            if (nodeData.BackChildIndex != -1)
+                back = BspNodeDataToBspNode(data, nodeData.BackChildIndex);
+
+            if (nodeData.FrontChildIndex != -1)
+                front = BspNodeDataToBspNode(data, nodeData.FrontChildIndex);
+
+            return new RoomBspNode(PlaneFromIndex(nodeData.PlaneIndex), back, front);
+        }
+    }
+}
Index: /OniSplit/Akira/AkiraDatWriter.cs
===================================================================
--- /OniSplit/Akira/AkiraDatWriter.cs	(revision 1114)
+++ /OniSplit/Akira/AkiraDatWriter.cs	(revision 1114)
@@ -0,0 +1,1070 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class AkiraDatWriter
+    {
+        #region Private data
+        private Importer importer;
+        private string name;
+        private bool debug;
+        private PolygonMesh source;
+
+        private List<DatPolygon> polygons = new List<DatPolygon>();
+        private Dictionary<Polygon, DatPolygon> polygonMap = new Dictionary<Polygon, DatPolygon>();
+
+        private UniqueList<Vector3> points = new UniqueList<Vector3>();
+        private UniqueList<Vector2> texCoords = new UniqueList<Vector2>();
+        private UniqueList<Material> materials = new UniqueList<Material>();
+        private UniqueList<Plane> planes = new UniqueList<Plane>();
+
+        private List<DatAlphaBspNode> alphaBspNodes = new List<DatAlphaBspNode>();
+
+        private List<DatRoom> rooms = new List<DatRoom>();
+        private Dictionary<Room, DatRoom> roomMap = new Dictionary<Room, DatRoom>();
+        private List<DatRoomBspNode> roomBspNodes = new List<DatRoomBspNode>();
+        private List<DatRoomSide> roomSides = new List<DatRoomSide>();
+        private List<DatRoomAdjacency> roomAdjacencies = new List<DatRoomAdjacency>();
+
+        private DatOctree octree;
+        #endregion
+
+        #region private class UniqueList<T>
+
+        private class UniqueList<T> : ICollection<T>
+        {
+            private readonly List<T> list = new List<T>();
+            private readonly Dictionary<T, int> indices = new Dictionary<T, int>();
+
+            public int Add(T t)
+            {
+                int index;
+
+                if (!indices.TryGetValue(t, out index))
+                {
+                    index = list.Count;
+                    indices.Add(t, index);
+                    list.Add(t);
+                }
+
+                return index;
+            }
+
+            #region ICollection<T> Members
+
+            void ICollection<T>.Add(T item) => Add(item);
+
+            public int Count => list.Count;
+
+            void ICollection<T>.Clear()
+            {
+                list.Clear();
+                indices.Clear();
+            }
+
+            bool ICollection<T>.Contains(T item) => indices.ContainsKey(item);
+
+            void ICollection<T>.CopyTo(T[] array, int arrayIndex)
+            {
+                list.CopyTo(array, arrayIndex);
+            }
+
+            bool ICollection<T>.IsReadOnly => false;
+
+            bool ICollection<T>.Remove(T item)
+            {
+                throw new NotImplementedException();
+            }
+
+            IEnumerator<T> IEnumerable<T>.GetEnumerator() => list.GetEnumerator();
+
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => list.GetEnumerator();
+            
+            #endregion
+        }
+
+        #endregion
+
+        #region private class DatObject<T>
+
+        private class DatObject<T>
+        {
+            private readonly T source;
+
+            public DatObject(T source)
+            {
+                this.source = source;
+            }
+
+            public T Source => source;
+        }
+
+        #endregion
+        #region private class DatPolygon
+
+        private class DatPolygon : DatObject<Polygon>
+        {
+            private static readonly Color defaultColor = new Color(207, 207, 207, 255);
+            private static readonly Color[] defaultColors = new[] { defaultColor, defaultColor, defaultColor, defaultColor };
+            private GunkFlags flags;
+            private readonly int index;
+            public readonly int[] PointIndices = new int[4];
+            public readonly int[] TexCoordIndices = new int[4];
+            public readonly Color[] Colors = new Color[4];
+            private readonly int objectId;
+            private readonly int scriptId;
+            private int materialIndex;
+            private int planeIndex;
+
+            public DatPolygon(Polygon source, int index, UniqueList<Vector3> points, UniqueList<Vector2> texCoords)
+                : base(source)
+            {
+                this.index = index;
+                this.scriptId = source.ScriptId;
+
+                objectId = source.ObjectType << 24 | (source.ObjectId & 0xffffff);
+                flags = source.Flags;
+
+                if (source.VertexCount == 3)
+                {
+                    flags |= GunkFlags.Triangle;
+
+                    Array.Copy(source.PointIndices, PointIndices, 3);
+                    PointIndices[3] = PointIndices[2];
+
+                    Array.Copy(source.TexCoordIndices, TexCoordIndices, 3);
+                    TexCoordIndices[3] = TexCoordIndices[2];
+
+                    if (source.Colors == null)
+                    {
+                        Colors = defaultColors;
+                    }
+                    else
+                    {
+                        Array.Copy(source.Colors, Colors, 3);
+                        Colors[3] = Colors[2];
+                    }
+                }
+                else
+                {
+                    Array.Copy(source.PointIndices, PointIndices, 4);
+                    Array.Copy(source.TexCoordIndices, TexCoordIndices, 4);
+
+                    if (source.Colors != null)
+                        Colors = source.Colors;
+                    else
+                        Colors = defaultColors;
+                }
+
+                for (int i = 0; i < 4; i++)
+                {
+                    PointIndices[i] = points.Add(source.Mesh.Points[PointIndices[i]]);
+                    TexCoordIndices[i] = texCoords.Add(source.Mesh.TexCoords[TexCoordIndices[i]]);
+                }
+            }
+
+            public int Index => index;
+            public int ObjectId => objectId;
+            public int ScriptId => scriptId;
+
+            public GunkFlags Flags
+            {
+                get { return flags; }
+                set { flags = value; }
+            }
+
+            public int MaterialIndex
+            {
+                get { return materialIndex; }
+                set { materialIndex = value; }
+            }
+
+            public int PlaneIndex
+            {
+                get { return planeIndex; }
+                set { planeIndex = value; }
+            }
+        }
+
+        #endregion
+        #region private class DatBspNode
+
+        private class DatBspNode<T> : DatObject<T>
+            where T : BspNode<T>
+        {
+            public int PlaneIndex;
+            public int FrontChildIndex = -1;
+            public int BackChildIndex = -1;
+
+            public DatBspNode(T source)
+                : base(source)
+            {
+            }
+        }
+
+        #endregion
+        #region private class DatAlphaBspNode
+
+        private class DatAlphaBspNode : DatBspNode<AlphaBspNode>
+        {
+            public readonly int PolygonIndex;
+
+            public DatAlphaBspNode(AlphaBspNode source, int polygonIndex)
+                : base(source)
+            {
+                PolygonIndex = polygonIndex;
+            }
+        }
+
+        #endregion
+        #region private class DatRoom
+
+        private class DatRoom : DatObject<Room>
+        {
+            private readonly int index;
+            private readonly RoomFlags flags;
+
+            public int BspTreeIndex;
+            public int SideListStart;
+            public int SideListEnd;
+            public int ChildIndex = -1;
+            public int SiblingIndex = -1;
+            public byte[] CompressedGridData;
+            public byte[] DebugData;
+
+            public DatRoom(Room source, int index)
+                : base(source)
+            {
+                this.index = index;
+                this.flags = RoomFlags.Room;
+
+                if (source.FloorPlane.Normal.Y < 0.999f)
+                    this.flags |= RoomFlags.Stairs;
+            }
+
+            public int Index => index;
+            public int Flags => (int)flags;
+        }
+
+        #endregion
+        #region private class DatRoomBspNode
+
+        private class DatRoomBspNode : DatBspNode<RoomBspNode>
+        {
+            public DatRoomBspNode(RoomBspNode source)
+                : base(source)
+            {
+            }
+        }
+
+        #endregion
+        #region private class DatRoomSide
+
+        private class DatRoomSide
+        {
+            public int AdjacencyListStart;
+            public int AdjacencyListEnd;
+        }
+
+        #endregion
+        #region private class DatRoomAdjacency
+
+        private class DatRoomAdjacency : DatObject<RoomAdjacency>
+        {
+            public DatRoomAdjacency(RoomAdjacency source)
+                : base(source)
+            {
+            }
+
+            public int AdjacentRoomIndex;
+            public int GhostIndex;
+        }
+
+        #endregion
+        #region private class DatOctree
+
+        private class DatOctree
+        {
+            public int[] Nodes;
+            public int[] QuadTrees;
+            public int[] Adjacency;
+            public DatOctreeBoundingBox[] BoundingBoxes;
+            public DatOctreePolygonRange[] PolygonLists;
+            public DatOctreeBnvRange[] BnvLists;
+            public int[] PolygonIndex;
+            public int[] BnvIndex;
+        }
+
+        #endregion
+        #region private struct DatOctreeBoundingBox
+
+        private struct DatOctreeBoundingBox
+        {
+            private readonly uint value;
+
+            public DatOctreeBoundingBox(BoundingBox bbox)
+            {
+                int size = (int)(Math.Log(bbox.Max.X - bbox.Min.X, 2)) - 4;
+                int maxX = (int)(bbox.Max.X + 4080.0f);
+                int maxY = (int)(bbox.Max.Y + 4080.0f);
+                int maxZ = (int)(bbox.Max.Z + 4080.0f);
+
+                value = (uint)((maxX << 14) | (maxY << 5) | (maxZ >> 4) | (size << 27));
+            }
+
+            public uint PackedValue => value;
+        }
+
+        #endregion
+        #region private struct DatOctreeBnvRange
+
+        private struct DatOctreeBnvRange
+        {
+            private const int indexBitOffset = 8;
+            private const int lengthBitMask = 255;
+            private readonly uint value;
+
+            public DatOctreeBnvRange(int start, int length)
+            {
+                ValidateRange(start, length);
+                value = (uint)((start << indexBitOffset) | (length & lengthBitMask));
+            }
+
+            private static void ValidateRange(int start, int length)
+            {
+                if (start > 16777215)
+                    throw new ArgumentException(string.Format("Invalid bnv list start index {0}", start), "start");
+
+                if (length > 255)
+                    throw new ArgumentException(string.Format("Invalid bnv list length {0}", length), "length");
+            }
+
+            public uint PackedValue => value;
+        }
+
+        #endregion
+        #region private struct DatOctreePolygonRange
+
+        private struct DatOctreePolygonRange
+        {
+            private const int indexBitOffset = 12;
+            private const int lengthBitMask = 4095;
+            private readonly uint value;
+
+            public DatOctreePolygonRange(int start, int length)
+            {
+                //ValidateRange(start, length);
+
+                if (start > 1048575)
+                {
+                    start = 1048575;
+                    length = 0;
+                }
+
+                value = (uint)((start << indexBitOffset) | (length & lengthBitMask));
+            }
+
+            private static void ValidateRange(int start, int length)
+            {
+                if (start > 1048575)
+                    throw new ArgumentException(string.Format("Invalid quad list start index {0}", start), "start");
+
+                if (length > 4095)
+                    throw new ArgumentException(string.Format("Invalid quad list length {0}", length), "length");
+            }
+
+            public uint PackedValue => value;
+        }
+
+        #endregion
+
+        public static void Write(PolygonMesh mesh, Importer importer, string name, bool debug)
+        {
+            var writer = new AkiraDatWriter {
+                name = name,
+                importer = importer,
+                source = mesh,
+                debug = debug
+            };
+
+            writer.Write();
+        }
+
+        private void Write()
+        {
+            Console.Error.WriteLine("Environment bounding box is {0}", source.GetBoundingBox());
+
+            RoomBuilder.BuildRooms(source);
+
+            ConvertPolygons(source.Polygons);
+            ConvertPolygons(source.Doors);
+            ConvertPolygons(source.Ghosts);
+            ConvertAlphaBspTree(AlphaBspBuilder.Build(source, debug));
+            ConvertRooms();
+            ConvertOctree();
+
+            WriteAKEV();
+
+            //foreach (Material material in materials)
+            //{
+            //    if (File.Exists(material.ImageFilePath))
+            //        importer.AddDependency(material.ImageFilePath, TemplateTag.TXMP);
+            //}
+        }
+
+        private void ConvertPolygons(List<Polygon> sourcePolygons)
+        {
+            foreach (var polygon in sourcePolygons)
+            {
+                if (polygon.VertexCount > 4)
+                {
+                    Console.Error.WriteLine("Geometry '{0}' has a {1}-gon, ignoring.", polygon.ObjectName, polygon.VertexCount);
+                    continue;
+                }
+
+                if (polygon.TexCoordIndices == null)
+                {
+                    Console.Error.WriteLine("Geometry '{0}' does not contain texture coordinates, ignoring.", polygon.ObjectName);
+                    continue;
+                }
+
+                var datPolygon = new DatPolygon(polygon, polygons.Count, points, texCoords) {
+                    PlaneIndex = planes.Add(polygon.Plane),
+                    MaterialIndex = materials.Add(polygon.Material)
+                };
+
+                polygons.Add(datPolygon);
+                polygonMap.Add(polygon, datPolygon);
+            }
+
+            //var noneMaterial = source.Materials.GetMaterial("NONE");
+            //int noneMaterialIndex = materials.Add(noneMaterial);
+
+            //foreach (var polygon in polygons)
+            //{
+            //    var material = polygon.Source.Material;
+
+            //if (material.IsMarker)
+            //{
+            //    polygon.MaterialIndex = noneMaterialIndex;
+
+            //    if (material != source.Materials.Markers.Blackness && (material.Flags & GunkFlags.Invisible) == 0)
+            //        polygon.Flags |= GunkFlags.Transparent;
+            //}
+            //else
+            //{
+            //    polygon.MaterialIndex = materials.Add(material);
+            //}
+            //}
+        }
+
+        private int ConvertAlphaBspTree(AlphaBspNode source)
+        {
+            if (source == null)
+                return -1;
+
+            int index = alphaBspNodes.Count;
+
+            var node = new DatAlphaBspNode(source, polygonMap[source.Polygon].Index) {
+                PlaneIndex = planes.Add(source.Plane)
+            };
+
+            alphaBspNodes.Add(node);
+
+            if (source.FrontChild != null)
+                node.FrontChildIndex = ConvertAlphaBspTree((AlphaBspNode)source.FrontChild);
+
+            if (source.BackChild != null)
+                node.BackChildIndex = ConvertAlphaBspTree((AlphaBspNode)source.BackChild);
+
+            return index;
+        }
+
+        private void ConvertRooms()
+        {
+            foreach (var room in source.Rooms)
+            {
+                var datRoom = new DatRoom(room, rooms.Count) {
+                    BspTreeIndex = ConvertRoomBspTree(room.BspTree),
+                    CompressedGridData = room.Grid.Compress(),
+                    DebugData = room.Grid.DebugData,
+                    SideListStart = roomSides.Count
+                };
+
+                if (room.Ajacencies.Count > 0)
+                {
+                    var datSide = new DatRoomSide {
+                        AdjacencyListStart = roomAdjacencies.Count
+                    };
+
+                    foreach (var adjacency in room.Ajacencies)
+                    {
+                        roomAdjacencies.Add(new DatRoomAdjacency(adjacency) {
+                            AdjacentRoomIndex = source.Rooms.IndexOf(adjacency.AdjacentRoom),
+                            GhostIndex = polygonMap[adjacency.Ghost].Index
+                        });
+                    }
+
+                    datSide.AdjacencyListEnd = roomAdjacencies.Count;
+                    roomSides.Add(datSide);
+                }
+
+                datRoom.SideListEnd = roomSides.Count;
+
+                rooms.Add(datRoom);
+                roomMap.Add(room, datRoom);
+            }
+        }
+
+        private int ConvertRoomBspTree(RoomBspNode node)
+        {
+            int index = roomBspNodes.Count;
+
+            var datNode = new DatRoomBspNode(node) {
+                PlaneIndex = planes.Add(node.Plane)
+            };
+
+            roomBspNodes.Add(datNode);
+
+            if (node.FrontChild != null)
+                datNode.FrontChildIndex = ConvertRoomBspTree(node.FrontChild);
+
+            if (node.BackChild != null)
+                datNode.BackChildIndex = ConvertRoomBspTree(node.BackChild);
+
+            return index;
+        }
+
+        private void ConvertOctree()
+        {
+            Console.Error.WriteLine("Building octtree for {0} polygons...", source.Polygons.Count);
+
+            var root = OctreeBuilder.Build(source, debug);
+
+            var nodeList = new List<OctreeNode>();
+            var leafList = new List<OctreeNode>();
+            int quadListLength = 0;
+            int roomListLength = 0;
+
+            //
+            // Assign indices to nodes/leafs and compute the length of the quad and room indexes.
+            //
+
+            root.DfsTraversal(node => {
+                if (node.IsLeaf)
+                {
+                    node.Index = leafList.Count;
+                    leafList.Add(node);
+                    quadListLength += node.Polygons.Count;
+                    roomListLength += node.Rooms.Count;
+                }
+                else
+                {
+                    node.Index = nodeList.Count;
+                    nodeList.Add(node);
+                }
+            });
+
+            //
+            // Create the octtree data structure that will be written to the file.
+            //
+
+            octree = new DatOctree {
+                Nodes = new int[nodeList.Count * OctreeNode.ChildCount],
+                Adjacency = new int[leafList.Count * OctreeNode.FaceCount],
+                BoundingBoxes = new DatOctreeBoundingBox[leafList.Count],
+                PolygonIndex = new int[quadListLength],
+                PolygonLists = new DatOctreePolygonRange[leafList.Count],
+                BnvLists = new DatOctreeBnvRange[leafList.Count],
+                BnvIndex = new int[roomListLength]
+            };
+
+            Console.WriteLine("Octtree: {0} interior nodes, {1} leafs", nodeList.Count, leafList.Count);
+
+            //
+            // Populate the node array.
+            // The octree builder stores child nodes in a different order than Oni (by mistake)
+            //
+
+            var interiorNodeIndexRemap = new int[] { 0, 4, 2, 6, 1, 5, 3, 7 };
+            var datOcttree = octree;
+
+            foreach (var node in nodeList)
+            {
+                int i = node.Index * OctreeNode.ChildCount;
+
+                for (int j = 0; j < OctreeNode.ChildCount; j++)
+                {
+                    var child = node.Children[j];
+                    int k = interiorNodeIndexRemap[j];
+
+                    if (child.IsLeaf)
+                        datOcttree.Nodes[i + k] = child.Index | int.MinValue;
+                    else
+                        datOcttree.Nodes[i + k] = child.Index;
+                }
+            }
+
+            //
+            // Generate the data needed by the leafs: bounding box, quad range and room range.
+            //
+
+            int quadListIndex = 0;
+            int bnvListIndex = 0;
+
+            foreach (var leaf in leafList)
+            {
+                datOcttree.BoundingBoxes[leaf.Index] = new DatOctreeBoundingBox(leaf.BoundingBox);
+
+                if (leaf.Polygons.Count > 0)
+                {
+                    datOcttree.PolygonLists[leaf.Index] = new DatOctreePolygonRange(quadListIndex, leaf.Polygons.Count);
+
+                    foreach (var polygon in leaf.Polygons)
+                        datOcttree.PolygonIndex[quadListIndex++] = polygonMap[polygon].Index;
+                }
+
+                if (leaf.Rooms.Count > 0)
+                {
+                    datOcttree.BnvLists[leaf.Index] = new DatOctreeBnvRange(bnvListIndex, leaf.Rooms.Count);
+
+                    foreach (var room in leaf.Rooms)
+                        datOcttree.BnvIndex[bnvListIndex++] = roomMap[room].Index;
+                }
+            }
+
+            //
+            // Generate the quad trees. Note that the octtree builder doesn't build quad trees because
+            // they're only needed when writing the octtree to the file. Currently OniSplit doesn't use
+            // the octtree for raycasting.
+            //
+
+            var quadTrees = new List<int>();
+
+            foreach (var leaf in leafList)
+            {
+                leaf.RefineAdjacency();
+
+                foreach (var face in OctreeNode.Face.All)
+                {
+                    var adjacentLeaf = leaf.Adjacency[face.Index];
+                    int index = leaf.Index * OctreeNode.FaceCount + face.Index;
+
+                    if (adjacentLeaf == null)
+                    {
+                        //
+                        // There's no adjacent node or leaf, this should only happen
+                        // on the edges of the octtree.
+                        //
+
+                        datOcttree.Adjacency[index] = -1;
+                    }
+                    else if (adjacentLeaf.IsLeaf)
+                    {
+                        //
+                        // The adjacent node is a leaf, there's no need for a quad tree.
+                        //
+
+                        datOcttree.Adjacency[index] = adjacentLeaf.Index | int.MinValue;
+                    }
+                    else
+                    {
+                        //
+                        // The adjacent node has children, a quad tree needs to be built.
+                        //
+
+                        int quadTreeBaseIndex = quadTrees.Count / 4;
+                        datOcttree.Adjacency[index] = quadTreeBaseIndex;
+
+                        var quadTreeRoot = leaf.BuildFaceQuadTree(face);
+
+                        foreach (var node in quadTreeRoot.GetDfsList())
+                        {
+                            for (int i = 0; i < 4; i++)
+                            {
+                                if (node.Nodes[i] != null)
+                                    quadTrees.Add(quadTreeBaseIndex + node.Nodes[i].Index);
+                                else
+                                    quadTrees.Add(node.Leafs[i].Index | int.MinValue);
+                            }
+                        }
+                    }
+                }
+            }
+
+            datOcttree.QuadTrees = quadTrees.ToArray();
+        }
+
+        private void WriteAKEV()
+        {
+            var akev = importer.CreateInstance(TemplateTag.AKEV, name);
+            var pnta = importer.CreateInstance(TemplateTag.PNTA);
+            var plea = importer.CreateInstance(TemplateTag.PLEA);
+            var txca = importer.CreateInstance(TemplateTag.TXCA);
+            var agqg = importer.CreateInstance(TemplateTag.AGQG);
+            var agqr = importer.CreateInstance(TemplateTag.AGQR);
+            var agqc = importer.CreateInstance(TemplateTag.AGQC);
+            var agdb = importer.CreateInstance(TemplateTag.AGDB);
+            var txma = importer.CreateInstance(TemplateTag.TXMA);
+            var akva = importer.CreateInstance(TemplateTag.AKVA);
+            var akba = importer.CreateInstance(TemplateTag.AKBA);
+            var idxa1 = importer.CreateInstance(TemplateTag.IDXA);
+            var idxa2 = importer.CreateInstance(TemplateTag.IDXA);
+            var akbp = importer.CreateInstance(TemplateTag.AKBP);
+            var abna = importer.CreateInstance(TemplateTag.ABNA);
+            var akot = importer.CreateInstance(TemplateTag.AKOT);
+            var akaa = importer.CreateInstance(TemplateTag.AKAA);
+            var akda = importer.CreateInstance(TemplateTag.AKDA);
+
+            using (var writer = akev.OpenWrite())
+            {
+                writer.Write(pnta);
+                writer.Write(plea);
+                writer.Write(txca);
+                writer.Write(agqg);
+                writer.Write(agqr);
+                writer.Write(agqc);
+                writer.Write(agdb);
+                writer.Write(txma);
+                writer.Write(akva);
+                writer.Write(akba);
+                writer.Write(idxa1);
+                writer.Write(idxa2);
+                writer.Write(akbp);
+                writer.Write(abna);
+                writer.Write(akot);
+                writer.Write(akaa);
+                writer.Write(akda);
+                writer.Write(source.GetBoundingBox());
+                writer.Skip(24);
+                writer.Write(12.0f);
+            }
+
+            pnta.WritePoints(points);
+            plea.WritePlanes(planes);
+            txca.WriteTexCoords(texCoords);
+            WriteAGQG(agqg);
+            WriteAGQR(agqr);
+            WriteAGQC(agqc);
+            WriteTXMA(txma);
+            WriteAKVA(akva);
+            WriteAKBA(akba);
+            WriteAKBP(akbp);
+            WriteABNA(abna);
+            WriteAKOT(akot);
+            WriteAKAA(akaa);
+            WriteAKDA(akda);
+            WriteScriptIds(idxa1, idxa2);
+            WriteAGDB(agdb);
+        }
+
+        private void WriteAGQG(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(polygons.Count);
+
+                foreach (var polygon in polygons)
+                {
+                    writer.Write(polygon.PointIndices);
+                    writer.Write(polygon.TexCoordIndices);
+                    writer.Write(polygon.Colors);
+                    writer.Write((uint)polygon.Flags);
+                    writer.Write(polygon.ObjectId);
+                }
+            }
+        }
+
+        private void WriteAGQC(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(polygons.Count);
+
+                foreach (var polygon in polygons)
+                {
+                    writer.Write(polygon.PlaneIndex);
+                    writer.Write(polygon.Source.BoundingBox);
+                }
+            }
+        }
+
+        private void WriteAGQR(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(polygons.Count);
+
+                foreach (var polygon in polygons)
+                {
+                    writer.Write(polygon.MaterialIndex);
+                }
+            }
+        }
+
+        private void WriteTXMA(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(materials.Count);
+
+                foreach (var material in materials)
+                {
+                    writer.Write(importer.CreateInstance(TemplateTag.TXMP, material.Name));
+                }
+            }
+        }
+
+        private void WriteABNA(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(alphaBspNodes.Count);
+
+                foreach (var node in alphaBspNodes)
+                {
+                    writer.Write(node.PolygonIndex);
+                    writer.Write(node.PlaneIndex);
+                    writer.Write(node.FrontChildIndex);
+                    writer.Write(node.BackChildIndex);
+                }
+            }
+        }
+
+        private void WriteAKVA(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(rooms.Count);
+
+                foreach (var room in rooms)
+                {
+                    writer.Write(room.BspTreeIndex);
+                    writer.Write(room.Index);
+                    writer.Write(room.SideListStart);
+                    writer.Write(room.SideListEnd);
+                    writer.Write(room.ChildIndex);
+                    writer.Write(room.SiblingIndex);
+                    writer.Skip(4);
+                    writer.Write(room.Source.Grid.XTiles);
+                    writer.Write(room.Source.Grid.ZTiles);
+                    writer.Write(importer.WriteRawPart(room.CompressedGridData));
+                    writer.Write(room.CompressedGridData.Length);
+                    writer.Write(room.Source.Grid.TileSize);
+                    writer.Write(room.Source.BoundingBox);
+                    writer.WriteInt16(room.Source.Grid.XOrigin);
+                    writer.WriteInt16(room.Source.Grid.ZOrigin);
+                    writer.Write(room.Index);
+                    writer.Skip(4);
+
+                    if (room.DebugData != null)
+                    {
+                        writer.Write(room.DebugData.Length);
+                        writer.Write(importer.WriteRawPart(room.DebugData));
+                    }
+                    else
+                    {
+                        writer.Write(0);
+                        writer.Write(0);
+                    }
+
+                    writer.Write(room.Flags);
+                    writer.Write(room.Source.FloorPlane);
+                    writer.Write(room.Source.Height);
+                }
+            }
+        }
+
+        private void WriteAKBA(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(roomSides.Count);
+
+                foreach (var side in roomSides)
+                {
+                    writer.Write(0);
+                    writer.Write(side.AdjacencyListStart);
+                    writer.Write(side.AdjacencyListEnd);
+                    writer.Skip(16);
+                }
+            }
+        }
+
+        private void WriteAKBP(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(22))
+            {
+                writer.WriteUInt16(roomBspNodes.Count);
+
+                foreach (var node in roomBspNodes)
+                {
+                    writer.Write(node.PlaneIndex);
+                    writer.Write(node.BackChildIndex);
+                    writer.Write(node.FrontChildIndex);
+                }
+            }
+        }
+
+        private void WriteAKAA(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(roomAdjacencies.Count);
+
+                foreach (var adjacency in roomAdjacencies)
+                {
+                    writer.Write(adjacency.AdjacentRoomIndex);
+                    writer.Write(adjacency.GhostIndex);
+                    writer.Write(0);
+                }
+            }
+        }
+
+        private void WriteAKDA(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(0);
+            }
+        }
+
+        private void WriteAKOT(ImporterDescriptor akot)
+        {
+            var otit = importer.CreateInstance(TemplateTag.OTIT);
+            var otlf = importer.CreateInstance(TemplateTag.OTLF);
+            var qtna = importer.CreateInstance(TemplateTag.QTNA);
+            var idxa1 = importer.CreateInstance(TemplateTag.IDXA);
+            var idxa2 = importer.CreateInstance(TemplateTag.IDXA);
+
+            using (var writer = akot.OpenWrite())
+            {
+                writer.Write(otit);
+                writer.Write(otlf);
+                writer.Write(qtna);
+                writer.Write(idxa1);
+                writer.Write(idxa2);
+            }
+
+            WriteOTIT(otit);
+            WriteOTLF(otlf);
+            WriteQTNA(qtna);
+
+            idxa1.WriteIndices(octree.PolygonIndex);
+            idxa2.WriteIndices(octree.BnvIndex);
+        }
+
+        private void WriteOTIT(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(octree.Nodes.Length / OctreeNode.ChildCount);
+                writer.Write(octree.Nodes);
+            }
+        }
+
+        private void WriteOTLF(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(octree.BoundingBoxes.Length);
+
+                for (int i = 0; i < octree.BoundingBoxes.Length; i++)
+                {
+                    writer.Write(octree.PolygonLists[i].PackedValue);
+                    writer.Write(octree.Adjacency, i * OctreeNode.FaceCount, OctreeNode.FaceCount);
+                    writer.Write(octree.BoundingBoxes[i].PackedValue);
+                    writer.Write(octree.BnvLists[i].PackedValue);
+                }
+            }
+        }
+
+        private void WriteQTNA(ImporterDescriptor descriptor)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(octree.QuadTrees.Length / 4);
+                writer.Write(octree.QuadTrees);
+            }
+        }
+
+        private void WriteScriptIds(ImporterDescriptor idxa1, ImporterDescriptor idxa2)
+        {
+            var scriptIdMap = new List<KeyValuePair<int, int>>(256);
+
+            foreach (var polygon in polygons)
+            {
+                if (polygon.ScriptId != 0)
+                    scriptIdMap.Add(new KeyValuePair<int, int>(polygon.ScriptId, polygon.Index));
+            }
+
+            scriptIdMap.Sort((x, y) => x.Key.CompareTo(y.Key));
+
+            var scriptIds = new int[scriptIdMap.Count];
+            var polygonIndices = new int[scriptIdMap.Count];
+
+            for (int i = 0; i < scriptIdMap.Count; i++)
+            {
+                scriptIds[i] = scriptIdMap[i].Key;
+                polygonIndices[i] = scriptIdMap[i].Value;
+            }
+
+            idxa1.WriteIndices(polygonIndices);
+            idxa2.WriteIndices(scriptIds);
+        }
+
+        private void WriteAGDB(ImporterDescriptor descriptor)
+        {
+            if (!debug)
+            {
+                using (var writer = descriptor.OpenWrite(20))
+                {
+                    writer.Write(0);
+                }
+
+                return;
+            }
+
+            var objectNames = new Dictionary<string, int>(polygons.Count, StringComparer.Ordinal);
+            var fileNames = new Dictionary<string, int>(polygons.Count, StringComparer.Ordinal);
+
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(polygons.Count);
+
+                foreach (var polygon in polygons)
+                {
+                    var objectName = polygon.Source.ObjectName;
+                    var fileName = polygon.Source.FileName;
+
+                    if (string.IsNullOrEmpty(objectName))
+                        objectName = "(none)";
+
+                    if (string.IsNullOrEmpty(fileName))
+                        fileName = "(none)";
+
+                    int objectOffset;
+                    int fileOffset;
+
+                    if (!objectNames.TryGetValue(objectName, out objectOffset))
+                    {
+                        objectOffset = importer.WriteRawPart(objectName);
+                        objectNames.Add(objectName, objectOffset);
+                    }
+
+                    if (!fileNames.TryGetValue(fileName, out fileOffset))
+                    {
+                        fileOffset = importer.WriteRawPart(fileName);
+                        fileNames.Add(fileName, fileOffset);
+                    }
+
+                    writer.Write(objectOffset);
+                    writer.Write(fileOffset);
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Akira/AkiraImporter.cs
===================================================================
--- /OniSplit/Akira/AkiraImporter.cs	(revision 1114)
+++ /OniSplit/Akira/AkiraImporter.cs	(revision 1114)
@@ -0,0 +1,64 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class AkiraImporter : Importer
+    {
+        private bool debug;
+        //private string defaultTexture = "NONE";
+        //private bool importLights;
+        //private bool noLight;
+        //private bool noTextures;
+
+        public AkiraImporter(string[] args)
+        {
+            foreach (string arg in args)
+            {
+                if (arg == "-debug")
+                {
+                    debug = true;
+                }
+                //if (arg.StartsWith("-texdefault:", StringComparison.Ordinal))
+                //{
+                //    int i = arg.IndexOf(':');
+                //    defaultTexture = arg.Substring(i + 1);
+                //}
+                //else if (arg.StartsWith("-lights", StringComparison.Ordinal))
+                //{
+                //    importLights = true;
+                //}
+                //else if (arg == "-env-notxmp")
+                //{
+                //    noTextures = true;
+                //}
+                //else if (arg.StartsWith("-nolight", StringComparison.Ordinal))
+                //{
+                //    noLight = true;
+                //}
+            }
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            Import(new[] { filePath }, outputDirPath);
+        }
+
+        public void Import(IList<string> files, string outputDirPath)
+        {
+            Import(files, outputDirPath, Path.GetFileNameWithoutExtension(files[0]));
+        }
+
+        public void Import(IList<string> files, string outputDirPath, string name)
+        {
+            PolygonMesh mesh = AkiraDaeReader.Read(files);
+
+            BeginImport();
+            AkiraDatWriter.Write(mesh, this, name, debug);
+            Write(outputDirPath);
+        }
+    }
+}
Index: /OniSplit/Akira/AlphaBspBuilder.cs
===================================================================
--- /OniSplit/Akira/AlphaBspBuilder.cs	(revision 1114)
+++ /OniSplit/Akira/AlphaBspBuilder.cs	(revision 1114)
@@ -0,0 +1,86 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal class AlphaBspBuilder
+    {
+        private PolygonMesh mesh;
+        private bool debug;
+
+        public static AlphaBspNode Build(PolygonMesh mesh, bool debug)
+        {
+            var builder = new AlphaBspBuilder
+            {
+                mesh = mesh,
+                debug = debug
+            };
+
+            return builder.Build();
+        }
+
+        private AlphaBspNode Build()
+        {
+            var transparent = new List<Polygon>(1024);
+
+            transparent.AddRange(mesh.Polygons.Where(p => p.IsTransparent));
+
+            if (debug)
+                transparent.AddRange(mesh.Ghosts.Where(p => p.IsTransparent));
+
+            Console.Error.WriteLine("Building bsp tree for {0} transparent polygons...", transparent.Count);
+            return Build(transparent);
+        }
+
+        private AlphaBspNode Build(List<Polygon> polygons)
+        {
+            if (polygons.Count == 0)
+                return null;
+
+            var separator = polygons[0].Plane;
+            AlphaBspNode frontNode = null, backNode = null;
+
+            if (polygons.Count > 1)
+            {
+                var front = new List<Polygon>(polygons.Count);
+                var back = new List<Polygon>(polygons.Count);
+
+                for (int i = 1; i < polygons.Count; i++)
+                {
+                    var polygon = polygons[i];
+                    var plane = polygon.Plane;
+
+                    bool isFront = false;
+                    bool isBack = false;
+
+                    if (Math.Abs(plane.D - separator.D) < 0.001f
+                        && Vector3.Distance(plane.Normal, separator.Normal) < 0.001f)
+                    {
+                        isFront = true;
+                    }
+                    else
+                    {
+                        foreach (var point in polygon.Points)
+                        {
+                            if (separator.DotCoordinate(point) > 0.0f)
+                                isFront = true;
+                            else
+                                isBack = true;
+                        }
+                    }
+
+                    if (isFront)
+                        front.Add(polygon);
+
+                    if (isBack)
+                        back.Add(polygon);
+                }
+
+                frontNode = Build(front);
+                backNode = Build(back);
+            }
+
+            return new AlphaBspNode(polygons[0], frontNode, backNode);
+        }
+    }
+}
Index: /OniSplit/Akira/AlphaBspNode.cs
===================================================================
--- /OniSplit/Akira/AlphaBspNode.cs	(revision 1114)
+++ /OniSplit/Akira/AlphaBspNode.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Akira
+{
+    internal class AlphaBspNode : BspNode<AlphaBspNode>
+    {
+        public readonly Polygon Polygon;
+
+        public AlphaBspNode(Polygon polygon, AlphaBspNode frontChild, AlphaBspNode backChild)
+            : base(polygon.Plane, frontChild, backChild)
+        {
+            Polygon = polygon;
+        }
+    }
+}
Index: /OniSplit/Akira/BspNode.cs
===================================================================
--- /OniSplit/Akira/BspNode.cs	(revision 1114)
+++ /OniSplit/Akira/BspNode.cs	(revision 1114)
@@ -0,0 +1,17 @@
+namespace Oni.Akira
+{
+    internal abstract class BspNode<T> 
+        where T : BspNode<T>
+    {
+        public readonly Plane Plane;
+        public readonly T BackChild;
+        public readonly T FrontChild;
+
+        protected BspNode(Plane plane, T backChild, T frontChild)
+        {
+            Plane = plane;
+            BackChild = backChild;
+            FrontChild = frontChild;
+        }
+    }
+}
Index: /OniSplit/Akira/GunkFlags.cs
===================================================================
--- /OniSplit/Akira/GunkFlags.cs	(revision 1114)
+++ /OniSplit/Akira/GunkFlags.cs	(revision 1114)
@@ -0,0 +1,46 @@
+using System;
+
+namespace Oni.Akira
+{
+    [Flags]
+    internal enum GunkFlags : uint
+    {
+        None = 0,
+        DoorFrame = 0x01,
+
+        Ghost = 0x02,
+        StairsUp = 0x04,
+        StairsDown = 0x08,
+
+        Stairs = 0x10,
+        Transparent = 0x80,
+        TwoSided = 0x0200,
+        NoCollision = 0x0800,
+        Invisible = 0x00002000,
+        NoObjectCollision = 0x4000,
+        NoCharacterCollision = 0x8000,
+        NoOcclusion = 0x010000,
+        Danger = 0x020000,
+        GridIgnore = 0x400000,
+        NoDecals = 0x800000,
+        Furniture = 0x01000000,
+
+        SoundTransparent = 0x08000000,
+        Impassable = 0x10000000,
+
+        Triangle = 0x00000040,
+        Horizontal = 0x00080000,
+        Vertical = 0x00100000,
+
+        ProjectionBit0 = 0x02000000,
+        ProjectionBit1 = 0x04000000,
+    }
+
+    internal enum PolygonProjectionPlane
+    {
+        None,
+        XY,
+        XZ,
+        YZ
+    }
+}
Index: /OniSplit/Akira/Material.cs
===================================================================
--- /OniSplit/Akira/Material.cs	(revision 1114)
+++ /OniSplit/Akira/Material.cs	(revision 1114)
@@ -0,0 +1,51 @@
+﻿using System;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class Material
+    {
+        private bool isMarker;
+        private string name;
+        private GunkFlags flags;
+        private string imageFilePath;
+        private Surface image;
+
+        public Material(string name)
+        {
+            this.name = name;
+        }
+
+        public Material(string name, bool isMarker)
+        {
+            this.name = name;
+            this.isMarker = isMarker;
+        }
+
+        public string Name
+        {
+            get { return name; }
+            set { name = value; }
+        }
+
+        public bool IsMarker => isMarker;
+
+        public GunkFlags Flags
+        {
+            get { return flags; }
+            set { flags = value; }
+        }
+
+        public string ImageFilePath
+        {
+            get { return imageFilePath; }
+            set { imageFilePath = value; }
+        }
+
+        public Surface Image
+        {
+            get { return image; }
+            set { image = value; }
+        }
+    }
+}
Index: /OniSplit/Akira/MaterialLibrary.cs
===================================================================
--- /OniSplit/Akira/MaterialLibrary.cs	(revision 1114)
+++ /OniSplit/Akira/MaterialLibrary.cs	(revision 1114)
@@ -0,0 +1,379 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class MaterialLibrary
+    {
+        private readonly MarkerMaterials markers = new MarkerMaterials();
+        private readonly Dictionary<string, Material> materials = new Dictionary<string, Material>(StringComparer.OrdinalIgnoreCase);
+        private Material notFound;
+
+        #region internal class MarkerMaterials
+
+        internal class MarkerMaterials
+        {
+            private Material ghost;
+            private Material stairs;
+            private Material door;
+            private Material danger;
+            private Material barrier;
+            private Material impassable;
+            private Material blackness;
+            private Material floor;
+
+            public Material GetMarker(string name)
+            {
+                EnsureMaterials();
+
+                switch (name)
+                {
+                    case "_marker_door":
+                        return door;
+                    case "_marker_ghost":
+                        return ghost;
+                    case "_marker_stairs":
+                        return stairs;
+                    case "_marker_danger":
+                        return danger;
+                    case "_marker_barrier":
+                        return barrier;
+                    case "_marker_impassable":
+                        return impassable;
+                    case "_marker_blackness":
+                        return blackness;
+                    case "_marker_floor":
+                        return floor;
+                    default:
+                        return null;
+                }
+            }
+
+            public Material GetMarker(Polygon polygon)
+            {
+                EnsureMaterials();
+
+                var flags = polygon.Flags & ~(GunkFlags.ProjectionBit0 | GunkFlags.ProjectionBit1 | GunkFlags.Horizontal | GunkFlags.Vertical);
+                var adjacencyFlags = GunkFlags.Ghost | GunkFlags.StairsUp | GunkFlags.StairsDown;
+
+                if ((flags & (GunkFlags.DoorFrame | adjacencyFlags)) == GunkFlags.DoorFrame)
+                    return door;
+
+                if ((flags & adjacencyFlags) != 0)
+                    return ghost;
+
+                if ((flags & GunkFlags.Invisible) != 0)
+                {
+                    flags &= ~GunkFlags.Invisible;
+
+                    if ((flags & GunkFlags.Danger) != 0)
+                        return danger;
+
+                    if ((flags & GunkFlags.Stairs) != 0)
+                        return stairs;
+
+                    if ((flags & (GunkFlags.NoObjectCollision | GunkFlags.NoCharacterCollision)) == GunkFlags.NoObjectCollision)
+                        return barrier;
+
+                    if ((flags & (GunkFlags.NoObjectCollision | GunkFlags.NoCharacterCollision | GunkFlags.NoCollision)) == 0)
+                        return impassable;
+
+                    //
+                    // What's the point of a invisible and no collision polygon?!
+                    //
+
+                    if ((flags & GunkFlags.NoCollision) != 0)
+                        return null;
+
+                    Console.Error.WriteLine("Unknown invisible material, fix tool: {0}", flags);
+                }
+                else
+                {
+                    if ((flags & (GunkFlags.TwoSided | GunkFlags.NoCollision)) == (GunkFlags.TwoSided | GunkFlags.NoCollision)
+                        && polygon.Material != null
+                        && polygon.Material.Name == "BLACKNESS")
+                    {
+                        return blackness;
+                    }
+                }
+
+                return null;
+            }
+
+            private void EnsureMaterials()
+            {
+                if (ghost != null)
+                    return;
+
+                CreateGhost();
+                CreateBarrier();
+                CreateDanger();
+                CreateDoor();
+                CreateStairs();
+                CreateImpassable();
+                CreateBlackness();
+                CreateFloor();
+            }
+
+            private void CreateBarrier()
+            {
+                var surface = new Surface(128, 128);
+
+                var fill = new Color(0, 240, 20, 180);
+                var sign = new Color(240, 20, 0, 255);
+
+                surface.Fill(0, 0, 128, 128, fill);
+
+                surface.Fill(0, 0, 128, 1, sign);
+                surface.Fill(0, 127, 128, 1, sign);
+                surface.Fill(0, 1, 1, 126, sign);
+                surface.Fill(127, 1, 1, 126, sign);
+                surface.Fill(64, 1, 1, 126, sign);
+                surface.Fill(1, 64, 126, 1, sign);
+
+                barrier = new Material("_marker_barrier", true)
+                {
+                    Flags = GunkFlags.NoObjectCollision | GunkFlags.Invisible,
+                    Image = surface
+                };
+            }
+
+            private void CreateImpassable()
+            {
+                var surface = new Surface(128, 128);
+
+                var fill = new Color(240, 0, 20, 180);
+                var sign = new Color(240, 20, 0, 255);
+
+                surface.Fill(0, 0, 128, 128, fill);
+
+                surface.Fill(0, 0, 128, 1, sign);
+                surface.Fill(0, 127, 128, 1, sign);
+                surface.Fill(0, 1, 1, 126, sign);
+                surface.Fill(127, 1, 1, 126, sign);
+                surface.Fill(64, 1, 1, 126, sign);
+                surface.Fill(1, 64, 126, 1, sign);
+
+                impassable = new Material("_marker_impassable", true)
+                {
+                    Flags = GunkFlags.Invisible,
+                    Image = surface
+                };
+            }
+
+            private void CreateGhost()
+            {
+                var surface = new Surface(128, 128);
+
+                var border = new Color(16, 48, 240, 240);
+                var fill = new Color(208, 240, 240, 80);
+
+                surface.Fill(0, 0, 128, 128, fill);
+
+                surface.Fill(0, 0, 128, 1, border);
+                surface.Fill(0, 127, 128, 1, border);
+                surface.Fill(0, 1, 1, 126, border);
+                surface.Fill(127, 1, 1, 126, border);
+                surface.Fill(64, 1, 1, 126, border);
+                surface.Fill(1, 64, 126, 1, border);
+
+                ghost = new Material("_marker_ghost", true);
+                ghost.Flags = GunkFlags.Ghost | GunkFlags.TwoSided | GunkFlags.Transparent | GunkFlags.NoCollision;
+                ghost.Image = surface;
+            }
+
+            private void CreateDoor()
+            {
+                var surface = new Surface(128, 128);
+
+                var fill = new Color(240, 240, 0, 208);
+                var line = new Color(0, 0, 240);
+
+                surface.Fill(0, 0, 128, 128, fill);
+
+                surface.Fill(1, 1, 126, 1, line);
+                surface.Fill(1, 1, 1, 126, line);
+                surface.Fill(1, 126, 126, 1, line);
+                surface.Fill(126, 1, 1, 126, line);
+
+                door = new Material("_marker_door", true)
+                {
+                    Flags = GunkFlags.DoorFrame | GunkFlags.TwoSided | GunkFlags.Transparent | GunkFlags.NoCollision,
+                    Image = surface
+                };
+            }
+
+            private void CreateDanger()
+            {
+                var surface = new Surface(128, 128);
+
+                var fill = new Color(255, 10, 0, 208);
+                var sign = new Color(255, 255, 255, 255);
+
+                surface.Fill(0, 0, 128, 128, fill);
+                surface.Fill(52, 16, 24, 64, sign);
+                surface.Fill(52, 96, 24, 16, sign);
+
+                danger = new Material("_marker_danger", true)
+                {
+                    Flags = GunkFlags.Danger | GunkFlags.Invisible | GunkFlags.NoCollision | GunkFlags.NoOcclusion,
+                    Image = surface
+                };
+            }
+
+            private void CreateStairs()
+            {
+                var surface = new Surface(128, 128);
+
+                var fill = new Color(40, 240, 0, 180);
+                var step = new Color(40, 0, 240, 180);
+
+                surface.Fill(0, 0, 128, 128, fill);
+
+                for (int y = 0; y < surface.Height; y += 32)
+                    surface.Fill(0, y, surface.Width, 16, step);
+
+                stairs = new Material("_marker_stairs", true)
+                {
+                    Flags = GunkFlags.Stairs | GunkFlags.Invisible | GunkFlags.NoObjectCollision | GunkFlags.TwoSided,
+                    Image = surface
+                };
+            }
+
+            private void CreateBlackness()
+            {
+                var surface = new Surface(16, 16, SurfaceFormat.BGRX);
+
+                surface.Fill(0, 0, 16, 16, Color.Black);
+
+                blackness = new Material("_marker_blackness", true)
+                {
+                    Flags = GunkFlags.TwoSided | GunkFlags.NoCollision,
+                    Image = surface
+                };
+            }
+
+            private void CreateFloor()
+            {
+                var surface = new Surface(256, 256);
+
+                surface.Fill(0, 0, 16, 16, Color.White);
+
+                for (int x = 0; x < 256; x += 4)
+                {
+                    surface.Fill(x, 0, 1, 256, Color.Black);
+                    surface.Fill(0, x, 256, 1, Color.Black);
+                }
+
+                floor = new Material("_marker_floor", true)
+                {
+                    Flags = GunkFlags.NoCollision,
+                    Image = surface
+                };
+            }
+
+            public Material Barrier
+            {
+                get
+                {
+                    EnsureMaterials();
+                    return barrier;
+                }
+            }
+
+            public Material Ghost
+            {
+                get
+                {
+                    EnsureMaterials();
+                    return ghost;
+                }
+            }
+
+            public Material Danger
+            {
+                get
+                {
+                    EnsureMaterials();
+                    return danger;
+                }
+            }
+
+            public Material DoorFrame
+            {
+                get
+                {
+                    EnsureMaterials();
+                    return door;
+                }
+            }
+
+            public Material Stairs
+            {
+                get
+                {
+                    EnsureMaterials();
+                    return stairs;
+                }
+            }
+
+            public Material Floor
+            {
+                get
+                {
+                    EnsureMaterials();
+                    return floor;
+                }
+            }
+
+            public Material Blackness
+            {
+                get
+                {
+                    EnsureMaterials();
+                    return blackness;
+                }
+            }
+        }
+
+        #endregion
+
+        public MarkerMaterials Markers => markers;
+
+        public Material NotFound
+        {
+            get
+            {
+                if (notFound == null)
+                    notFound = GetMaterial("notfoundtex");
+
+                return notFound;
+            }
+        }
+
+        public IEnumerable<Material> All => materials.Values;
+
+        public Material GetMaterial(string name)
+        {
+            Material material;
+
+            if (!materials.TryGetValue(name, out material))
+            {
+                material = markers.GetMarker(name);
+
+                if (material == null)
+                    material = new Material(name);
+
+                materials.Add(name, material);
+            }
+
+            if (name.StartsWith("lmap_", StringComparison.OrdinalIgnoreCase))
+            {
+                material.Flags |= GunkFlags.NoCollision | GunkFlags.NoOcclusion | GunkFlags.SoundTransparent;
+            }
+
+            return material;
+        }
+    }
+}
Index: /OniSplit/Akira/OctreeBuilder.cs
===================================================================
--- /OniSplit/Akira/OctreeBuilder.cs	(revision 1114)
+++ /OniSplit/Akira/OctreeBuilder.cs	(revision 1114)
@@ -0,0 +1,43 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal static class OctreeBuilder
+    {
+        private static readonly BoundingBox rootBoundingBox = new BoundingBox(new Vector3(-4096.0f), new Vector3(4096.0f));
+
+        public static OctreeNode Build(PolygonMesh mesh, bool debug)
+        {
+            IEnumerable<Polygon> polygons = mesh.Polygons;
+
+            if (debug)
+                polygons = polygons.Concatenate(mesh.Ghosts);
+
+            var root = new OctreeNode(rootBoundingBox, polygons, mesh.Rooms);
+            root.Build();
+            return root;
+        }
+
+        public static OctreeNode Build(PolygonMesh mesh, GunkFlags excludeFlags)
+        {
+            var root = new OctreeNode(rootBoundingBox, mesh.Polygons.Where(p => (p.Flags & excludeFlags) == 0), mesh.Rooms);
+            root.Build();
+            return root;
+        }
+
+        public static OctreeNode Build(PolygonMesh mesh, Func<Polygon, bool> polygonFilter)
+        {
+            var root = new OctreeNode(rootBoundingBox, mesh.Polygons.Where(polygonFilter), mesh.Rooms);
+            root.Build();
+            return root;
+        }
+
+        public static OctreeNode BuildRoomsOctree(PolygonMesh mesh)
+        {
+            var root = new OctreeNode(rootBoundingBox, new Polygon[0], mesh.Rooms);
+            root.Build();
+            return root;
+        }
+    }
+}
Index: /OniSplit/Akira/OctreeNode.cs
===================================================================
--- /OniSplit/Akira/OctreeNode.cs	(revision 1114)
+++ /OniSplit/Akira/OctreeNode.cs	(revision 1114)
@@ -0,0 +1,492 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal class OctreeNode
+    {
+        public const int FaceCount = 6;
+        public const int ChildCount = 8;
+
+        private const float MinNodeSize = 16.0f;
+        private const int MaxQuadsPerLeaf = 4096;
+        private const int MaxRoomsPerLeaf = 255;
+
+        #region Private data
+        private int index;
+        private BoundingBox bbox;
+        private Polygon[] polygons;
+        private OctreeNode[] children;
+        private OctreeNode[] adjacency = new OctreeNode[FaceCount];
+        private Room[] rooms;
+        #endregion
+
+        #region public enum Axis
+
+        public enum Axis
+        {
+            Z,
+            Y,
+            X
+        }
+
+        #endregion
+        #region public enum Direction
+
+        public enum Direction
+        {
+            Negative,
+            Positive
+        }
+
+        #endregion
+        #region public struct Face
+
+        public struct Face
+        {
+            private readonly int index;
+
+            public Face(int index)
+            {
+                this.index = index;
+            }
+
+            public int Index => index;
+            public Axis Axis => (Axis)(2 - ((index & 6) >> 1));
+            public Direction Direction => (Direction)(index & 1);
+
+            public static IEnumerable<Face> All
+            {
+                get
+                {
+                    for (int i = 0; i < FaceCount; i++)
+                        yield return new Face(i);
+                }
+            }
+        }
+
+        #endregion
+        #region public struct ChildPosition
+
+        public struct ChildPosition
+        {
+            private int index;
+
+            public ChildPosition(int index)
+            {
+                this.index = index;
+            }
+
+            public int Index => index;
+
+            public int X => this[Axis.X];
+            public int Y => this[Axis.Y];
+            public int Z => this[Axis.Z];
+
+            public int this[Axis axis]
+            {
+                get
+                {
+                    return ((index >> (int)axis) & 1);
+                }
+                set
+                {
+                    int mask = (1 << (int)axis);
+
+                    if (value == 0)
+                        index &= ~mask;
+                    else
+                        index |= mask;
+                }
+            }
+
+            public static IEnumerable<ChildPosition> All
+            {
+                get
+                {
+                    for (int i = 0; i < 8; i++)
+                        yield return new ChildPosition(i);
+                }
+            }
+        }
+
+        #endregion
+
+        public OctreeNode(BoundingBox bbox, IEnumerable<Polygon> polygons, IEnumerable<Room> rooms)
+        {
+            this.bbox = bbox;
+            this.polygons = polygons.ToArray();
+            this.rooms = rooms.ToArray();
+        }
+
+        private OctreeNode(BoundingBox bbox, Polygon[] polygons, Room[] rooms)
+        {
+            this.bbox = bbox;
+            this.polygons = polygons;
+            this.rooms = rooms;
+        }
+
+        public int Index
+        {
+            get { return index; }
+            set { index = value; }
+        }
+
+        public BoundingBox BoundingBox => bbox;
+        public OctreeNode[] Children => children;
+        public OctreeNode[] Adjacency => adjacency;
+        public bool IsLeaf => polygons != null;
+        public ICollection<Polygon> Polygons => polygons;
+        public ICollection<Room> Rooms => rooms;
+        private Vector3 Center => (bbox.Min + bbox.Max) * 0.5f;
+        private float Size => bbox.Max.X - bbox.Min.X;
+
+        public void Build()
+        {
+            BuildRecursive();
+
+            //
+            // Force a split of the root node because the root cannot be a leaf.
+            //
+
+            if (children == null)
+                Split();
+        }
+
+        private void BuildRecursive()
+        {
+            if ((polygons == null || polygons.Length <= 19) && (rooms == null || rooms.Length < 16))
+            {
+                return;
+            }
+
+            if (Size <= MinNodeSize)
+            {
+                if (polygons.Length > MaxQuadsPerLeaf)
+                    throw new NotSupportedException(string.Format("Octtree: Quad density too big: current {0} max 4096 bbox {1}", polygons.Length, BoundingBox));
+
+                if (rooms.Length > MaxRoomsPerLeaf)
+                    throw new NotSupportedException(string.Format("Octtree: Room density too big: current {0} max 255 bbox {1}", rooms.Length, BoundingBox));
+
+                return;
+            }
+
+            Split();
+        }
+
+        private void Split()
+        {
+            children = SplitCore();
+            polygons = null;
+            rooms = null;
+
+            BuildSimpleAdjaceny();
+
+            foreach (var child in children)
+                child.BuildRecursive();
+        }
+
+        private OctreeNode[] SplitCore()
+        {
+            var children = new OctreeNode[ChildCount];
+            var center = Center;
+            var childPolygons = new List<Polygon>(polygons.Length);
+            var childRooms = new List<Room>(rooms.Length);
+
+            foreach (var position in ChildPosition.All)
+            {
+                var childNodeBBox = new BoundingBox(center, center);
+
+                if (position.X == 0)
+                    childNodeBBox.Min.X = bbox.Min.X;
+                else
+                    childNodeBBox.Max.X = bbox.Max.X;
+
+                if (position.Y == 0)
+                    childNodeBBox.Min.Y = bbox.Min.Y;
+                else
+                    childNodeBBox.Max.Y = bbox.Max.Y;
+
+                if (position.Z == 0)
+                    childNodeBBox.Min.Z = bbox.Min.Z;
+                else
+                    childNodeBBox.Max.Z = bbox.Max.Z;
+
+                childPolygons.Clear();
+                childRooms.Clear();
+
+                var boxIntersector = new PolygonBoxIntersector(ref childNodeBBox);
+
+                foreach (var polygon in polygons)
+                {
+                    if (boxIntersector.Intersects(polygon))
+                        childPolygons.Add(polygon);
+                }
+
+                foreach (var room in rooms)
+                {
+                    if (room.Intersect(childNodeBBox))
+                        childRooms.Add(room);
+                }
+
+                children[position.Index] = new OctreeNode(childNodeBBox, childPolygons.ToArray(), childRooms.ToArray());
+            }
+
+            return children;
+        }
+
+        private void BuildSimpleAdjaceny()
+        {
+            foreach (ChildPosition position in ChildPosition.All)
+            {
+                var child = children[position.Index];
+
+                foreach (var face in Face.All)
+                    child.Adjacency[face.Index] = GetChildAdjacency(position, face);
+            }
+        }
+
+        private OctreeNode GetChildAdjacency(ChildPosition position, Face face)
+        {
+            if (face.Direction == Direction.Positive)
+            {
+                if (position[face.Axis] == 0)
+                {
+                    position[face.Axis] = 1;
+                    return children[position.Index];
+                }
+            }
+            else
+            {
+                if (position[face.Axis] == 1)
+                {
+                    position[face.Axis] = 0;
+                    return children[position.Index];
+                }
+            }
+
+            return adjacency[face.Index];
+        }
+
+        public void RefineAdjacency()
+        {
+            Vector3 center = Center;
+            float size = Size;
+
+            foreach (var face in Face.All)
+            {
+                var node = adjacency[face.Index];
+
+                if (node != null && !node.IsLeaf && node.Size > Size)
+                {
+                    Vector3 adjacentCenter = MovePoint(center, face, size);
+                    adjacency[face.Index] = node.FindLargestOrEqual(adjacentCenter, size);
+                }
+            }
+        }
+
+        public QuadtreeNode BuildFaceQuadTree(Face face)
+        {
+            Vector3 faceCenter = MovePoint(Center, face, Size * 0.5f);
+            var quadTreeNode = new QuadtreeNode(faceCenter, Size, face);
+            quadTreeNode.Build(adjacency[face.Index]);
+            return quadTreeNode;
+        }
+
+        public void DfsTraversal(Action<OctreeNode> action)
+        {
+            action(this);
+
+            if (!IsLeaf)
+            {
+                foreach (var child in children)
+                    child.DfsTraversal(action);
+            }
+        }
+
+        public static Vector3 MovePoint(Vector3 point, Face face, float delta)
+        {
+            if (face.Direction == Direction.Negative)
+                delta = -delta;
+
+            if (face.Axis == Axis.X)
+                point.X += delta;
+            else if (face.Axis == Axis.Y)
+                point.Y += delta;
+            else
+                point.Z += delta;
+
+            return point;
+        }
+
+        private struct TriangleBoxIntersector
+        {
+            private Vector3 center;
+            private Vector3 size;
+            private Vector3[] triangle;
+            private Vector3 edge;
+
+            public TriangleBoxIntersector(ref BoundingBox box)
+            {
+                center = (box.Min + box.Max) * 0.5f;
+                size = (box.Max - box.Min) * 0.5f;
+
+                triangle = new Vector3[3];
+                edge = Vector3.Zero;
+            }
+
+            public Vector3[] Triangle => triangle;
+
+            public bool Intersect()
+            {
+                for (int i = 0; i < triangle.Length; i++)
+                    triangle[i] -= center;
+
+                edge = triangle[1] - triangle[0];
+
+                if (AxisTest(Y, Z, 0, 2) || AxisTest(Z, X, 0, 2) || AxisTest(X, Y, 2, 1))
+                    return false;
+
+                edge = triangle[2] - triangle[1];
+
+                if (AxisTest(Y, Z, 0, 2) || AxisTest(Z, X, 0, 2) || AxisTest(X, Y, 0, 1))
+                    return false;
+
+                edge = triangle[0] - triangle[2];
+
+                if (AxisTest(Y, Z, 0, 1) || AxisTest(Z, X, 0, 1) || AxisTest(X, Y, 2, 1))
+                    return false;
+
+                return true;
+            }
+
+            private const int X = 0;
+            private const int Y = 1;
+            private const int Z = 2;
+
+            private bool AxisTest(int a1, int a2, int p0, int p1)
+            {
+                Vector3 v0 = triangle[p0];
+                Vector3 v1 = triangle[p1];
+                float e1 = edge[a1];
+                float e2 = edge[a2];
+
+                float c0 = e2 * v0[a1] - e1 * v0[a2];
+                float c1 = e2 * v1[a1] - e1 * v1[a2];
+                float rad = Math.Abs(e2) * size[a1] + Math.Abs(e1) * size[a2];
+
+                return (c0 < c1) ? (c0 > rad || c1 < -rad) : (c1 > rad || c0 < -rad);
+            }
+        }
+
+        private struct PolygonBoxIntersector
+        {
+            private BoundingBox bbox;
+            private TriangleBoxIntersector triangleBoxIntersector;
+
+            public PolygonBoxIntersector(ref BoundingBox bbox)
+            {
+                this.bbox = bbox;
+                this.triangleBoxIntersector = new TriangleBoxIntersector(ref bbox);
+            }
+
+            public bool Intersects(Polygon polygon)
+            {
+                if (!bbox.Intersects(polygon.BoundingBox))
+                    return false;
+
+                if (!bbox.Intersects(polygon.Plane))
+                    return false;
+
+                var intersector = new TriangleBoxIntersector(ref bbox);
+                var points = polygon.Mesh.Points;
+                var indices = polygon.PointIndices;
+
+                intersector.Triangle[0] = points[indices[0]];
+                intersector.Triangle[1] = points[indices[1]];
+                intersector.Triangle[2] = points[indices[2]];
+
+                if (intersector.Intersect())
+                    return true;
+
+                if (indices.Length > 3)
+                {
+                    intersector.Triangle[0] = points[indices[2]];
+                    intersector.Triangle[1] = points[indices[3]];
+                    intersector.Triangle[2] = points[indices[0]];
+
+                    if (intersector.Intersect())
+                        return true;
+                }
+
+                return false;
+            }
+        }
+
+        public OctreeNode FindLargestOrEqual(Vector3 point, float largestSize)
+        {
+            var node = this;
+
+            while (!node.IsLeaf && node.Size > largestSize)
+            {
+                Vector3 center = node.Center;
+
+                int nx = (point.X < center.X) ? 0 : 4;
+                int ny = (point.Y < center.Y) ? 0 : 2;
+                int nz = (point.Z < center.Z) ? 0 : 1;
+
+                var childNode = node.children[nx + ny + nz];
+
+                if (childNode.Size < largestSize)
+                    break;
+
+                node = childNode;
+            }
+
+            return node;
+        }
+
+        public OctreeNode FindLeaf(Vector3 point)
+        {
+            if (!bbox.Contains(point))
+                return null;
+
+            if (children == null)
+                return this;
+
+            Vector3 center = Center;
+
+            int nx = (point.X < center.X) ? 0 : 4;
+            int ny = (point.Y < center.Y) ? 0 : 2;
+            int nz = (point.Z < center.Z) ? 0 : 1;
+
+            OctreeNode childNode = children[nx + ny + nz];
+
+            return childNode.FindLeaf(point);
+        }
+
+        public IEnumerable<OctreeNode> FindLeafs(BoundingBox box)
+        {
+            var stack = new Stack<OctreeNode>();
+            stack.Push(this);
+
+            while (stack.Count > 0)
+            {
+                var node = stack.Pop();
+
+                if (node.bbox.Intersects(box))
+                {
+                    if (node.children != null)
+                    {
+                        foreach (OctreeNode child in node.children)
+                            stack.Push(child);
+                    }
+                    else
+                    {
+                        yield return node;
+                    }
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Akira/Polygon.cs
===================================================================
--- /OniSplit/Akira/Polygon.cs	(revision 1114)
+++ /OniSplit/Akira/Polygon.cs	(revision 1114)
@@ -0,0 +1,293 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class Polygon
+    {
+        #region Private data
+        private PolygonMesh mesh;
+        private GunkFlags flags;
+        private int[] pointIndices;
+        private int[] texCoordIndices;
+        private int[] normalIndices;
+        private Color[] colors;
+        private Material material;
+        private Plane plane;
+        private int objectType = -1;
+        private int objectId = -1;
+        private int scriptId;
+        private string fileName;
+        private string objectName;
+        private PolygonEdge[] edges;
+        private BoundingBox bbox;
+        #endregion
+
+        public Polygon(PolygonMesh mesh, int[] pointIndices)
+        {
+            this.mesh = mesh;
+            this.pointIndices = pointIndices;
+            this.plane = GetPlane();
+            this.bbox = GetBoundingBox();
+
+            BuildFlags();
+        }
+
+        public Polygon(PolygonMesh mesh, int[] pointIndices, GunkFlags flags)
+            : this(mesh, pointIndices)
+        {
+            this.flags |= flags;
+        }
+
+        public Polygon(PolygonMesh mesh, int[] pointIndices, Plane plane)
+        {
+            this.mesh = mesh;
+            this.pointIndices = pointIndices;
+            this.plane = plane;
+
+            BuildFlags();
+        }
+
+        public PolygonMesh Mesh => mesh;
+
+        public GunkFlags Flags
+        {
+            get
+            {
+                if (material == null)
+                    return flags;
+
+                return flags | material.Flags;
+            }
+            set
+            {
+                flags = value;
+            }
+        }
+
+        public bool IsTransparent => (Flags & GunkFlags.Transparent) != 0;
+        public bool IsStairs=>(Flags & GunkFlags.Stairs) != 0;
+
+        public Material Material
+        {
+            get { return material; }
+            set { material = value; }
+        }
+
+        public int VertexCount => pointIndices.Length;
+
+        public int[] PointIndices => pointIndices;
+
+        public IEnumerable<Vector3> Points
+        {
+            get
+            {
+                foreach (int i in pointIndices)
+                    yield return mesh.Points[i];
+            }
+        }
+
+        public int[] TexCoordIndices
+        {
+            get { return texCoordIndices; }
+            set { texCoordIndices = value; }
+        }
+
+        public int[] NormalIndices
+        {
+            get { return normalIndices; }
+            set { normalIndices = value; }
+        }
+
+        public Color[] Colors
+        {
+            get { return colors; }
+            set { colors = value; }
+        }
+
+        public Plane Plane => plane;
+
+        public int ObjectType
+        {
+            get { return objectType; }
+            set { objectType = value; }
+        }
+
+        public int ObjectId
+        {
+            get { return objectId; }
+            set { objectId = value; }
+        }
+
+        public int ScriptId
+        {
+            get { return scriptId; }
+            set { scriptId = value; }
+        }
+
+        public string FileName
+        {
+            get { return fileName; }
+            set { fileName = value; }
+        }
+
+        public string ObjectName
+        {
+            get { return objectName; }
+            set { objectName = value; }
+        }
+
+        private Plane GetPlane()
+        {
+            var plane = new Plane(
+                mesh.Points[pointIndices[0]],
+                mesh.Points[pointIndices[1]],
+                mesh.Points[pointIndices[2]]);
+
+            var bbox = GetBoundingBox();
+            var bboxSize = bbox.Max - bbox.Min;
+
+            if (Math.Abs(bboxSize.X) < 0.0001f)
+            {
+                if (plane.Normal.X < 0.0f)
+                    plane = new Plane(Vector3.Left, bbox.Min.X);
+                else
+                    plane = new Plane(Vector3.Right, -bbox.Max.X);
+            }
+            else if (Math.Abs(bboxSize.Y) < 0.0001f)
+            {
+                if (plane.Normal.Y < 0.0f)
+                    plane = new Plane(Vector3.Down, bbox.Min.Y);
+                else
+                    plane = new Plane(Vector3.Up, -bbox.Max.Y);
+            }
+            else if (Math.Abs(bboxSize.Z) < 0.0001f)
+            {
+                if (plane.Normal.Z < 0.0f)
+                    plane = new Plane(Vector3.Forward, bbox.Min.Z);
+                else
+                    plane = new Plane(Vector3.Backward, -bbox.Max.Z);
+            }
+            else
+            {
+                plane.Normal.X = FMath.Round(plane.Normal.X, 4);
+                plane.Normal.Y = FMath.Round(plane.Normal.Y, 4);
+                plane.Normal.Z = FMath.Round(plane.Normal.Z, 4);
+            }
+
+            return plane;
+        }
+
+        public BoundingBox BoundingBox => bbox;
+
+        private BoundingBox GetBoundingBox()
+        {
+            var point = mesh.Points[pointIndices[0]];
+            var bbox = new BoundingBox(point, point);
+
+            for (int i = 1; i < pointIndices.Length; i++)
+            {
+                point = mesh.Points[pointIndices[i]];
+
+                Vector3.Min(ref bbox.Min, ref point, out bbox.Min);
+                Vector3.Max(ref bbox.Max, ref point, out bbox.Max);
+            }
+
+            return bbox;
+        }
+
+        private void BuildFlags()
+        {
+            SetProjectionPlane();
+            SetHorizontalVertical();
+        }
+
+        private void SetHorizontalVertical()
+        {
+            if (Math.Abs(Vector3.Dot(plane.Normal, Vector3.UnitY)) < 0.3420201f)
+                flags |= GunkFlags.Vertical;
+            else
+                flags |= GunkFlags.Horizontal;
+        }
+
+        private void SetProjectionPlane()
+        {
+            var points = new Vector3[pointIndices.Length];
+
+            for (int i = 0; i < pointIndices.Length; i++)
+                points[i] = mesh.Points[pointIndices[i]];
+
+            float xyArea = MathHelper.Area(Project(points, PolygonProjectionPlane.XY));
+            float yzArea = MathHelper.Area(Project(points, PolygonProjectionPlane.YZ));
+            float xzArea = MathHelper.Area(Project(points, PolygonProjectionPlane.XZ));
+
+            var plane = PolygonProjectionPlane.None;
+
+            if (xyArea > yzArea)
+            {
+                if (xyArea > xzArea)
+                    plane = PolygonProjectionPlane.XY;
+                else
+                    plane = PolygonProjectionPlane.XZ;
+            }
+            else
+            {
+                if (yzArea > xzArea)
+                    plane = PolygonProjectionPlane.YZ;
+                else
+                    plane = PolygonProjectionPlane.XZ;
+            }
+
+            flags |= (GunkFlags)((int)plane << 25);
+        }
+
+        private static Vector2[] Project(Vector3[] points, PolygonProjectionPlane plane)
+        {
+            var result = new Vector2[points.Length];
+
+            switch (plane)
+            {
+                case PolygonProjectionPlane.XY:
+                    for (int i = 0; i < points.Length; i++)
+                    {
+                        result[i].X = points[i].X;
+                        result[i].Y = points[i].Y;
+                    }
+                    break;
+                case PolygonProjectionPlane.XZ:
+                    for (int i = 0; i < points.Length; i++)
+                    {
+                        result[i].X = points[i].X;
+                        result[i].Y = points[i].Z;
+                    }
+                    break;
+                case PolygonProjectionPlane.YZ:
+                    for (int i = 0; i < points.Length; i++)
+                    {
+                        result[i].X = points[i].Z;
+                        result[i].Y = points[i].Y;
+                    }
+                    break;
+            }
+
+            return result;
+        }
+
+        public PolygonEdge[] Edges
+        {
+            get
+            {
+                if (edges == null)
+                {
+                    edges = new PolygonEdge[pointIndices.Length];
+
+                    for (int i = 0; i < edges.Length; i++)
+                        edges[i] = new PolygonEdge(this, i);
+                }
+
+                return edges;
+            }
+        }
+    }
+}
Index: /OniSplit/Akira/Polygon2Clipper.cs
===================================================================
--- /OniSplit/Akira/Polygon2Clipper.cs	(revision 1114)
+++ /OniSplit/Akira/Polygon2Clipper.cs	(revision 1114)
@@ -0,0 +1,247 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal struct Polygon2Clipper
+    {
+        private readonly List<Polygon2> result;
+        private readonly RoomBspNode bspTree;
+
+        #region private struct Line
+
+        private struct Line
+        {
+            private float a, c, d;
+
+            public Line(Plane plane)
+            {
+                a = plane.Normal.X;
+                c = plane.Normal.Z;
+                d = plane.D;
+            }
+
+            public int RelativePosition(Vector2 point)
+            {
+                float r = a * point.X + c * point.Y + d;
+
+                if (r < -0.00001f)
+                    return -1;
+                else if (r > 0.00001f)
+                    return 1;
+                else
+                    return 0;
+            }
+
+            public Vector2 Intersect(Vector2 p0, Vector2 p1)
+            {
+                if (p0.X == p1.X)
+                {
+                    float x = p0.X;
+                    float y = (d + a * x) / -c;
+                    return new Vector2(x, y);
+                }
+                else if (p0.Y == p1.Y)
+                {
+                    float x = (-c * p0.Y - d) / a;
+                    float y = p0.Y;
+                    return new Vector2(x, y);
+                }
+                else
+                {
+                    float m = (p1.Y - p0.Y) / (p1.X - p0.X);
+                    float x = (c * m * p0.X - c * p0.Y - d) / (a + c * m);
+                    float y = m * (x - p0.X) + p0.Y;
+                    return new Vector2(x, y);
+                }
+            }
+        }
+
+        #endregion
+
+        public Polygon2Clipper(RoomBspNode bspTree)
+        {
+            this.result = new List<Polygon2>();
+            this.bspTree = bspTree;
+        }
+
+        public IEnumerable<Polygon2> Clip(Polygon2 polygon)
+        {
+            result.Clear();
+            Clip(new[] { polygon }, bspTree);
+            return result;
+        }
+
+        private void Clip(IEnumerable<Polygon2> polygons, RoomBspNode node)
+        {
+            var negative = new List<Polygon2>();
+            var positive = new List<Polygon2>();
+
+            var plane = node.Plane;
+
+            if (Math.Abs(plane.Normal.Y) > 0.001f)
+            {
+                negative.AddRange(polygons);
+                positive.AddRange(polygons);
+            }
+            else
+            {
+                var line = new Line(plane);
+
+                foreach (Polygon2 polygon in polygons)
+                    Clip(polygon, line, negative, positive);
+            }
+
+            if (node.FrontChild != null)
+                Clip(positive, node.FrontChild);
+
+            if (node.BackChild != null)
+                Clip(negative, node.BackChild);
+            else
+                result.AddRange(negative);
+        }
+
+        private static void Clip(Polygon2 polygon, Line line, List<Polygon2> negative, List<Polygon2> positive)
+        {
+            var signs = new int[polygon.Length];
+            int positiveCount = 0, negativeCount = 0;
+
+            for (int i = 0; i < polygon.Length; i++)
+            {
+                signs[i] = line.RelativePosition(polygon[i]);
+
+                if (signs[i] >= 0)
+                    positiveCount++;
+
+                if (signs[i] <= 0)
+                    negativeCount++;
+            }
+
+            if (negativeCount == polygon.Length)
+            {
+                //
+                // All points are in the negative half plane, nothing to clip.
+                //
+
+                negative.Add(polygon);
+                return;
+            }
+
+            if (positiveCount == polygon.Length)
+            {
+                //
+                // All points are in the positive half plane, nothing to clip.
+                //
+
+                positive.Add(polygon);
+                return;
+            }
+
+            var negativePoints = new List<Vector2>();
+            var positivePoints = new List<Vector2>();
+
+            int start = 0;
+            Vector2 p0;
+            int s0;
+
+            do
+            {
+                p0 = polygon[start];
+                s0 = signs[start];
+                start++;
+
+                //
+                // do not start right on the clip line
+                //
+
+            } while (s0 == 0);
+
+            var intersections = new Vector2[2];
+            int intersectionCount = 0;
+
+            for (int i = 0; i < polygon.Length; i++)
+            {
+                Vector2 p1 = polygon[(i + start) % polygon.Length];
+                int s1 = signs[(i + start) % polygon.Length];
+
+                if (s0 == s1)
+                {
+                    //
+                    // Same half plane, no intersection, add the existing edge.
+                    //
+
+                    if (s0 < 0)
+                        negativePoints.Add(p0);
+                    else
+                        positivePoints.Add(p0);
+                }
+                else if (s0 == 0)
+                {
+                    //
+                    // If the previous point was on the clip line then we need
+                    // to use the current point sign to figure out the destination polygon.
+                    //
+
+                    if (s1 < 0)
+                        negativePoints.Add(p0);
+                    else
+                        positivePoints.Add(p0);
+                }
+                else
+                {
+                    //
+                    // Different half plane, split the edge in two.
+                    //
+
+                    Vector2 intersection;
+
+                    if (s1 == 0)
+                        intersection = p1;
+                    else
+                        intersection = line.Intersect(p0, p1);
+
+                    intersections[intersectionCount++] = intersection;
+
+                    if (s0 < 0)
+                    {
+                        // the negative polygon needs to be closed
+                        // the positive polygon needs to have an edge added from the previous intersection to the new one
+
+                        negativePoints.Add(p0);
+
+                        if (intersectionCount == 2)
+                        {
+                            negativePoints.Add(intersection);
+                            positivePoints.Add(intersections[0]);
+                        }
+
+                        if (s1 != 0)
+                            positivePoints.Add(intersection);
+                    }
+                    else
+                    {
+                        // the positive polygon needs to be closed
+                        // the negative polygon needs to have an edge added from the previous intersection to the new one
+
+                        positivePoints.Add(p0);
+
+                        if (intersectionCount == 2)
+                        {
+                            positivePoints.Add(intersection);
+                            negativePoints.Add(intersections[0]);
+                        }
+
+                        if (s1 != 0)
+                            negativePoints.Add(intersection);
+                    }
+                }
+
+                p0 = p1;
+                s0 = s1;
+            }
+
+            negative.Add(new Polygon2(negativePoints.ToArray()));
+            positive.Add(new Polygon2(positivePoints.ToArray()));
+        }
+    }
+}
Index: /OniSplit/Akira/PolygonEdge.cs
===================================================================
--- /OniSplit/Akira/PolygonEdge.cs	(revision 1114)
+++ /OniSplit/Akira/PolygonEdge.cs	(revision 1114)
@@ -0,0 +1,32 @@
+﻿using System;
+
+namespace Oni.Akira
+{
+    internal class PolygonEdge
+    {
+        private static readonly PolygonEdge[] emptyEdges = new PolygonEdge[0];
+        private readonly Polygon polygon;
+        private readonly int index;
+        private PolygonEdge[] adjacency = emptyEdges;
+
+        public PolygonEdge(Polygon polygon, int index)
+        {
+            this.polygon = polygon;
+            this.index = index;
+        }
+
+        public Polygon Polygon => polygon;
+
+        public int Index => index;
+        public int EndIndex => (index + 1) % polygon.Edges.Length;
+
+        public int Point0Index => polygon.PointIndices[index];
+        public int Point1Index => polygon.PointIndices[EndIndex];
+
+        public PolygonEdge[] Adjancency
+        {
+            get { return adjacency; }
+            set { adjacency = value; }
+        }
+    }
+}
Index: /OniSplit/Akira/PolygonMesh.cs
===================================================================
--- /OniSplit/Akira/PolygonMesh.cs	(revision 1114)
+++ /OniSplit/Akira/PolygonMesh.cs	(revision 1114)
@@ -0,0 +1,105 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class PolygonMesh
+    {
+        private readonly MaterialLibrary materialLibrary;
+        private readonly List<Vector3> points = new List<Vector3>();
+        private readonly List<Vector3> normals = new List<Vector3>();
+        private readonly List<Vector2> texCoords = new List<Vector2>();
+        private readonly List<Polygon> polygons = new List<Polygon>();
+        private readonly List<Polygon> doors = new List<Polygon>();
+        private readonly List<Room> rooms = new List<Room>();
+        private readonly List<Polygon> ghosts = new List<Polygon>();
+        private readonly List<Polygon> floors = new List<Polygon>();
+        private bool hasDebugInfo;
+
+        public PolygonMesh(MaterialLibrary materialLibrary)
+        {
+            this.materialLibrary = materialLibrary;
+        }
+
+        public MaterialLibrary Materials => materialLibrary;
+
+        public List<Vector3> Points => points;
+        public List<Vector2> TexCoords => texCoords;
+        public List<Vector3> Normals => normals;
+        public List<Polygon> Polygons => polygons;
+        public List<Polygon> Doors => doors;
+        public List<Room> Rooms => rooms;
+        public List<Polygon> Floors => floors;
+        public List<Polygon> Ghosts => ghosts;
+
+        public bool HasDebugInfo
+        {
+            get { return hasDebugInfo; }
+            set { hasDebugInfo = value; }
+        }
+
+        public BoundingBox GetBoundingBox() => BoundingBox.CreateFromPoints(points);
+
+        public void DoLighting()
+        {
+            var ambientColor = new Vector3(0.6f, 0.6f, 0.6f);
+
+            var lightDir = new[] {
+                new Vector3(-0.526f, -0.573f, -0.627f),
+                new Vector3(0.719f, 0.342f, 0.604f),
+                new Vector3(0.454f, 0.766f, 0.454f)
+            };
+
+            var lightColor = new[] {
+                new Vector3(1.0f, 1.0f, 1.0f),
+                new Vector3(1.0f, 1.0f, 1.0f),
+                new Vector3(1.0f, 1.0f, 1.0f)
+            };
+
+            //if (importLights)
+            //{
+            //    ambientColor = ambient;
+            //}
+
+            foreach (var polygon in polygons)
+            {
+                if (polygon.Colors != null)
+                    continue;
+
+                var colors = new Color[polygon.VertexCount];
+
+                if (polygon.NormalIndices != null)
+                {
+                    for (int i = 0; i < colors.Length; i++)
+                    {
+                        var color = ambientColor;
+
+                        for (int j = 0; j < lightDir.Length; j++)
+                        {
+                            float dot = Vector3.Dot(lightDir[j], normals[polygon.NormalIndices[i]]);
+                            color += lightColor[j] * dot;
+                        }
+
+                        colors[i] = new Color(Vector3.Clamp(color, Vector3.Zero, Vector3.One));
+                    }
+                }
+                else
+                {
+                    var color = ambientColor;
+
+                    for (int j = 0; j < lightDir.Length; j++)
+                    {
+                        float dot = Vector3.Dot(lightDir[j], polygon.Plane.Normal);
+                        color += lightColor[j] * dot;
+                    }
+
+                    for (int i = 0; i < colors.Length; i++)
+                        colors[i] = new Color(Vector3.Clamp(color, Vector3.Zero, Vector3.One));
+                }
+
+                polygon.Colors = colors;
+            }
+        }
+    }
+}
Index: /OniSplit/Akira/PolygonQuadrangulate.cs
===================================================================
--- /OniSplit/Akira/PolygonQuadrangulate.cs	(revision 1114)
+++ /OniSplit/Akira/PolygonQuadrangulate.cs	(revision 1114)
@@ -0,0 +1,269 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal class PolygonQuadrangulate
+    {
+        private readonly PolygonMesh mesh;
+
+        public PolygonQuadrangulate(PolygonMesh mesh)
+        {
+            this.mesh = mesh;
+        }
+
+        public void Execute()
+        {
+            GenerateAdjacency();
+
+            var candidates = new List<QuadCandidate>();
+            var polygons = mesh.Polygons;
+
+            var newPolygons = new Polygon[polygons.Count];
+            var marked = new bool[polygons.Count];
+            int quadCount = 0;
+
+            for (int i = 0; i < polygons.Count; i++)
+            {
+                var p1 = polygons[i];
+
+                if (marked[i] || p1.Edges.Length != 3)
+                    continue;
+
+                candidates.Clear();
+
+                foreach (PolygonEdge e1 in p1.Edges)
+                {
+                    foreach (PolygonEdge e2 in e1.Adjancency)
+                    {
+                        if (marked[polygons.IndexOf(e2.Polygon)])
+                            continue;
+
+                        if (QuadCandidate.IsQuadCandidate(e1, e2))
+                            candidates.Add(new QuadCandidate(e1, e2));
+                    }
+                }
+
+                if (candidates.Count > 0)
+                {
+                    candidates.Sort();
+                    newPolygons[i] = candidates[0].CreateQuad(mesh);
+
+                    int k = polygons.IndexOf(candidates[0].Polygon2);
+
+                    marked[i] = true;
+                    marked[k] = true;
+
+                    quadCount++;
+                }
+            }
+
+            var newPolygonList = new List<Polygon>(polygons.Count - quadCount);
+
+            for (int i = 0; i < polygons.Count; i++)
+            {
+                if (newPolygons[i] != null)
+                    newPolygonList.Add(newPolygons[i]);
+                else if (!marked[i])
+                    newPolygonList.Add(polygons[i]);
+            }
+
+            polygons = newPolygonList;
+        }
+
+        #region private class QuadCandidate
+
+        private class QuadCandidate : IComparable<QuadCandidate>
+        {
+            private PolygonEdge e1;
+            private PolygonEdge e2;
+            private float l;
+
+            public static bool IsQuadCandidate(PolygonEdge e1, PolygonEdge e2)
+            {
+                //
+                // To merge 2 polygons into one the following must be true:
+                // - both must be triangles
+                // - they must share the same plane
+                // - they must use the same material
+                // - TODO: the resulting polygon must be convex!!!
+                // - TODO: must have the same texture coordinates
+                //
+
+                Polygon p1 = e1.Polygon;
+                Polygon p2 = e2.Polygon;
+
+                return (
+                       p1.Edges.Length == 3
+                    && p2.Edges.Length == 3
+                    && p1.Plane == p2.Plane
+                    && p1.Material == p2.Material
+                    //&& e1.Point0Index == e2.Point0Index
+                    //&& e1.Point1Index == e2.Point1Index
+                    );
+            }
+
+            public QuadCandidate(PolygonEdge e1, PolygonEdge e2)
+            {
+                this.e1 = e1;
+                this.e2 = e2;
+
+                List<Vector3> points = e1.Polygon.Mesh.Points;
+                this.l = Vector3.DistanceSquared(points[e1.Point0Index], points[e1.Point1Index]);
+            }
+
+            public Polygon Polygon1 => e1.Polygon;
+            public Polygon Polygon2 => e2.Polygon;
+
+            public int CompareTo(QuadCandidate other)
+            {
+                return l.CompareTo(other.l);
+            }
+
+            public Polygon CreateQuad(PolygonMesh mesh)
+            {
+                int[] newPoints = new int[4];
+                int[] newTexCoords = new int[4];
+                int l = 0;
+
+                newPoints[l] = e1.Polygon.PointIndices[e1.EndIndex];
+                newTexCoords[l] = e1.Polygon.TexCoordIndices[e1.EndIndex];
+                l++;
+
+                for (int k = 0; k < 3; k++)
+                {
+                    if (k != e1.Index && k != e1.EndIndex)
+                    {
+                        newPoints[l] = e1.Polygon.PointIndices[k];
+                        newTexCoords[l] = e1.Polygon.TexCoordIndices[k];
+                        l++;
+
+                        break;
+                    }
+                }
+
+                newPoints[l] = e1.Polygon.PointIndices[e1.Index];
+                newTexCoords[l] = e1.Polygon.TexCoordIndices[e1.Index];
+                l++;
+
+                for (int k = 0; k < 3; k++)
+                {
+                    if (k != e2.Index && k != e2.EndIndex)
+                    {
+                        newPoints[l] = e2.Polygon.PointIndices[k];
+                        newTexCoords[l] = e2.Polygon.TexCoordIndices[k];
+                        l++;
+
+                        break;
+                    }
+                }
+
+                return new Polygon(mesh, newPoints, e1.Polygon.Plane)
+                {
+                    TexCoordIndices = newTexCoords,
+                    Material = e1.Polygon.Material
+                };
+            }
+        }
+
+        #endregion
+
+        private void GenerateAdjacency()
+        {
+            var points = mesh.Points;
+            var polygons = mesh.Polygons;
+
+            var pointUseCount = new int[points.Count];
+            var pointUsage = new int[points.Count][];
+
+            foreach (var polygon in polygons)
+            {
+                foreach (int i in polygon.PointIndices)
+                    pointUseCount[i]++;
+            }
+
+            for (int polygon = 0; polygon < polygons.Count; polygon++)
+            {
+                foreach (int i in polygons[polygon].PointIndices)
+                {
+                    int useCount = pointUseCount[i];
+                    int[] pa = pointUsage[i];
+
+                    if (pa == null)
+                    {
+                        pa = new int[useCount];
+                        pointUsage[i] = pa;
+                    }
+
+                    pa[pa.Length - useCount] = polygon;
+                    pointUseCount[i] = useCount - 1;
+                }
+            }
+
+            var adjacencyBuffer = new List<PolygonEdge>();
+
+            foreach (var p1 in polygons)
+            {
+                foreach (var e1 in p1.Edges)
+                {
+                    adjacencyBuffer.Clear();
+
+                    int[] a0 = pointUsage[e1.Point0Index];
+                    int[] a1 = pointUsage[e1.Point1Index];
+
+                    if (a0 == null || a1 == null)
+                        continue;
+
+                    foreach (int pi2 in MatchSortedArrays(a0, a1))
+                    {
+                        var p2 = polygons[pi2];
+
+                        if (p2 == p1)
+                            continue;
+
+                        foreach (var e2 in p2.Edges)
+                        {
+                            if (e1.Point0Index == e2.Point1Index && e1.Point1Index == e2.Point0Index
+                                || e1.Point0Index == e2.Point0Index && e1.Point1Index == e2.Point1Index)
+                            {
+                                adjacencyBuffer.Add(e2);
+                            }
+                        }
+                    }
+
+                    e1.Adjancency = adjacencyBuffer.ToArray();
+                }
+            }
+        }
+
+        private static IEnumerable<int> MatchSortedArrays(int[] a1, int[] a2)
+        {
+            int l1 = a1.Length;
+            int l2 = a2.Length;
+            int i1 = 0;
+            int i2 = 0;
+
+            while (i1 < l1 && i2 < l2)
+            {
+                int v1 = a1[i1];
+                int v2 = a2[i2];
+
+                if (v1 < v2)
+                {
+                    i1++;
+                }
+                else if (v1 > v2)
+                {
+                    i2++;
+                }
+                else
+                {
+                    i1++;
+                    i2++;
+
+                    yield return v1;
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Akira/PolygonUtils.cs
===================================================================
--- /OniSplit/Akira/PolygonUtils.cs	(revision 1114)
+++ /OniSplit/Akira/PolygonUtils.cs	(revision 1114)
@@ -0,0 +1,95 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal class PolygonUtils
+    {
+        public static List<Vector3> ClipToPlane(List<Vector3> points, Plane plane)
+        {
+            var signs = new int[points.Count];
+            var negativeCount = 0;
+            var positiveCount = 0;
+            var start = 0;
+
+            for (int i = 0; i < points.Count; i++)
+            {
+                signs[i] = RelativePosition(points[i], plane);
+
+                if (signs[i] >= 0)
+                    positiveCount++;
+
+                if (signs[i] <= 0)
+                    negativeCount++;
+            }
+
+            if (negativeCount == points.Count)
+                return null;
+
+            if (positiveCount == points.Count)
+                return points;
+
+            var newPoints = new List<Vector3>();
+
+            for (int i = 0; i < points.Count; i++)
+            {
+                int i0 = (i + start) % points.Count;
+                int i1 = (i + start + 1) % points.Count;
+
+                int s0 = signs[i0];
+                int s1 = signs[i1];
+
+                if (s0 >= 0)
+                {
+                    newPoints.Add(points[i0]);
+
+                    if (s0 > 0 && s1 < 0)
+                        newPoints.Add(Intersect(points[i0], points[i1], plane));
+                }
+                else
+                {
+                    if (s0 < 0 && s1 > 0)
+                        newPoints.Add(Intersect(points[i1], points[i0], plane));
+                }
+            }
+
+            return newPoints;
+        }
+
+        private static Vector3 Intersect(Vector3 p0, Vector3 p1, Plane plane)
+        {
+            Vector3 dir = p1 - p0;
+
+            float dND = plane.DotNormal(dir);
+
+            if (Math.Abs(dND) < 1e-05f)
+                throw new InvalidOperationException();
+
+            float distance = (-plane.D - plane.DotNormal(p0)) / dND;
+
+            if (distance < 0.0f)
+            {
+                if (distance < -1e-05f)
+                    throw new InvalidOperationException();
+
+                return p0;
+            }
+            else
+            {
+                return p0 + dir * distance;
+            }
+        }
+
+        private static int RelativePosition(Vector3 point, Plane plane)
+        {
+            float pos = plane.DotCoordinate(point);
+
+            if (pos < -1e-05f)
+                return -1;
+            else if (pos > 1e-05f)
+                return 1;
+            else
+                return 0;
+        }
+    }
+}
Index: /OniSplit/Akira/QuadtreeNode.cs
===================================================================
--- /OniSplit/Akira/QuadtreeNode.cs	(revision 1114)
+++ /OniSplit/Akira/QuadtreeNode.cs	(revision 1114)
@@ -0,0 +1,134 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal class QuadtreeNode
+    {
+        public const int ChildCount = 4;
+        private QuadtreeNode[] nodes = new QuadtreeNode[ChildCount];
+        private OctreeNode[] leafs = new OctreeNode[ChildCount];
+        private readonly Vector3 center;
+        private readonly float size;
+        private readonly OctreeNode.Face face;
+        private int index;
+
+        public enum Axis
+        {
+            U = 0,
+            V = 1
+        }
+
+        public struct ChildPosition
+        {
+            private readonly int index;
+
+            public ChildPosition(int index)
+            {
+                this.index = index;
+            }
+
+            public int Index => index;
+
+            public int U => this[Axis.U];
+            public int V => this[Axis.V];
+
+            public int this[Axis axis] => ((index >> (int)axis) & 1);
+
+            public static IEnumerable<ChildPosition> All
+            {
+                get
+                {
+                    for (int i = 0; i < 4; i++)
+                        yield return new ChildPosition(i);
+                }
+            }
+        }
+
+        public QuadtreeNode(Vector3 center, float size, OctreeNode.Face face)
+        {
+            this.center = center;
+            this.size = size;
+            this.face = face;
+        }
+
+        public QuadtreeNode[] Nodes => nodes;
+        public OctreeNode[] Leafs => leafs;
+
+        public void Build(OctreeNode adjacentNode)
+        {
+            float childSize = size * 0.5f;
+
+            foreach (var position in ChildPosition.All)
+            {
+                var childCenter = GetChildNodeCenter(position);
+                var adjacentCenter = OctreeNode.MovePoint(childCenter, face, childSize * 0.5f);
+                var childAdjacentNode = adjacentNode.FindLargestOrEqual(adjacentCenter, childSize);
+
+                if (childAdjacentNode.IsLeaf)
+                {
+                    leafs[position.Index] = childAdjacentNode;
+                }
+                else
+                {
+                    var child = new QuadtreeNode(childCenter, childSize, face);
+                    child.Build(childAdjacentNode);
+                    nodes[position.Index] = child;
+                }
+            }
+        }
+
+        private Vector3 GetChildNodeCenter(ChildPosition position)
+        {
+            float offset = size * 0.25f;
+
+            float u = (position.U == 0) ? -offset : offset;
+            float v = (position.V == 0) ? -offset : offset;
+
+            var result = center;
+
+            if (face.Axis == OctreeNode.Axis.X)
+            {
+                result.Y += u;
+                result.Z += v;
+            }
+            else if (face.Axis == OctreeNode.Axis.Y)
+            {
+                result.X += u;
+                result.Z += v;
+            }
+            else
+            {
+                result.X += u;
+                result.Y += v;
+            }
+
+            return result;
+        }
+
+        public int Index => index;
+
+        public List<QuadtreeNode> GetDfsList()
+        {
+            var list = new List<QuadtreeNode>();
+
+            DfsTraversal(node => {
+                node.index = list.Count;
+                list.Add(node);
+            });
+
+            return list;
+        }
+
+        private void DfsTraversal(Action<QuadtreeNode> action)
+        {
+            action(this);
+
+            foreach (var node in nodes)
+            {
+                if (node != null)
+                    node.DfsTraversal(action);
+            }
+        }
+    }
+}
Index: /OniSplit/Akira/Room.cs
===================================================================
--- /OniSplit/Akira/Room.cs	(revision 1114)
+++ /OniSplit/Akira/Room.cs	(revision 1114)
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+
+namespace Oni.Akira
+{
+    internal class Room
+    {
+        private Polygon floorPolygon;
+        private RoomBspNode bspTree;
+        private BoundingBox boundingBox;
+        private RoomGrid grid;
+        private Plane floorPlane;
+        private float height;
+        private readonly List<RoomAdjacency> adjacencies = new List<RoomAdjacency>();
+
+        public BoundingBox BoundingBox
+        {
+            get { return boundingBox; }
+            set { boundingBox = value; }
+        }
+
+        public RoomBspNode BspTree
+        {
+            get { return bspTree; }
+            set { bspTree = value; }
+        }
+
+        public RoomGrid Grid
+        {
+            get { return grid; }
+            set { grid = value; }
+        }
+
+        public Polygon FloorPolygon
+        {
+            get { return floorPolygon; }
+            set { floorPolygon = value; }
+        }
+
+        public Plane FloorPlane
+        {
+            get { return floorPlane; }
+            set { floorPlane = value; }
+        }
+
+        public bool IsStairs => floorPlane.Normal.Y < 0.999f;
+
+        public float Height
+        {
+            get { return height; }
+            set { height = value; }
+        }
+
+        public List<RoomAdjacency> Ajacencies => adjacencies;
+
+        public bool Contains(Vector3 point)
+        {
+            if (!boundingBox.Contains(point))
+                return false;
+
+            bool front = false;
+            RoomBspNode node = bspTree;
+
+            while (node != null)
+            {
+                front = (node.Plane.DotCoordinate(point) >= MathHelper.Eps);
+                node = front ? node.FrontChild : node.BackChild;
+            }
+
+            return !front;
+        }
+
+        public bool Intersect(BoundingBox bbox)
+        {
+            if (!boundingBox.Intersects(bbox))
+                return false;
+
+            bool front = false;
+            RoomBspNode node = bspTree;
+
+            while (node != null)
+            {
+                int intersects = node.Plane.Intersects(bbox);
+
+                if (intersects == 0)
+                    return true;
+
+                front = intersects > 0;
+                node = front ? node.FrontChild : node.BackChild;
+            }
+
+            return !front;
+        }
+
+        public List<Vector3[]> GetFloorPolygons()
+        {
+            var polys = new List<Vector3[]>();
+
+            if (floorPolygon != null)
+            {
+                polys.Add(floorPolygon.Points.ToArray());
+                return polys;
+            }
+
+            var min = new Vector2(boundingBox.Min.X, boundingBox.Min.Z);
+            var max = new Vector2(boundingBox.Max.X, boundingBox.Max.Z);
+
+            var root = new Polygon2(new[]
+            {
+                new Vector2(min.X, min.Y),
+                new Vector2(max.X, min.Y),
+                new Vector2(max.X, max.Y),
+                new Vector2(min.X, max.Y)
+            });
+
+            var cutter = new Polygon2Clipper(bspTree);
+
+            foreach (Polygon2 polygon in cutter.Clip(root))
+            {
+                var points = new Vector3[polygon.Length];
+
+                for (int i = 0; i < points.Length; i++)
+                {
+                    var point = polygon[i];
+
+                    points[i].X = point.X;
+                    points[i].Y = (-floorPlane.D - floorPlane.Normal.Z * point.Y - floorPlane.Normal.X * point.X) / floorPlane.Normal.Y;
+                    points[i].Z = point.Y;
+                }
+
+                Array.Reverse(points);
+
+                polys.Add(points);
+            }
+
+            return polys;
+        }
+    }
+}
Index: /OniSplit/Akira/RoomAdjacency.cs
===================================================================
--- /OniSplit/Akira/RoomAdjacency.cs	(revision 1114)
+++ /OniSplit/Akira/RoomAdjacency.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿namespace Oni.Akira
+{
+    internal class RoomAdjacency
+    {
+        private readonly Room adjacentRoom;
+        private readonly Polygon ghost;
+
+        public RoomAdjacency(Room room, Polygon ghost)
+        {
+            this.adjacentRoom = room;
+            this.ghost = ghost;
+        }
+
+        public Room AdjacentRoom => adjacentRoom;
+
+        public Polygon Ghost => ghost;
+    }
+}
Index: /OniSplit/Akira/RoomBspNode.cs
===================================================================
--- /OniSplit/Akira/RoomBspNode.cs	(revision 1114)
+++ /OniSplit/Akira/RoomBspNode.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿namespace Oni.Akira
+{
+    internal class RoomBspNode : BspNode<RoomBspNode>
+    {
+        public RoomBspNode(Plane plane, RoomBspNode backChild, RoomBspNode frontChild)
+            : base(plane, backChild, frontChild)
+        {
+        }
+    }
+}
Index: /OniSplit/Akira/RoomBuilder.cs
===================================================================
--- /OniSplit/Akira/RoomBuilder.cs	(revision 1114)
+++ /OniSplit/Akira/RoomBuilder.cs	(revision 1114)
@@ -0,0 +1,256 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class RoomBuilder
+    {
+        private const float roomHeight = 20.0f;
+        private readonly PolygonMesh mesh;
+        private OctreeNode octtree;
+
+        public static void BuildRooms(PolygonMesh mesh)
+        {
+            var builder = new RoomBuilder(mesh);
+            builder.BuildRooms();
+        }
+
+        private RoomBuilder(PolygonMesh mesh)
+        {
+            this.mesh = mesh;
+        }
+
+        private void BuildRooms()
+        {
+            foreach (Polygon floor in mesh.Floors)
+                mesh.Rooms.Add(CreateRoom(floor, roomHeight));
+
+            ConnectRooms();
+            UpdateRoomsHeight();
+        }
+
+        private Room CreateRoom(Polygon floor, float height)
+        {
+            var floorPlane = floor.Plane;
+
+            var bbox = floor.BoundingBox;
+            bbox.Max.Y += height * floorPlane.Normal.Y;
+
+            var room = new Room
+            {
+                FloorPolygon = floor,
+                BoundingBox = bbox,
+                FloorPlane = floor.Plane,
+                Height = height * floorPlane.Normal.Y,
+                BspTree = BuildBspTree(floor, height * floorPlane.Normal.Y)
+            };
+
+            if (floor.Material != null)
+                room.Grid = CreateRoomGrid(floor);
+
+            return room;
+        }
+
+        private static RoomBspNode BuildBspTree(Polygon floor, float height)
+        {
+            var points = floor.Points.ToArray();
+            var floorPlane = floor.Plane;
+
+            var bottom = new Plane(-floorPlane.Normal, -floorPlane.D);
+            var node = new RoomBspNode(bottom, null, null);
+
+            var top = new Plane(floorPlane.Normal, floorPlane.D - height);
+            node = new RoomBspNode(top, node, null);
+
+            for (int i = 0; i < points.Length; i++)
+            {
+                var p0 = points[i];
+                var p1 = points[(i + 1) % points.Length];
+                var p2 = p1 + Vector3.Up;
+
+                node = new RoomBspNode(new Plane(p0, p1, p2), node, null);
+            }
+
+            return node;
+        }
+
+        private static RoomGrid CreateRoomGrid(Polygon floor)
+        {
+            if (!File.Exists(floor.Material.ImageFilePath))
+                return null;
+
+            var image = TgaReader.Read(floor.Material.ImageFilePath);
+            var grid = RoomGrid.FromImage(image);
+
+            //BoundingBox bbox = floor.GetBoundingBox();
+
+            //
+            // TODO: don't use hardcoded constants
+            //
+
+            //int gx = (int)(((bbox.Max.X - bbox.Min.X) / 4) + 5);
+            //int gz = (int)(((bbox.Max.Z - bbox.Min.Z) / 4) + 5);
+
+            //if (gx != image.Width || gz != image.Height)
+            //{
+            //    //Console.Error.WriteLine("Warning: Grid {0} has wrong size, expecting {1}x{2}, got {3}x{4}",
+            //    //    floor.Material.Name,
+            //    //    gx, gz,
+            //    //    image.Width, image.Height);
+            //}
+
+            return grid;
+        }
+
+        private void ConnectRooms()
+        {
+            octtree = OctreeBuilder.BuildRoomsOctree(mesh);
+
+            foreach (Polygon ghost in mesh.Ghosts)
+            {
+                float minY = ghost.Points.Select(p => p.Y).Min();
+                Vector3[] points = ghost.Points.Where(p => Math.Abs(p.Y - minY) <= 0.1f).ToArray();
+
+                if (points.Length != 2)
+                {
+                    Console.Error.WriteLine("BNV Builder: Bad ghost, it must have 2 lowest points, it has {0}, ignoring", points.Length);
+                    continue;
+                }
+
+                Vector3 mid = (points[0] + points[1]) / 2.0f;
+                Vector3 normal = ghost.Plane.Normal;
+
+                Vector3 p0 = mid - normal + Vector3.Up * 2.0f;
+                Vector3 p1 = mid + normal + Vector3.Up * 2.0f;
+
+                RoomPair pair = PairRooms(p0, p1);
+
+                if (pair == null)
+                {
+                    Console.WriteLine("BNV Builder: Ghost '{0}' has no adjacencies at {1} and {2}, ignoring", ghost.ObjectName, p0, p1);
+                    continue;
+                }
+
+                if (pair.Room0.IsStairs || pair.Room1.IsStairs)
+                {
+                    var stairs = pair.Room0;
+
+                    if (!stairs.IsStairs)
+                        stairs = pair.Room1;
+
+                    ghost.Flags &= ~GunkFlags.Ghost;
+
+                    if (ghost.Material != null)
+                        ghost.Material.Flags &= ~GunkFlags.Ghost;
+
+                    if (ghost.BoundingBox.Min.Y > stairs.FloorPolygon.BoundingBox.Max.Y - 1.0f)
+                        ghost.Flags |= GunkFlags.StairsDown;
+                    else
+                        ghost.Flags |= GunkFlags.StairsUp;
+                }
+                else
+                {
+                    ghost.Flags |= GunkFlags.Ghost;
+                }
+
+                pair.Room1.Ajacencies.Add(new RoomAdjacency(pair.Room0, ghost));
+                pair.Room0.Ajacencies.Add(new RoomAdjacency(pair.Room1, ghost));
+            }
+        }
+
+        #region private class RoomPair
+
+        private class RoomPair : IComparable<RoomPair>
+        {
+            public readonly Room Room0;
+            public readonly Room Room1;
+            public readonly float HeightDelta;
+            public readonly float VolumeDelta;
+
+            public RoomPair(Room r0, Vector3 p0, Room r1, Vector3 p1)
+            {
+                Room0 = r0;
+                Room1 = r1;
+                HeightDelta = r0.FloorPlane.DotCoordinate(p0) - r1.FloorPlane.DotCoordinate(p1);
+                VolumeDelta = r0.BoundingBox.Volume() - r1.BoundingBox.Volume();
+            }
+
+            int IComparable<RoomPair>.CompareTo(RoomPair other)
+            {
+                if (Math.Abs(HeightDelta - other.HeightDelta) < 1e-5f)
+                    return VolumeDelta.CompareTo(other.VolumeDelta);
+                else if (HeightDelta < other.HeightDelta)
+                    return -1;
+                else
+                    return 1;
+            }
+        }
+
+        #endregion
+
+        private RoomPair PairRooms(Vector3 p0, Vector3 p1)
+        {
+            var pairs = new List<RoomPair>();
+
+            var rooms0 = FindRooms(p0);
+            var rooms1 = FindRooms(p1);
+
+            foreach (Room r0 in rooms0)
+            {
+                foreach (Room r1 in rooms1)
+                {
+                    if (r0 != r1)
+                        pairs.Add(new RoomPair(r0, p0, r1, p1));
+                }
+            }
+
+            pairs.Sort();
+
+            return pairs.Count > 0 ? pairs[0] : null;
+        }
+
+        private List<Room> FindRooms(Vector3 point)
+        {
+            var rooms = new List<Room>();
+
+            var node = octtree.FindLeaf(point);
+
+            if (node != null)
+            {
+                foreach (var room in node.Rooms)
+                {
+                    if (room.Contains(point))
+                        rooms.Add(room);
+                }
+            }
+
+            return rooms;
+        }
+
+        /// <summary>
+        /// Set the height of the room to the max height of adjacencies.
+        /// </summary>
+        private void UpdateRoomsHeight()
+        {
+            foreach (var room in mesh.Rooms)
+            {
+                float maxFloorY = room.FloorPolygon.Points.Max(p => p.Y);
+                float maxRoomY;
+
+                if (room.Ajacencies.Count == 0)
+                    maxRoomY = maxFloorY + 20.0f;
+                else
+                    maxRoomY = room.Ajacencies.Max(a => a.Ghost.Points.Max(p => p.Y));
+
+                var bbox = room.FloorPolygon.BoundingBox;
+                bbox.Max.Y = maxRoomY;
+
+                room.BoundingBox = bbox;
+                room.Height = (maxRoomY - maxFloorY) * room.FloorPlane.Normal.Y;
+                room.BspTree = BuildBspTree(room.FloorPolygon, room.Height);
+            }
+        }
+    }
+}
Index: /OniSplit/Akira/RoomDaeReader.cs
===================================================================
--- /OniSplit/Akira/RoomDaeReader.cs	(revision 1114)
+++ /OniSplit/Akira/RoomDaeReader.cs	(revision 1114)
@@ -0,0 +1,214 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class RoomDaeReader
+    {
+        private readonly PolygonMesh mesh;
+        private readonly List<Vector3> positions;
+        private readonly Stack<Matrix> nodeTransformStack;
+        private Dae.Scene scene;
+        private Matrix nodeTransform;
+
+        public static PolygonMesh Read(Dae.Scene scene)
+        {
+            var reader = new RoomDaeReader();
+            reader.ReadScene(scene);
+            return reader.mesh;
+        }
+
+        private RoomDaeReader()
+        {
+            mesh = new PolygonMesh(new MaterialLibrary());
+
+            positions = mesh.Points;
+
+            nodeTransformStack = new Stack<Matrix>();
+            nodeTransform = Matrix.Identity;
+        }
+
+        private void ReadScene(Dae.Scene scene)
+        {
+            this.scene = scene;
+
+            foreach (Dae.Node node in scene.Nodes)
+                ReadNode(node);
+        }
+
+        private void ReadNode(Dae.Node node)
+        {
+            nodeTransformStack.Push(nodeTransform);
+
+            foreach (var transform in node.Transforms)
+                nodeTransform = transform.ToMatrix() * nodeTransform;
+
+            foreach (var geometryInstance in node.GeometryInstances)
+                ReadGeometryInstance(node, geometryInstance);
+
+            foreach (var child in node.Nodes)
+                ReadNode(child);
+
+            nodeTransform = nodeTransformStack.Pop();
+        }
+
+        private void ReadGeometryInstance(Dae.Node node, Dae.GeometryInstance instance)
+        {
+            var geometry = instance.Target;
+
+            foreach (var primitives in geometry.Primitives)
+            {
+                if (primitives.PrimitiveType != Dae.MeshPrimitiveType.Polygons)
+                {
+                    Console.Error.WriteLine("Unsupported primitive type '{0}' found in geometry '{1}', ignoring.", primitives.PrimitiveType, geometry.Id);
+                    continue;
+                }
+
+                ReadPolygonPrimitives(node, primitives, instance.Materials.Find(m => m.Symbol == primitives.MaterialSymbol));
+            }
+        }
+
+        private void ReadPolygonPrimitives(Dae.Node node, Dae.MeshPrimitives primitives, Dae.MaterialInstance materialInstance)
+        {
+            var positionInput = primitives.Inputs.FirstOrDefault(i => i.Semantic == Dae.Semantic.Position);
+            var positionIndices = ReadInputIndexed(positionInput, positions, Dae.Source.ReadVector3);
+
+            foreach (int i in positionIndices)
+                positions[i] = Vector3.Transform(positions[i], ref nodeTransform);
+
+            int startIndex = 0;
+
+            foreach (int vertexCount in primitives.VertexCounts)
+            {
+                var polygon = CreatePolygon(positionIndices, startIndex, vertexCount);
+                startIndex += vertexCount;
+
+                if (polygon == null)
+                {
+                    Console.Error.WriteLine("BNV polygon: discarded, polygon is degenerate");
+                    continue;
+                }
+
+                polygon.FileName = node.FileName;
+                polygon.ObjectName = node.Name;
+
+                if (Math.Abs(polygon.Plane.Normal.Y) < 0.0001f)
+                {
+                    if (polygon.BoundingBox.Height < 1.0f)
+                    {
+                        Console.Error.WriteLine("BNV polygon: discarded, ghost height must be greater than 1, it is {0}", polygon.BoundingBox.Height);
+                        continue;
+                    }
+
+                    if (polygon.PointIndices.Length != 4)
+                    {
+                        Console.Error.WriteLine("BNV polygon: discarded, ghost is a {0}-gon", polygon.PointIndices.Length);
+                        continue;
+                    }
+
+                    mesh.Ghosts.Add(polygon);
+                }
+                else if ((polygon.Flags & GunkFlags.Horizontal) != 0)
+                {
+                    mesh.Floors.Add(polygon);
+                }
+                else
+                {
+                    Console.Error.WriteLine("BNV polygon: discarded, not a ghost and not a floor");
+                }
+            }
+        }
+
+        private Polygon CreatePolygon(int[] positionIndices, int startIndex, int vertexCount)
+        {
+            int endIndex = startIndex + vertexCount;
+
+            var indices = new List<int>(vertexCount);
+
+            for (int i = startIndex; i < endIndex; i++)
+            {
+                int i0 = positionIndices[i == startIndex ? endIndex - 1 : i - 1];
+                int i1 = positionIndices[i];
+                int i2 = positionIndices[i + 1 == endIndex ? startIndex : i + 1];
+
+                if (i0 == i1)
+                {
+                    Console.Error.WriteLine("BNV polygon: discarding degenerate edge {0}", mesh.Points[i1]);
+                    continue;
+                }
+
+                Vector3 p0 = mesh.Points[i0];
+                Vector3 p1 = mesh.Points[i1];
+                Vector3 p2 = mesh.Points[i2];
+
+                Vector3 p1p0 = p1 - p0;
+                Vector3 p2p1 = p2 - p1;
+
+                //if (p1p0.LengthSquared() < 0.000001f)
+                //{
+                //    Console.Error.WriteLine("BNV polygon: merging duplicate points {0} {1}", p0, p1);
+                //    continue;
+                //}
+
+                if (Vector3.Cross(p2p1, p1p0).LengthSquared() < 0.000001f)
+                {
+                    //Console.Error.WriteLine("BNV polygon: combining colinear edges at {0}", p1);
+                    continue;
+                }
+
+                indices.Add(i1);
+            }
+
+            var indicesArray = indices.ToArray();
+
+            if (CheckDegenerate(mesh.Points, indicesArray))
+                return null;
+
+            return new Polygon(mesh, indicesArray);
+        }
+
+        private static bool CheckDegenerate(List<Vector3> positions, int[] indices)
+        {
+            if (indices.Length < 3)
+                return true;
+
+            Vector3 p0 = positions[indices[0]];
+            Vector3 p1 = positions[indices[1]];
+            Vector3 s0, s1, c;
+
+            for (int i = 2; i < indices.Length; i++)
+            {
+                Vector3 p2 = positions[indices[i]];
+
+                Vector3.Substract(ref p0, ref p1, out s0);
+                Vector3.Substract(ref p2, ref p1, out s1);
+                Vector3.Cross(ref s0, ref s1, out c);
+
+                if (Math.Abs(c.LengthSquared()) < 0.0001f && Vector3.Dot(ref s0, ref s1) > 0.0f)
+                    return true;
+
+                p0 = p1;
+                p1 = p2;
+            }
+
+            return false;
+        }
+
+        private static int[] ReadInputIndexed<T>(Dae.IndexedInput input, List<T> list, Func<Dae.Source, int, T> elementReader)
+            where T : struct
+        {
+            var indices = new int[input.Indices.Count];
+
+            for (int i = 0; i < input.Indices.Count; i++)
+            {
+                var v = elementReader(input.Source, input.Indices[i]);
+                indices[i] = list.Count;
+                list.Add(v);
+            }
+
+            return indices;
+        }
+    }
+}
Index: /OniSplit/Akira/RoomDaeWriter.cs
===================================================================
--- /OniSplit/Akira/RoomDaeWriter.cs	(revision 1114)
+++ /OniSplit/Akira/RoomDaeWriter.cs	(revision 1114)
@@ -0,0 +1,376 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using Oni.Collections;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class RoomDaeWriter
+    {
+        #region Private data
+        private readonly PolygonMesh source;
+        private DaeSceneBuilder world;
+
+        private static readonly string[] objectTypeNames = new[] {
+            "",
+            "char",
+            "patr",
+            "door",
+            "flag",
+            "furn",
+            "",
+            "",
+            "part",
+            "pwru",
+            "sndg",
+            "trgv",
+            "weap",
+            "trig",
+            "turr",
+            "cons",
+            "cmbt",
+            "mele",
+            "neut"
+        };
+
+        #endregion
+
+        #region private class DaePolygon
+
+        private class DaePolygon
+        {
+            private readonly Polygon source;
+            private readonly Material material;
+            private readonly int[] pointIndices;
+            private readonly int[] texCoordIndices;
+            private readonly int[] colorIndices;
+
+            public DaePolygon(Polygon source, int[] pointIndices, int[] texCoordIndices, int[] colorIndices)
+            {
+                this.source = source;
+                this.material = source.Material;
+                this.pointIndices = pointIndices;
+                this.texCoordIndices = texCoordIndices;
+                this.colorIndices = colorIndices;
+            }
+
+            public DaePolygon(Material material, int[] pointIndices, int[] texCoordIndices)
+            {
+                this.material = material;
+                this.pointIndices = pointIndices;
+                this.texCoordIndices = texCoordIndices;
+            }
+
+            public Polygon Source => source;
+            public Material Material => material;
+
+            public int[] PointIndices => pointIndices;
+            public int[] TexCoordIndices => texCoordIndices;
+            public int[] ColorIndices => colorIndices;
+        }
+
+        #endregion
+        #region private class DaeMeshBuilder
+
+        private class DaeMeshBuilder
+        {
+            private readonly List<DaePolygon> polygons = new List<DaePolygon>();
+            private readonly List<Vector3> points = new List<Vector3>();
+            private readonly Dictionary<Vector3, int> uniquePoints = new Dictionary<Vector3, int>();
+            private readonly List<Vector2> texCoords = new List<Vector2>();
+            private readonly Dictionary<Vector2, int> uniqueTexCoords = new Dictionary<Vector2, int>();
+            private readonly List<Color> colors = new List<Color>();
+            private readonly Dictionary<Color, int> uniqueColors = new Dictionary<Color, int>();
+            private string name;
+            private Vector3 translation;
+            private Dae.Geometry geometry;
+
+            public DaeMeshBuilder(string name)
+            {
+                this.name = name;
+            }
+
+            public string Name
+            {
+                get { return name; }
+                set { name = value; }
+            }
+
+            public Vector3 Translation => translation;
+
+            public void ResetTransform()
+            {
+                //
+                // Attempt to un-bake the translation of the furniture
+                //
+
+                Vector3 center = BoundingSphere.CreateFromPoints(points).Center;
+
+                BoundingBox bbox = BoundingBox.CreateFromPoints(points);
+                center.Y = bbox.Min.Y;
+
+                translation = center;
+
+                for (int i = 0; i < points.Count; i++)
+                    points[i] -= center;
+            }
+
+            public void AddPolygon(Polygon polygon)
+            {
+                polygons.Add(new DaePolygon(
+                    polygon,
+                    Remap(polygon.Mesh.Points, polygon.PointIndices, points, uniquePoints),
+                    null,
+                    null));
+            }
+
+            public IEnumerable<Polygon> Polygons => from p in polygons
+                                                    where p.Source != null
+                                                    select p.Source;
+
+            private static int[] Remap<T>(IList<T> values, int[] indices, List<T> list, Dictionary<T, int> unique) where T : struct
+            {
+                var result = new int[indices.Length];
+
+                for (int i = 0; i < indices.Length; i++)
+                    result[i] = AddUnique(list, unique, values[indices[i]]);
+
+                return result;
+            }
+
+            private static int[] Remap<T>(IList<T> values, List<T> list, Dictionary<T, int> unique) where T : struct
+            {
+                var result = new int[values.Count];
+
+                for (int i = 0; i < values.Count; i++)
+                    result[i] = AddUnique(list, unique, values[i]);
+
+                return result;
+            }
+
+            private static int AddUnique<T>(List<T> list, Dictionary<T, int> unique, T value) where T : struct
+            {
+                int index;
+
+                if (!unique.TryGetValue(value, out index))
+                {
+                    index = list.Count;
+                    unique.Add(value, index);
+                    list.Add(value);
+                }
+
+                return index;
+            }
+
+            public void Build()
+            {
+                var positionSource = new Dae.Source(points);
+                var primitives = new Dae.MeshPrimitives(Dae.MeshPrimitiveType.Polygons);
+                var posInput = new Dae.IndexedInput(Dae.Semantic.Position, positionSource);
+
+                primitives.Inputs.Add(posInput);
+
+                foreach (var poly in polygons)
+                {
+                    primitives.VertexCounts.Add(poly.PointIndices.Length);
+
+                    posInput.Indices.AddRange(poly.PointIndices);
+                }
+
+                geometry = new Dae.Geometry
+                {
+                    Name = Name + "_geo",
+                    Vertices = { new Dae.Input(Dae.Semantic.Position, positionSource) },
+                    Primitives = { primitives }
+                };
+            }
+
+            public Dae.Geometry Geometry
+            {
+                get
+                {
+                    return geometry;
+                }
+            }
+        }
+
+        #endregion
+        #region private class DaeSceneBuilder
+
+        private class DaeSceneBuilder
+        {
+            private readonly Dae.Scene scene;
+            private readonly Dictionary<string, DaeMeshBuilder> nameMeshBuilder;
+            private readonly List<DaeMeshBuilder> meshBuilders;
+            private readonly Dictionary<Material, Dae.Material> materials;
+            private string imagesFolder = "images";
+
+            public DaeSceneBuilder()
+            {
+                scene = new Dae.Scene();
+                nameMeshBuilder = new Dictionary<string, DaeMeshBuilder>(StringComparer.Ordinal);
+                meshBuilders = new List<DaeMeshBuilder>();
+                materials = new Dictionary<Material, Dae.Material>();
+            }
+
+            public string ImagesFolder
+            {
+                get { return imagesFolder; }
+                set { imagesFolder = value; }
+            }
+
+            public DaeMeshBuilder GetMeshBuilder(string name)
+            {
+                DaeMeshBuilder result;
+
+                if (!nameMeshBuilder.TryGetValue(name, out result))
+                {
+                    result = new DaeMeshBuilder(name);
+                    nameMeshBuilder.Add(name, result);
+                    meshBuilders.Add(result);
+                }
+
+                return result;
+            }
+
+            public IEnumerable<DaeMeshBuilder> MeshBuilders
+            {
+                get { return meshBuilders; }
+            }
+
+            public Dae.Material GetMaterial(Material material)
+            {
+                Dae.Material result;
+
+                if (!materials.TryGetValue(material, out result))
+                {
+                    result = new Dae.Material();
+                    materials.Add(material, result);
+                }
+
+                return result;
+            }
+
+            public void Build()
+            {
+                BuildNodes();
+                BuildMaterials();
+            }
+
+            private void BuildNodes()
+            {
+                foreach (var meshBuilder in meshBuilders)
+                {
+                    meshBuilder.Build();
+
+                    var inst = new Dae.GeometryInstance(meshBuilder.Geometry);
+
+                    var node = new Dae.Node();
+                    node.Name = meshBuilder.Name;
+                    node.Instances.Add(inst);
+
+                    if (meshBuilder.Translation != Vector3.Zero)
+                        node.Transforms.Add(new Dae.TransformTranslate(meshBuilder.Translation));
+
+                    scene.Nodes.Add(node);
+                }
+            }
+
+            private void BuildMaterials()
+            {
+                foreach (KeyValuePair<Material, Dae.Material> pair in materials)
+                {
+                    var material = pair.Key;
+
+                    var image = new Dae.Image
+                    {
+                        FilePath = "./" + GetImageFileName(material).Replace('\\', '/'),
+                        Name = material.Name + "_img"
+                    };
+
+                    var effectSurface = new Dae.EffectSurface(image);
+
+                    var effectSampler = new Dae.EffectSampler(effectSurface)
+                    {
+                        //WrapS = texture.WrapU ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None,
+                        //WrapT = texture.WrapV ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None
+                    };
+
+                    var effectTexture = new Dae.EffectTexture(effectSampler, "diffuse_TEXCOORD");
+
+                    var effect = new Dae.Effect
+                    {
+                        Name = material.Name + "_fx",
+                        AmbientValue = Vector4.One,
+                        SpecularValue = Vector4.Zero,
+                        DiffuseValue = effectTexture,
+                        TransparentValue = material.Image.HasAlpha ? effectTexture : null,
+                        Parameters = {
+                            new Dae.EffectParameter("surface", effectSurface),
+                            new Dae.EffectParameter("sampler", effectSampler)
+                        }
+                    };
+
+                    var daeMaterial = pair.Value;
+                    daeMaterial.Name = material.Name;
+                    daeMaterial.Effect = effect;
+                }
+            }
+
+            private string GetImageFileName(Material material)
+            {
+                if (material.IsMarker)
+                    return Path.Combine("markers", material.Name + ".tga");
+
+                return Path.Combine(imagesFolder, material.Name + ".tga");
+            }
+
+            public void Write(string filePath)
+            {
+                string outputDirPath = Path.GetDirectoryName(filePath);
+
+                foreach (var material in materials.Keys)
+                    TgaWriter.Write(material.Image, Path.Combine(outputDirPath, GetImageFileName(material)));
+
+                Dae.Writer.WriteFile(filePath, scene);
+            }
+        }
+
+        #endregion
+
+        public static void Write(PolygonMesh mesh, string filePath)
+        {
+            var writer = new RoomDaeWriter(mesh);
+            writer.WriteGeometry();
+            writer.world.Write(filePath);
+        }
+
+        private RoomDaeWriter(PolygonMesh source)
+        {
+            this.source = source;
+        }
+
+        private void WriteGeometry()
+        {
+            world = new DaeSceneBuilder();
+
+            for (int i = 0; i < source.Polygons.Count; i++)
+            {
+                var polygon = source.Polygons[i];
+
+                var name = string.Format(CultureInfo.InvariantCulture, "floor_{0}", i);
+                var meshBuilder = world.GetMeshBuilder(name);
+                meshBuilder.AddPolygon(polygon);
+            }
+
+            foreach (var meshBuilder in world.MeshBuilders)
+            {
+                meshBuilder.ResetTransform();
+            }
+
+            world.Build();
+        }
+    }
+}
Index: /OniSplit/Akira/RoomExtractor.cs
===================================================================
--- /OniSplit/Akira/RoomExtractor.cs	(revision 1114)
+++ /OniSplit/Akira/RoomExtractor.cs	(revision 1114)
@@ -0,0 +1,128 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Akira
+{
+    internal class RoomExtractor
+    {
+        private readonly IEnumerable<string> fromFiles;
+        private readonly string outputFilePath;
+
+        private PolygonMesh mesh;
+        private List<Vector3> positions;
+        private Stack<Matrix> nodeTransformStack;
+        private Matrix nodeTransform;
+        private string nodeName;
+
+        public RoomExtractor(IEnumerable<string> fromFiles, string outputFilePath)
+        {
+            this.fromFiles = fromFiles;
+            this.outputFilePath = outputFilePath;
+        }
+
+        public void Extract()
+        {
+            mesh = new PolygonMesh(new MaterialLibrary());
+
+            positions = mesh.Points;
+
+            nodeTransformStack = new Stack<Matrix>();
+            nodeTransform = Matrix.Identity;
+
+            foreach (var filePath in fromFiles)
+                ReadScene(Dae.Reader.ReadFile(filePath));
+
+            var q = new PolygonQuadrangulate(mesh);
+            q.Execute();
+
+            RoomDaeWriter.Write(mesh, outputFilePath);
+        }
+
+        private void ReadScene(Dae.Scene scene)
+        {
+            foreach (var node in scene.Nodes)
+                ReadNode(node);
+        }
+
+        private void ReadNode(Dae.Node node)
+        {
+            nodeTransformStack.Push(nodeTransform);
+
+            foreach (var transform in node.Transforms)
+                nodeTransform = transform.ToMatrix() * nodeTransform;
+
+            nodeName = node.Name;
+
+            foreach (var geometryInstance in node.GeometryInstances)
+                ReadGeometryInstance(geometryInstance);
+
+            foreach (var child in node.Nodes)
+                ReadNode(child);
+
+            nodeTransform = nodeTransformStack.Pop();
+        }
+
+        private void ReadGeometryInstance(Dae.GeometryInstance instance)
+        {
+            foreach (var primitives in instance.Target.Primitives)
+            {
+                if (primitives.PrimitiveType != Dae.MeshPrimitiveType.Polygons)
+                {
+                    Console.Error.WriteLine("Unsupported primitive type '{0}' found in geometry '{1}', ignoring.", primitives.PrimitiveType, instance.Name);
+                    continue;
+                }
+
+                ReadPolygonPrimitives(primitives, instance.Materials.Find(m => m.Symbol == primitives.MaterialSymbol));
+            }
+        }
+
+        private void ReadPolygonPrimitives(Dae.MeshPrimitives primitives, Dae.MaterialInstance materialInstance)
+        {
+            int[] positionIndices = null;
+
+            foreach (var input in primitives.Inputs)
+            {
+                switch (input.Semantic)
+                {
+                    case Dae.Semantic.Position:
+                        positionIndices = ReadInputIndexed(input, positions, Dae.Source.ReadVector3);
+                        break;
+                }
+            }
+
+            foreach (int i in positionIndices)
+                positions[i] = Vector3.Transform(positions[i], ref nodeTransform);
+
+            int startIndex = 0;
+
+            foreach (int vertexCount in primitives.VertexCounts)
+            {
+                var polygonPointIndices = new int[vertexCount];
+                Array.Copy(positionIndices, startIndex, polygonPointIndices, 0, vertexCount);
+
+                var polygon = new Polygon(mesh, polygonPointIndices);
+
+                if (Vector3.Dot(polygon.Plane.Normal, Vector3.UnitY) >= 0.3420201f)
+                    mesh.Polygons.Add(polygon);
+
+                startIndex += vertexCount;
+            }
+        }
+
+        private static int[] ReadInputIndexed<T>(Dae.IndexedInput input, List<T> list, Func<Dae.Source, int, T> elementReader)
+            where T : struct
+        {
+            var indices = new int[input.Indices.Count];
+
+            for (int i = 0; i < input.Indices.Count; i++)
+            {
+                var v = elementReader(input.Source, input.Indices[i]);
+                indices[i] = list.Count;
+                list.Add(v);
+            }
+
+            return indices;
+        }
+    }
+}
Index: /OniSplit/Akira/RoomFlags.cs
===================================================================
--- /OniSplit/Akira/RoomFlags.cs	(revision 1114)
+++ /OniSplit/Akira/RoomFlags.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+
+namespace Oni.Akira
+{
+    [Flags]
+    internal enum RoomFlags
+    {
+        None = 0x00,
+        Stairs = 0x01,
+        Room = 0x04,
+        Simple = 0x10
+    }
+}
Index: /OniSplit/Akira/RoomGrid.cs
===================================================================
--- /OniSplit/Akira/RoomGrid.cs	(revision 1114)
+++ /OniSplit/Akira/RoomGrid.cs	(revision 1114)
@@ -0,0 +1,136 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class RoomGrid
+    {
+        #region Private data
+        private static readonly Color[] gridColors = new[]
+        {
+            new Color(255, 255, 255),
+            new Color(0x90, 0xee, 0x90),
+            new Color(0xad, 0xd8, 0xe6),
+            new Color(0x87, 0xce, 0xfa),
+            new Color(0, 255, 0),
+            new Color(0, 0, 255),
+            new Color(0, 0, 128),
+            new Color(0, 128, 0),
+            new Color(255, 165, 0),
+            new Color(255, 0, 0)
+        };
+
+        private const int origin = -2;
+        private const float tileSize = 4.0f;
+        private readonly int xOrigin = -2;
+        private readonly int xTiles;
+        private readonly int zOrigin = -2;
+        private readonly int zTiles;
+        private readonly byte[] data;
+        private readonly byte[] debugData;
+        #endregion
+
+        public RoomGrid(int xTiles, int zTiles, byte[] data, byte[] debugData)
+        {
+            this.xTiles = xTiles;
+            this.zTiles = zTiles;
+            this.data = data;
+            this.debugData = debugData;
+        }
+
+        public static RoomGrid FromImage(Surface image)
+        {
+            var data = new byte[image.Width * image.Height];
+
+            for (int z = 0; z < image.Height; z++)
+            {
+                for (int x = 0; x < image.Width; x++)
+                {
+                    int type = Array.IndexOf(gridColors, image[x, z]);
+
+                    if (type == -1)
+                        throw new InvalidDataException(string.Format("Color '{0}' does not match a valid tile type", image[x, z]));
+
+                    data[z * image.Width + x] = (byte)type;
+                }
+            }
+
+            return new RoomGrid(image.Width, image.Height, data, null);
+        }
+
+        public static RoomGrid FromCompressedData(int xTiles, int zTiles, byte[] compressedData)
+        {
+            var data = new byte[xTiles * zTiles];
+
+            if (compressedData != null)
+            {
+                int k = 0;
+
+                for (int i = 0; i < compressedData.Length;)
+                {
+                    byte run = compressedData[i++];
+                    byte type = (byte)(run & 0x0f);
+                    byte count = (byte)(run >> 4);
+
+                    if (count == 0)
+                        count = compressedData[i++];
+
+                    for (int j = 0; j < count; j++)
+                        data[k++] = type;
+                }
+            }
+
+            return new RoomGrid(xTiles, zTiles, data, null);
+        }
+
+        public int XTiles => xTiles;
+        public int ZTiles => zTiles;
+        public float TileSize => tileSize;
+        public int XOrigin => xOrigin;
+        public int ZOrigin => zOrigin;
+        public byte[] DebugData => debugData;
+
+        public byte[] Compress()
+        {
+            var compressed = new List<byte>(data.Length);
+
+            for (int i = 0; i < data.Length;)
+            {
+                byte type = data[i];
+                int count = 1;
+
+                while (count < 255 && i + count < data.Length && data[i + count] == type)
+                    count++;
+
+                if (count < 16)
+                {
+                    compressed.Add((byte)((count << 4) | type));
+                }
+                else
+                {
+                    compressed.Add(type);
+                    compressed.Add((byte)count);
+                }
+
+                i += count;
+            }
+
+            return compressed.ToArray();
+        }
+
+        public Surface ToImage()
+        {
+            var image = new Surface(xTiles, zTiles, SurfaceFormat.BGRX);
+
+            for (int z = 0; z < zTiles; z++)
+            {
+                for (int x = 0; x < xTiles; x++)
+                    image[x, z] = gridColors[data[z * xTiles + x]];
+            }
+
+            return image;
+        }
+    }
+}
Index: /OniSplit/Akira/RoomGridBuilder.cs
===================================================================
--- /OniSplit/Akira/RoomGridBuilder.cs	(revision 1114)
+++ /OniSplit/Akira/RoomGridBuilder.cs	(revision 1114)
@@ -0,0 +1,244 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Collections;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class RoomGridBuilder
+    {
+        private readonly Dae.Scene roomsScene;
+        private readonly PolygonMesh geometryMesh;
+        private PolygonMesh roomsMesh;
+        private OctreeNode geometryOcttree;
+        private OctreeNode dangerOcttree;
+
+        public RoomGridBuilder(Dae.Scene roomsScene, PolygonMesh geometryMesh)
+        {
+            this.roomsScene = roomsScene;
+            this.geometryMesh = geometryMesh;
+        }
+
+        public PolygonMesh Mesh => roomsMesh;
+
+        public void Build()
+        {
+            roomsMesh = RoomDaeReader.Read(roomsScene);
+
+            RoomBuilder.BuildRooms(roomsMesh);
+
+            Console.Error.WriteLine("Read {0} rooms", roomsMesh.Rooms.Count);
+
+            geometryOcttree = OctreeBuilder.Build(geometryMesh, GunkFlags.NoCollision | GunkFlags.NoCharacterCollision);
+            dangerOcttree = OctreeBuilder.Build(geometryMesh, p => (p.Flags & GunkFlags.Danger) != 0);
+
+            ProcessStairsCollision();
+
+            Parallel.ForEach(roomsMesh.Rooms, room =>
+            {
+                BuildGrid(room);
+            });
+        }
+
+        private void ProcessStairsCollision()
+        {
+            var verticalTolerance1 = new Vector3(0.0f, 0.1f, 0.0f);
+            var verticalTolerance2 = new Vector3(0.0f, 7.5f, 0.0f);
+
+            foreach (var stairs in geometryMesh.Polygons.Where(p => p.IsStairs && p.VertexCount == 4))
+            {
+                var floorPoints = stairs.Points.Select(v => v + verticalTolerance1).ToArray();
+                var ceilPoints = stairs.Points.Select(v => v + verticalTolerance2).ToArray();
+                var bbox = BoundingBox.CreateFromPoints(floorPoints.Concatenate(ceilPoints));
+
+                var floorPlane = new Plane(floorPoints[0], floorPoints[1], floorPoints[2]);
+                var ceilingPlane = new Plane(ceilPoints[0], ceilPoints[1], ceilPoints[2]);
+
+                foreach (var node in geometryOcttree.FindLeafs(bbox))
+                {
+                    foreach (var poly in node.Polygons)
+                    {
+                        if ((poly.Flags & (GunkFlags.NoCollision | GunkFlags.NoCharacterCollision)) != 0)
+                        {
+                            //
+                            // already a no collision polygon, skip it
+                            //
+
+                            continue;
+                        }
+
+                        if (!poly.BoundingBox.Intersects(bbox))
+                            continue;
+
+                        var points = poly.Points.ToList();
+
+                        points = PolygonUtils.ClipToPlane(points, floorPlane);
+
+                        if (points == null)
+                        {
+                            //
+                            // this polygon is below stairs, skip it
+                            //
+
+                            continue;
+                        }
+
+                        points = PolygonUtils.ClipToPlane(points, ceilingPlane);
+
+                        if (points != null)
+                        {
+                            //
+                            // this polygon is too high above the stairs, skip it
+                            //
+
+                            continue;
+                        }
+
+                        poly.Flags |= GunkFlags.NoCharacterCollision;
+                    }
+                }
+            }
+        }
+
+        private void BuildGrid(Room room)
+        {
+            var floor = room.FloorPolygon;
+            var bbox = room.BoundingBox;
+
+            //
+            // Create an empty grid and mark all tiles as 'danger'
+            //
+
+            var rasterizer = new RoomGridRasterizer(bbox);
+            rasterizer.Clear(RoomGridWeight.Danger);
+
+            //
+            // Collect all polygons that intersect the room
+            // 
+
+            bbox.Inflate(2.0f * new Vector3(rasterizer.TileSize, 0.0f, rasterizer.TileSize));
+
+            var testbox = bbox;
+            testbox.Min.X -= 1.0f;
+            testbox.Min.Y = bbox.Min.Y - 6.0f;
+            testbox.Min.Z -= 1.0f;
+            testbox.Max.X += 1.0f;
+            testbox.Max.Y = bbox.Max.Y - 6.0f;
+            testbox.Max.Z += 1.0f;
+
+            var polygons = new Set<Polygon>();
+            var dangerPolygons = new Set<Polygon>();
+
+            foreach (var node in geometryOcttree.FindLeafs(testbox))
+            {
+                polygons.UnionWith(node.Polygons);
+            }
+
+            foreach (var node in dangerOcttree.FindLeafs(testbox))
+            {
+                dangerPolygons.UnionWith(node.Polygons);
+            }
+
+            //
+            // Draw all the floors on the grid. This will overwrite the 'danger'
+            // areas with 'clear' areas. This means danger area will remain
+            // where there isn't any floor.
+            //
+
+            foreach (var polygon in polygons)
+            {
+                if (polygon.Plane.Normal.Y > 0.5f)
+                    rasterizer.DrawFloor(polygon.Points);
+            }
+
+            if (room.FloorPlane.Normal.Y >= 0.999f)
+            {
+                //
+                // Draw the walls.
+                //
+
+                float floorMaxY = floor.BoundingBox.Max.Y;
+                var floorPlane = new Plane(floor.Plane.Normal, floor.Plane.D - 4.0f);
+                var ceilingPlane = new Plane(-floor.Plane.Normal, -(floor.Plane.D - 20.0f));
+
+                foreach (var polygon in polygons)
+                {
+                    if ((polygon.Flags & (GunkFlags.Stairs | GunkFlags.NoCharacterCollision | GunkFlags.Impassable)) == 0)
+                    {
+                        //
+                        // Remove curbs from character collision.
+                        //
+
+                        var polyBox = polygon.BoundingBox;
+
+                        if (Math.Abs(polygon.Plane.Normal.Y) < MathHelper.Eps
+                            && polyBox.Height <= 4.0f
+                            && Math.Abs(polyBox.Max.Y - floorMaxY) <= 4.0f)
+                        {
+                            polygon.Flags |= GunkFlags.NoCharacterCollision;
+                            continue;
+                        }
+                    }
+
+                    if ((polygon.Flags & (GunkFlags.Stairs | GunkFlags.GridIgnore | GunkFlags.NoCollision | GunkFlags.NoCharacterCollision)) != 0)
+                        continue;
+
+                    var points = polygon.Points.ToList();
+
+                    points = PolygonUtils.ClipToPlane(points, floorPlane);
+
+                    if (points == null)
+                        continue;
+
+                    points = PolygonUtils.ClipToPlane(points, ceilingPlane);
+
+                    if (points == null)
+                        continue;
+
+                    if (Math.Abs(polygon.Plane.Normal.Y) <= 0.1f)
+                        rasterizer.DrawWall(points);
+                    else
+                        rasterizer.DrawImpassable(points);
+                }
+
+                //
+                // Draw ramp entry/exit lines. Hmm, this seems useless.
+                //
+
+                //if (room.FloorPlane.Normal.Y >= 0.98f)
+                //{
+                //    foreach (RoomAdjacency adj in room.Ajacencies)
+                //    {
+                //        if (Math.Abs(room.FloorPlane.Normal.Y - adj.AdjacentRoom.FloorPlane.Normal.Y) > 0.001f)
+                //            rasterizer.DrawStairsEntry(adj.Ghost.Points);
+                //    }
+                //}
+
+                //
+                // Draw 'danger' quads.
+                //
+
+                foreach (var polygon in dangerPolygons)
+                {
+                    rasterizer.DrawDanger(polygon.Points);
+                }
+
+                //
+                // Draw 'near wall' borders.
+                //
+
+                rasterizer.AddBorders();
+            }
+
+            //
+            // Finally, get the rasterized pathfinding grid.
+            //
+
+            room.Grid = rasterizer.GetGrid();
+
+            if (room.Grid.XTiles * room.Grid.ZTiles > 256 * 256)
+                Console.Error.WriteLine("Warning: pathfinding grid too large");
+        }
+    }
+}
Index: /OniSplit/Akira/RoomGridRasterizer.cs
===================================================================
--- /OniSplit/Akira/RoomGridRasterizer.cs	(revision 1114)
+++ /OniSplit/Akira/RoomGridRasterizer.cs	(revision 1114)
@@ -0,0 +1,428 @@
+﻿using System;
+using System.IO;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Akira
+{
+    internal class RoomGridRasterizer
+    {
+        private const int origin = -2;
+        private const float tileSize = 4.0f;
+        private const int margin = 3;
+        private readonly int xTiles;
+        private readonly int zTiles;
+        private readonly byte[] data;
+        private readonly Vector3 worldOrigin;
+        //private readonly List<RoomGridDebugData> debugData;
+
+        private enum RoomGridDebugType : byte
+        {
+            None,
+            SlopedQuad,
+            StairQuad,
+            Wall,
+            DangerQuad,
+            ImpassableQuad,
+            Floor
+        }
+
+        //private class RoomGridDebugData
+        //{
+        //    public int quadIndex;
+        //    public RoomGridDebugType type;
+        //    public RoomGridWeight weight;
+        //    public short x, y;
+        //}
+
+        public RoomGridRasterizer(BoundingBox bbox)
+        {
+            this.xTiles = (int)(((bbox.Max.X - bbox.Min.X) / tileSize) + (-origin * 2) + 1) + margin * 2;
+            this.zTiles = (int)(((bbox.Max.Z - bbox.Min.Z) / tileSize) + (-origin * 2) + 1) + margin * 2;
+            this.data = new byte[xTiles * zTiles];
+            this.worldOrigin = bbox.Min;
+            //this.debugData = new List<RoomGridDebugData>();
+        }
+
+        public void Clear(RoomGridWeight weight)
+        {
+            for (int i = 0; i < xTiles * zTiles; i++)
+                data[i] = (byte)weight;
+        }
+
+        public int XTiles => xTiles;
+        public int ZTiles => ZTiles;
+
+        public float TileSize => tileSize;
+
+        public RoomGridWeight this[int x, int z]
+        {
+            get { return (RoomGridWeight)data[x + z * xTiles]; }
+            set { data[x + z * xTiles] = (byte)value; }
+        }
+
+        public void DrawFloor(IEnumerable<Vector3> points)
+        {
+            foreach (var point in ScanPolygon(points.Select(v => WorldToGrid(v)).ToList()))
+                this[point.X, point.Y] = RoomGridWeight.Clear;
+        }
+
+        public void DrawDanger(IEnumerable<Vector3> points)
+        {
+            foreach (var point in ScanPolygon(points.Select(v => WorldToGrid(v)).ToList()))
+                this[point.X, point.Y] = RoomGridWeight.Danger;
+        }
+
+        public void DrawStairsEntry(IEnumerable<Vector3> points)
+        {
+            var v0 = points.First();
+            var v1 = v0;
+
+            foreach (var v in points)
+            {
+                if (v.X < v0.X || (v.X == v0.X && v.Z < v0.Z))
+                    v0 = v;
+
+                if (v.X > v1.X || (v.X == v1.X && v.Z > v1.Z))
+                    v1 = v;
+            }
+
+            Point p0 = WorldToGrid(v0);
+            Point p1 = WorldToGrid(v1);
+
+            DrawLine(p0, p1, RoomGridWeight.Stairs);
+
+            DrawLine(p0 - Point.UnitY, p1 - Point.UnitY, RoomGridWeight.Clear);
+            DrawLine(p0 + Point.UnitY, p1 + Point.UnitY, RoomGridWeight.Clear);
+            DrawLine(p0 + Point.UnitX, p1 + Point.UnitX, RoomGridWeight.Clear);
+            DrawLine(p0 - Point.UnitX, p1 - Point.UnitX, RoomGridWeight.Clear);
+
+            var pp = points.ToArray();
+            Array.Sort(pp, (x, y) => x.Y.CompareTo(y.Y));
+            DrawImpassable(pp[0]);
+            DrawImpassable(pp[1]);
+        }
+
+        public void DrawWall(IEnumerable<Vector3> points)
+        {
+            var v0 = points.First();
+            var v1 = v0;
+
+            foreach (var v in points)
+            {
+                if (v.X < v0.X || (v.X == v0.X && v.Z < v0.Z))
+                    v0 = v;
+
+                if (v.X > v1.X || (v.X == v1.X && v.Z > v1.Z))
+                    v1 = v;
+            }
+
+            Point p0 = WorldToGrid(v0);
+            Point p1 = WorldToGrid(v1);
+
+            DrawLine(p0, p1, RoomGridWeight.Impassable);
+
+            DrawLine(p0 - Point.UnitY, p1 - Point.UnitY, RoomGridWeight.SemiPassable);
+            DrawLine(p0 + Point.UnitY, p1 + Point.UnitY, RoomGridWeight.SemiPassable);
+            DrawLine(p0 + Point.UnitX, p1 + Point.UnitX, RoomGridWeight.SemiPassable);
+            DrawLine(p0 - Point.UnitX, p1 - Point.UnitX, RoomGridWeight.SemiPassable);
+        }
+
+        private void DrawLine(Point p0, Point p1, RoomGridWeight weight)
+        {
+            foreach (Point p in ScanLine(p0, p1))
+            {
+                if (weight > this[p.X, p.Y])
+                {
+                    this[p.X, p.Y] = weight;
+
+                    //debugData.Add(new RoomGridDebugData {
+                    //    x = (short)p.X,
+                    //    y = (short)p.Y,
+                    //    weight = weight
+                    //});
+                }
+            }
+        }
+
+        private void FillPolygon(IEnumerable<Vector3> points, RoomGridWeight weight)
+        {
+            foreach (var point in ScanPolygon(points.Select(v => WorldToGrid(v)).ToList()))
+            {
+                if (weight > this[point.X, point.Y])
+                    this[point.X, point.Y] = weight;
+            }
+        }
+
+        public void DrawImpassable(IEnumerable<Vector3> points)
+        {
+            FillPolygon(points, RoomGridWeight.Impassable);
+        }
+
+        public void DrawImpassable(Vector3 position)
+        {
+            var point = WorldToGrid(position);
+            int x = point.X;
+            int y = point.Y;
+
+            DrawTile(x, y, RoomGridWeight.Impassable);
+
+            DrawTile(x - 1, y, RoomGridWeight.SemiPassable);
+            DrawTile(x + 1, y, RoomGridWeight.SemiPassable);
+            DrawTile(x, y - 1, RoomGridWeight.SemiPassable);
+            DrawTile(x, y + 1, RoomGridWeight.SemiPassable);
+            DrawTile(x - 1, y - 1, RoomGridWeight.SemiPassable);
+            DrawTile(x + 1, y - 1, RoomGridWeight.SemiPassable);
+            DrawTile(x + 1, y + 1, RoomGridWeight.SemiPassable);
+            DrawTile(x - 1, y + 1, RoomGridWeight.SemiPassable);
+        }
+
+        private void DrawTile(int x, int y, RoomGridWeight weight)
+        {
+            if (0 <= x && x < xTiles && 0 <= y && y < zTiles)
+            {
+                if (weight > this[x, y])
+                    this[x, y] = weight;
+            }
+        }
+
+        public void AddBorders()
+        {
+            AddBorder(RoomGridWeight.Danger, RoomGridWeight.Clear, RoomGridWeight.Border4);
+            AddBorder(RoomGridWeight.Border4, RoomGridWeight.Clear, RoomGridWeight.Border3);
+            AddBorder(RoomGridWeight.Border3, RoomGridWeight.Clear, RoomGridWeight.Border2);
+            AddBorder(RoomGridWeight.Border2, RoomGridWeight.Clear, RoomGridWeight.Border1);
+            AddBorder(RoomGridWeight.SemiPassable, RoomGridWeight.Clear, RoomGridWeight.NearWall);
+        }
+
+        private void AddBorder(RoomGridWeight aroundOf, RoomGridWeight onlyIf, RoomGridWeight border)
+        {
+            for (int z = 0; z < zTiles; z++)
+            {
+                for (int x = 0; x < xTiles; x++)
+                {
+                    if (this[x, z] != aroundOf)
+                        continue;
+
+                    if (x - 1 >= 0 && this[x - 1, z] == onlyIf)
+                        this[x - 1, z] = border;
+
+                    if (x + 1 < xTiles && this[x + 1, z] == onlyIf)
+                        this[x + 1, z] = border;
+
+                    if (z - 1 >= 0 && this[x, z - 1] == onlyIf)
+                        this[x, z - 1] = border;
+
+                    if (z + 1 < zTiles && this[x, z + 1] == onlyIf)
+                        this[x, z + 1] = border;
+                }
+            }
+        }
+
+        private Point WorldToGrid(Vector3 world)
+        {
+            return new Point(
+                FMath.TruncateToInt32((world.X - worldOrigin.X) / tileSize) - origin + margin,
+                FMath.TruncateToInt32((world.Z - worldOrigin.Z) / tileSize) - origin + margin);
+        }
+
+        public RoomGrid GetGrid()
+        {
+            int gridXTiles = xTiles - 2 * margin;
+            int gridZTiles = zTiles - 2 * margin;
+
+            var gridData = new byte[gridXTiles * gridZTiles];
+
+            for (int z = margin; z < zTiles - margin; z++)
+            {
+                for (int x = margin; x < xTiles - margin; x++)
+                    gridData[(x - margin) + (z - margin) * gridXTiles] = data[x + z * xTiles];
+            }
+
+            //var debugStream = new MemoryStream(debugData.Count * 16);
+
+            //using (var writer = new BinaryWriter(debugStream))
+            //{
+            //    foreach (var data in debugData)
+            //    {
+            //        writer.WriteByte((byte)data.type);
+            //        writer.WriteByte(0);
+            //        writer.Write(data.x);
+            //        writer.Write(data.y);
+            //        writer.WriteInt16(0);
+            //        writer.Write(data.quadIndex);
+            //        writer.Write((int)data.weight);
+            //    }
+            //}
+
+            return new RoomGrid(gridXTiles, gridZTiles, gridData, null);
+        }
+
+        private IEnumerable<Point> ScanLine(Point p0, Point p1)
+        {
+            return ScanLine(p0.X, p0.Y, p1.X, p1.Y);
+        }
+
+        private IEnumerable<Point> ScanLine(int x0, int y0, int x1, int y1)
+        {
+            int dx = (x0 < x1) ? x1 - x0 : x0 - x1;
+            int dy = (y0 < y1) ? y1 - y0 : y0 - y1;
+            int sx = (x0 < x1) ? 1 : -1;
+            int sy = (y0 < y1) ? 1 : -1;
+            int err = dx - dy;
+
+            while (true)
+            {
+                if (0 <= x0 && x0 < xTiles && 0 <= y0 && y0 < zTiles)
+                    yield return new Point(x0, y0);
+
+                if (x0 == x1 && y0 == y1)
+                    break;
+
+                int derr = 2 * err;
+
+                if (derr > -dy)
+                {
+                    err = err - dy;
+                    x0 = x0 + sx;
+                }
+
+                if (derr < dx)
+                {
+                    err = err + dx;
+                    y0 = y0 + sy;
+                }
+            }
+        }
+
+        private IEnumerable<Vector2> ScanLine(Vector2 p0, Vector2 p1)
+        {
+            return ScanLine(p0.X, p0.Y, p1.X, p1.Y);
+        }
+
+        private IEnumerable<Vector2> ScanLine(float x0, float y0, float x1, float y1)
+        {
+            float dx = (x0 < x1) ? x1 - x0 : x0 - x1;
+            float dy = (y0 < y1) ? y1 - y0 : y0 - y1;
+            float sx = (x0 < x1) ? 1 : -1;
+            float sy = (y0 < y1) ? 1 : -1;
+            float err = dx - dy;
+
+            while (true)
+            {
+                if (0 <= x0 && x0 < xTiles && 0 <= y0 && y0 < zTiles)
+                    yield return new Vector2(x0, y0);
+
+                if (x0 == x1 && y0 == y1)
+                    break;
+
+                float derr = 2 * err;
+
+                if (derr > -dy)
+                {
+                    err = err - dy;
+                    x0 = x0 + sx;
+                }
+
+                if (derr < dx)
+                {
+                    err = err + dx;
+                    y0 = y0 + sy;
+                }
+            }
+        }
+
+        private IEnumerable<Point> ScanPolygon(IList<Point> points)
+        {
+            var activeEdgeList = new List<Edge>();
+            var activeEdgeTable = new List<List<Edge>>();
+            int minY = BuildActiveEdgeTable(points, activeEdgeTable);
+
+            for (int y = 0; y < activeEdgeTable.Count; y++)
+            {
+                for (int i = 0; i < activeEdgeTable[y].Count; i++)
+                    activeEdgeList.Add(activeEdgeTable[y][i]);
+
+                for (int i = 0; i < activeEdgeList.Count; i++)
+                {
+                    if (activeEdgeList[i].maxY <= y + minY)
+                    {
+                        activeEdgeList.RemoveAt(i);
+                        i--;
+                    }
+                }
+
+                activeEdgeList.Sort((a, b) => (a.currentX == b.currentX ? a.slopeRecip.CompareTo(b.slopeRecip) : a.currentX.CompareTo(b.currentX)));
+
+                for (int i = 0; i < activeEdgeList.Count; i += 2)
+                {
+                    int yLine = minY + y;
+
+                    if (0 <= yLine && yLine < zTiles)
+                    {
+                        int xStart = Math.Max(0, (int)Math.Ceiling(activeEdgeList[i].currentX));
+                        int xEnd = Math.Min(xTiles - 1, (int)activeEdgeList[i + 1].currentX);
+
+                        for (int x = xStart; x <= xEnd; x++)
+                            yield return new Point(x, yLine);
+                    }
+                }
+
+                for (int i = 0; i < activeEdgeList.Count; i++)
+                    activeEdgeList[i].Next();
+            }
+        }
+
+        private class Edge
+        {
+            public float maxY;
+            public float currentX;
+            public float slopeRecip;
+
+            public Edge(Point current, Point next)
+            {
+                maxY = Math.Max(current.Y, next.Y);
+                slopeRecip = (current.X - next.X) / (float)(current.Y - next.Y);
+
+                if (current.Y == maxY)
+                    currentX = next.X;
+                else
+                    currentX = current.X;
+            }
+
+            public void Next()
+            {
+                currentX += slopeRecip;
+            }
+        }
+
+        private static int BuildActiveEdgeTable(IList<Point> points, List<List<Edge>> activeEdgeTable)
+        {
+            activeEdgeTable.Clear();
+
+            int minY = points.Min(p => p.Y);
+            int maxY = points.Max(p => p.Y);
+
+            for (int i = minY; i <= maxY; i++)
+                activeEdgeTable.Add(new List<Edge>());
+
+            for (int i = 0; i < points.Count; i++)
+            {
+                Point current = points[i];
+                Point next = points[(i + 1) % points.Count];
+
+                if (current.Y == next.Y)
+                    continue;
+
+                var e = new Edge(current, next);
+
+                if (current.Y == e.maxY)
+                    activeEdgeTable[next.Y - minY].Add(e);
+                else
+                    activeEdgeTable[current.Y - minY].Add(e);
+            }
+
+            return minY;
+        }
+    }
+}
Index: /OniSplit/Akira/RoomGridWeight.cs
===================================================================
--- /OniSplit/Akira/RoomGridWeight.cs	(revision 1114)
+++ /OniSplit/Akira/RoomGridWeight.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿using System;
+
+namespace Oni.Akira
+{
+    internal enum RoomGridWeight : byte
+    {
+        Clear,
+        NearWall,
+        Border1,
+        Border2,
+        SemiPassable,
+        Border3,
+        Border4,
+        Stairs,
+        Danger,
+        Impassable
+    }
+}
Index: /OniSplit/BinImporter.cs
===================================================================
--- /OniSplit/BinImporter.cs	(revision 1114)
+++ /OniSplit/BinImporter.cs	(revision 1114)
@@ -0,0 +1,37 @@
+﻿using System;
+using System.IO;
+using Oni.Metadata;
+
+namespace Oni
+{
+    internal class BinImporter : Importer
+    {
+        public override void Import(string filePath, string outputDirPath)
+        {
+            using (var reader = new BinaryReader(filePath))
+            {
+                int tag = reader.ReadInt32();
+
+                if (!Enum.IsDefined(typeof(BinaryTag), tag))
+                    throw new NotSupportedException(string.Format(".bin file with tag '{0:x}' is unuspported", tag));
+
+                var tagName = ((BinaryTag)tag).ToString();
+
+                BeginImport();
+
+                var bina = CreateInstance(TemplateTag.BINA, tagName + Path.GetFileNameWithoutExtension(filePath));
+
+                using (var writer = bina.OpenWrite())
+                {
+                    writer.Write(reader.Length);
+                    writer.Write(32);
+                }
+
+                RawWriter.Write(tag);
+                RawWriter.Write(reader.ReadBytes(reader.Length - 4));
+
+                Write(outputDirPath);
+            }
+        }
+    }
+}
Index: /OniSplit/BinaryReader.cs
===================================================================
--- /OniSplit/BinaryReader.cs	(revision 1114)
+++ /OniSplit/BinaryReader.cs	(revision 1114)
@@ -0,0 +1,438 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Oni.Imaging;
+
+namespace Oni
+{
+    internal sealed class BinaryReader : IDisposable
+    {
+        #region Private data
+        private static readonly byte[] seekBuffer = new byte[512];
+        private static readonly Encoding encoding = Encoding.UTF8;
+        private const float rotationAngleScale = MathHelper.Pi / 32767.5f;
+
+        private FileStream stream;
+        private byte[] buffer;
+        private bool bigEndian;
+        private InstanceFile instanceFile;
+        #endregion
+
+        public BinaryReader(string filePath)
+        {
+            this.buffer = new byte[8];
+            this.stream = File.OpenRead(filePath);
+        }
+
+        public BinaryReader(string filePath, bool bigEndian)
+            : this(filePath)
+        {
+            this.bigEndian = bigEndian;
+        }
+
+        public BinaryReader(string filePath, InstanceFile instanceFile)
+            : this(filePath)
+        {
+            this.instanceFile = instanceFile;
+        }
+
+        public void Dispose()
+        {
+            if (stream != null)
+                stream.Dispose();
+
+            stream = null;
+            buffer = null;
+        }
+
+        public string Name => stream.Name;
+
+        public int Length => (int)stream.Length;
+
+        public int Position
+        {
+            get
+            {
+                return (int)stream.Position;
+            }
+            set
+            {
+                int currentPosition = (int)stream.Position;
+                int delta = value - currentPosition;
+
+                if (delta == 0)
+                    return;
+
+                if (delta > 0 && delta <= seekBuffer.Length)
+                    stream.Read(seekBuffer, 0, delta);
+                else
+                    stream.Position = value;
+            }
+        }
+
+        public void Skip(int length)
+        {
+            Position += length;
+        }
+
+        public void SkipCString()
+        {
+            for (int b = 1; b != 0 && b != -1; b = stream.ReadByte())
+                ;
+        }
+
+        public int Read(byte[] buffer, int offset, int length)
+        {
+            return stream.Read(buffer, offset, length);
+        }
+
+        public byte[] ReadBytes(int length)
+        {
+            var buffer = new byte[length];
+            int offset = 0;
+
+            while (length > 0)
+            {
+                int read = stream.Read(buffer, offset, length);
+
+                if (read == 0)
+                    break;
+
+                offset += read;
+                length -= read;
+            }
+
+            if (offset != buffer.Length)
+            {
+                var result = new byte[offset];
+                Buffer.BlockCopy(buffer, 0, result, 0, offset);
+                buffer = result;
+            }
+
+            return buffer;
+        }
+
+        public byte ReadByte()
+        {
+            int value = stream.ReadByte();
+
+            if (value == -1)
+                throw new EndOfStreamException();
+
+            return (byte)value;
+        }
+
+        public bool ReadBoolean()
+        {
+            return (ReadByte() != 0);
+        }
+
+        public ushort ReadUInt16()
+        {
+            FillBuffer(2);
+
+            if (bigEndian)
+            {
+                return (ushort)(buffer[1] | buffer[0] << 8);
+            }
+
+            return (ushort)(buffer[0] | buffer[1] << 8);
+        }
+
+        public ushort[] ReadUInt16Array(int length)
+        {
+            var array = new ushort[length];
+
+            for (int i = 0; i < array.Length; i++)
+                array[i] = ReadUInt16();
+
+            return array;
+        }
+
+        public uint ReadUInt32()
+        {
+            FillBuffer(4);
+
+            if (bigEndian)
+            {
+                return (uint)(buffer[3] | buffer[2] << 8 | buffer[1] << 16 | buffer[0] << 24);
+            }
+
+            return (uint)(buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24);
+        }
+
+        public ulong ReadUInt64()
+        {
+            FillBuffer(8);
+
+            ulong lo, hi;
+
+            if (bigEndian)
+            {
+                hi = (uint)(buffer[3] | buffer[2] << 8 | buffer[1] << 16 | buffer[0] << 24);
+                lo = (uint)(buffer[7] | buffer[6] << 8 | buffer[5] << 16 | buffer[4] << 24);
+            }
+            else
+            {
+                lo = (uint)(buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24);
+                hi = (uint)(buffer[4] | buffer[5] << 8 | buffer[6] << 16 | buffer[7] << 24);
+            }
+
+            return (hi << 32) | lo;
+        }
+
+        public short ReadInt16()
+        {
+            return (short)ReadUInt16();
+        }
+
+        public short[] ReadInt16Array(int length)
+        {
+            var array = new short[length];
+
+            for (int i = 0; i < array.Length; i++)
+                array[i] = ReadInt16();
+
+            return array;
+        }
+
+        public int ReadInt32()
+        {
+            return (int)ReadUInt32();
+        }
+
+        public int[] ReadInt32VarArray()
+        {
+            return ReadInt32Array(ReadInt32());
+        }
+
+        public int[] ReadInt32Array(int length)
+        {
+            var array = new int[length];
+
+            for (int i = 0; i < array.Length; i++)
+                array[i] = ReadInt32();
+
+            return array;
+        }
+
+        public long ReadInt64()
+        {
+            return (long)ReadUInt64();
+        }
+
+        public unsafe float ReadSingle()
+        {
+            uint value = ReadUInt32();
+            return *((float*)&value);
+        }
+
+        public float[] ReadSingleArray(int length)
+        {
+            var data = new float[length];
+
+            for (int i = 0; i < data.Length; i++)
+                data[i] = ReadSingle();
+
+            return data;
+        }
+
+        public unsafe double ReadDouble()
+        {
+            ulong value = ReadUInt64();
+            return *((double*)&value);
+        }
+
+        public Vector2 ReadVector2()
+        {
+            return new Vector2(ReadSingle(), ReadSingle());
+        }
+
+        public Vector2[] ReadVector2VarArray()
+        {
+            return ReadVector2Array(ReadInt32());
+        }
+
+        public Vector2[] ReadVector2Array(int length)
+        {
+            var data = new Vector2[length];
+
+            for (int i = 0; i < data.Length; i++)
+                data[i] = ReadVector2();
+
+            return data;
+        }
+
+        public Vector3 ReadVector3()
+        {
+            return new Vector3(ReadSingle(), ReadSingle(), ReadSingle());
+        }
+
+        public Vector3[] ReadVector3VarArray()
+        {
+            return ReadVector3Array(ReadInt32());
+        }
+
+        public Vector3[] ReadVector3Array(int length)
+        {
+            var data = new Vector3[length];
+
+            for (int i = 0; i < data.Length; i++)
+                data[i] = ReadVector3();
+
+            return data;
+        }
+
+        public Plane ReadPlane()
+        {
+            return new Plane(ReadVector3(), ReadSingle());
+        }
+
+        public Plane[] ReadPlaneVarArray()
+        {
+            return ReadPlaneArray(ReadInt32());
+        }
+
+        public Plane[] ReadPlaneArray(int length)
+        {
+            var data = new Plane[length];
+
+            for (int i = 0; i < data.Length; i++)
+                data[i] = ReadPlane();
+
+            return data;
+        }
+
+        public Quaternion ReadQuaternion()
+        {
+            return new Quaternion(ReadSingle(), ReadSingle(), ReadSingle(), -ReadSingle());
+        }
+
+        public Quaternion ReadCompressedQuaternion()
+        {
+            return Quaternion.CreateFromAxisAngle(Vector3.UnitX, ReadInt16() * rotationAngleScale)
+                * Quaternion.CreateFromAxisAngle(Vector3.UnitY, ReadInt16() * rotationAngleScale)
+                * Quaternion.CreateFromAxisAngle(Vector3.UnitZ, ReadInt16() * rotationAngleScale);
+        }
+
+        public BoundingBox ReadBoundingBox()
+        {
+            return new BoundingBox(ReadVector3(), ReadVector3());
+        }
+
+        public Matrix ReadMatrix4x3()
+        {
+            Matrix m;
+            m.M11 = ReadSingle();
+            m.M12 = ReadSingle();
+            m.M13 = ReadSingle();
+            m.M14 = 0.0f;
+            m.M21 = ReadSingle();
+            m.M22 = ReadSingle();
+            m.M23 = ReadSingle();
+            m.M24 = 0.0f;
+            m.M31 = ReadSingle();
+            m.M32 = ReadSingle();
+            m.M33 = ReadSingle();
+            m.M34 = 0.0f;
+            m.M41 = ReadSingle();
+            m.M42 = ReadSingle();
+            m.M43 = ReadSingle();
+            m.M44 = 1.0f;
+            return m;
+        }
+
+        public Color ReadColor()
+        {
+            uint color = ReadUInt32();
+
+            var r = (byte)((color >> 16) & 0xff);
+            var g = (byte)((color >> 08) & 0xff);
+            var b = (byte)((color >> 00) & 0xff);
+            var a = (byte)((color >> 24) & 0xff);
+
+            return new Color(r, g, b, a);
+        }
+
+        public Color[] ReadColorArray(int length)
+        {
+            var data = new Color[length];
+
+            for (int i = 0; i < data.Length; i++)
+                data[i] = ReadColor();
+
+            return data;
+        }
+
+        public string ReadString(int maxLength)
+        {
+            var bytes = ReadBytes(maxLength);
+
+            for (int i = 0; i < bytes.Length; i++)
+            {
+                if (bytes[i] == 0)
+                    return encoding.GetString(bytes, 0, i);
+            }
+
+            return encoding.GetString(bytes);
+        }
+
+        public string ReadCString()
+        {
+            var buffer = new List<byte>(64);
+            byte b;
+
+            while ((b = ReadByte()) != 0)
+                buffer.Add(b);
+
+            return encoding.GetString(buffer.ToArray());
+        }
+
+        public InstanceDescriptor ReadInstance()
+        {
+            return instanceFile.ResolveLink(ReadInt32());
+        }
+
+        public InstanceDescriptor[] ReadInstanceArray(int length)
+        {
+            var data = new InstanceDescriptor[length];
+
+            for (int i = 0; i < data.Length; i++)
+                data[i] = ReadInstance();
+
+            return data;
+        }
+
+        public InstanceDescriptor ReadLink()
+        {
+            return instanceFile.GetDescriptor(ReadInt32());
+        }
+
+        public InstanceDescriptor[] ReadLinkArray(int length)
+        {
+            var data = new InstanceDescriptor[length];
+
+            for (int i = 0; i < data.Length; i++)
+                data[i] = ReadLink();
+
+            return data;
+        }
+
+        private void FillBuffer(int count)
+        {
+            int offset = 0;
+
+            while (count > 0)
+            {
+                int read = stream.Read(buffer, offset, count);
+
+                if (read == 0)
+                    throw new EndOfStreamException();
+
+                offset += read;
+                count -= read;
+            }
+        }
+    }
+}
Index: /OniSplit/BinaryWriter.cs
===================================================================
--- /OniSplit/BinaryWriter.cs	(revision 1114)
+++ /OniSplit/BinaryWriter.cs	(revision 1114)
@@ -0,0 +1,297 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Oni.Imaging;
+
+namespace Oni
+{
+    internal class BinaryWriter : System.IO.BinaryWriter
+    {
+        private static readonly byte[] padding = new byte[32];
+        private static readonly Encoding encoding = Encoding.UTF8;
+        private readonly Stack<int> positionStack = new Stack<int>();
+
+        public BinaryWriter(Stream stream)
+            : base(stream, encoding)
+        {
+        }
+
+        public void WriteInstanceId(int index)
+        {
+            Write(InstanceFileWriter.MakeInstanceId(index));
+        }
+
+        public void Write(IEnumerable<ImporterDescriptor> descriptors)
+        {
+            foreach (var descriptor in descriptors)
+                Write(descriptor);
+        }
+
+        public void Write(ImporterDescriptor descriptor)
+        {
+            if (descriptor == null)
+                Write(0);
+            else
+                Write(InstanceFileWriter.MakeInstanceId(descriptor.Index));
+        }
+
+        public void Write(Color c)
+        {
+            Write(c.ToBgra32());
+        }
+
+        public void Write(Vector2 v)
+        {
+            Write(v.X);
+            Write(v.Y);
+        }
+
+        public void Write(Vector3 v)
+        {
+            Write(v.X);
+            Write(v.Y);
+            Write(v.Z);
+        }
+
+        public void Write(Vector4 v)
+        {
+            Write(v.X);
+            Write(v.Y);
+            Write(v.Z);
+            Write(v.W);
+        }
+
+        public void Write(Quaternion q)
+        {
+            Write(q.X);
+            Write(q.Y);
+            Write(q.Z);
+            Write(-q.W);
+        }
+
+        public void Write(Plane p)
+        {
+            Write(p.Normal);
+            Write(p.D);
+        }
+
+        public void Write(BoundingBox bbox)
+        {
+            Write(bbox.Min);
+            Write(bbox.Max);
+        }
+
+        public void Write(BoundingSphere bsphere)
+        {
+            Write(bsphere.Center);
+            Write(bsphere.Radius);
+        }
+
+        public void WriteMatrix4x3(Matrix m)
+        {
+            Write(m.M11);
+            Write(m.M12);
+            Write(m.M13);
+            Write(m.M21);
+            Write(m.M22);
+            Write(m.M23);
+            Write(m.M31);
+            Write(m.M32);
+            Write(m.M33);
+            Write(m.M41);
+            Write(m.M42);
+            Write(m.M43);
+        }
+
+        public void Write(short[] a)
+        {
+            foreach (short v in a)
+                Write(v);
+        }
+
+        public void Write(ushort[] a)
+        {
+            foreach (ushort v in a)
+                Write(v);
+        }
+
+        public void Write(int[] a)
+        {
+            foreach (int v in a)
+                Write(v);
+        }
+
+        public void Write(int[] v, int startIndex, int length)
+        {
+            for (int i = startIndex; i < startIndex + length; i++)
+                Write(v[i]);
+        }
+
+        public void Write(IEnumerable<float> a)
+        {
+            foreach (float v in a)
+                Write(v);
+        }
+
+        public void Write(IEnumerable<int> a)
+        {
+            foreach (int i in a)
+                Write(i);
+        }
+
+        public void Write(Color[] a)
+        {
+            foreach (Color v in a)
+                Write(v);
+        }
+
+        public void Write(IEnumerable<Vector2> a)
+        {
+            foreach (Vector2 v in a)
+                Write(v);
+        }
+
+        public void Write(IEnumerable<Vector3> a)
+        {
+            foreach (Vector3 v in a)
+                Write(v);
+        }
+
+        public void Write(IEnumerable<Plane> a)
+        {
+            foreach (Plane v in a)
+                Write(v);
+        }
+
+        public void Write(string s, int maxLength)
+        {
+            if (s == null)
+            {
+                Skip(maxLength);
+                return;
+            }
+
+            if (encoding.GetByteCount(s) > maxLength)
+                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "The string '{0}' is too long (max length is {1})", s, maxLength));
+
+            byte[] data = new byte[maxLength];
+            encoding.GetBytes(s, 0, s.Length, data, 0);
+            Write(data);
+        }
+
+        public void WriteByte(int value)
+        {
+            if (value < byte.MinValue || byte.MaxValue < value)
+                throw new ArgumentOutOfRangeException("Value too large for Byte", "value");
+
+            Write((byte)value);
+        }
+
+        public void WriteInt16(int value)
+        {
+            if (value < short.MinValue || short.MaxValue < value)
+                throw new ArgumentOutOfRangeException("Value too large for Int16", "value");
+
+            Write((short)value);
+        }
+
+        public void WriteUInt16(int value)
+        {
+            if (value < 0 || value > UInt16.MaxValue)
+                throw new ArgumentOutOfRangeException("Value too large for UInt16", "value");
+
+            Write((ushort)value);
+        }
+
+        public void Write(byte value, int count)
+        {
+            if (value == 0 && Position == OutStream.Length)
+            {
+                Seek(count, SeekOrigin.Current);
+            }
+            else
+            {
+                for (int i = 0; i < count; i++)
+                    Write(value);
+            }
+        }
+
+        public void Skip(int length)
+        {
+            Position += length;
+        }
+
+        public override Stream BaseStream
+        {
+            get
+            {
+                //
+                // Note: return base OutStream directly instead of BaseStream to avoid
+                // flushing the stream.
+                //
+
+                return base.OutStream;
+            }
+        }
+
+        public void PushPosition(int newPosition)
+        {
+            positionStack.Push(Position);
+            Position = newPosition;
+        }
+
+        public void PopPosition()
+        {
+            Position = positionStack.Pop();
+        }
+
+        public int Position
+        {
+            get
+            {
+                return (int)BaseStream.Position;
+            }
+            set
+            {
+                int currentPosition = (int)OutStream.Position;
+                int delta = value - currentPosition;
+
+                if (delta == 0)
+                    return;
+
+                //
+                // Prevent changing the output stream position for small changes of position.
+                // This avoids flushing the write cache of the stream which results in poor perf.
+                //
+
+                if (0 < delta && delta <= 32 && Position == OutStream.Length)
+                    OutStream.Write(padding, 0, delta);
+                else
+                    OutStream.Position = value;
+            }
+        }
+
+        public void WriteAt(int position, int value)
+        {
+            PushPosition(position);
+            Write(value);
+            PopPosition();
+        }
+
+        public void WriteAt(int position, short value)
+        {
+            PushPosition(position);
+            Write(value);
+            PopPosition();
+        }
+
+        public int Align32()
+        {
+            int position = Utils.Align32(Position);
+            Position = position;
+            return position;
+        }
+    }
+}
Index: /OniSplit/Collections/Set.cs
===================================================================
--- /OniSplit/Collections/Set.cs	(revision 1114)
+++ /OniSplit/Collections/Set.cs	(revision 1114)
@@ -0,0 +1,47 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Collections
+{
+    internal class Set<T> : IEnumerable<T>
+    {
+        private readonly Dictionary<T, int> set;
+
+        public Set()
+        {
+            set = new Dictionary<T, int>();
+        }
+
+        public Set(IEqualityComparer<T> comparer)
+        {
+            set = new Dictionary<T, int>(comparer);
+        }
+
+        public bool Add(T t)
+        {
+            if (set.ContainsKey(t))
+                return false;
+
+            set.Add(t, 0);
+            return true;
+        }
+
+        public bool Contains(T t) => set.ContainsKey(t);
+
+        public int Count => set.Count;
+
+        public void UnionWith(IEnumerable<T> with)
+        {
+            foreach (T t in with)
+                set[t] = 0;
+        }
+
+        public IEnumerator<T> GetEnumerator()
+        {
+            foreach (var pair in set)
+                yield return pair.Key;
+        }
+
+        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
+    }
+}
Index: /OniSplit/Dae/Axis.cs
===================================================================
--- /OniSplit/Dae/Axis.cs	(revision 1114)
+++ /OniSplit/Dae/Axis.cs	(revision 1114)
@@ -0,0 +1,9 @@
+﻿namespace Oni.Dae
+{
+	internal enum Axis
+	{
+		X,
+		Y,
+		Z
+	}
+}
Index: /OniSplit/Dae/Camera.cs
===================================================================
--- /OniSplit/Dae/Camera.cs	(revision 1114)
+++ /OniSplit/Dae/Camera.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Dae
+{
+    internal class Camera : Entity
+    {
+        public CameraType Type { get; set; }
+        public float XMag { get; set; }
+        public float YMag { get; set; }
+        public float XFov { get; set; }
+        public float YFov { get; set; }
+        public float AspectRatio { get; set; }
+        public float ZNear { get; set; }
+        public float ZFar { get; set; }
+    }
+}
Index: /OniSplit/Dae/CameraInstance.cs
===================================================================
--- /OniSplit/Dae/CameraInstance.cs	(revision 1114)
+++ /OniSplit/Dae/CameraInstance.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Dae
+{
+    internal class CameraInstance : Instance<Camera>
+    {
+        public CameraInstance()
+        {
+        }
+
+        public CameraInstance(Camera camera) 
+            : base(camera)
+        {
+        }
+    }
+}
Index: /OniSplit/Dae/CameraType.cs
===================================================================
--- /OniSplit/Dae/CameraType.cs	(revision 1114)
+++ /OniSplit/Dae/CameraType.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Dae
+{
+    internal enum CameraType
+    {
+        Perspective,
+        Orthographic
+    }
+}
Index: /OniSplit/Dae/Converters/AxisConverter.cs
===================================================================
--- /OniSplit/Dae/Converters/AxisConverter.cs	(revision 1114)
+++ /OniSplit/Dae/Converters/AxisConverter.cs	(revision 1114)
@@ -0,0 +1,215 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Collections;
+
+namespace Oni.Dae
+{
+    internal class AxisConverter
+    {
+        private Scene scene;
+        private Axis fromUpAxis;
+        private Axis toUpAxis;
+        private Set<float[]> convertedValues;
+
+        public static void Convert(Scene scene, Axis fromUpAxis, Axis toUpAxis)
+        {
+            var converter = new AxisConverter {
+                scene = scene,
+                fromUpAxis = fromUpAxis,
+                toUpAxis = toUpAxis,
+                convertedValues = new Set<float[]>()
+            };
+
+            converter.Convert();
+        }
+
+        private void Convert()
+        {
+            Convert(scene);
+        }
+
+        private void Convert(Node node)
+        {
+            foreach (var transform in node.Transforms)
+                Convert(transform);
+
+            foreach (var instance in node.Instances)
+                Convert(instance);
+
+            foreach (var child in node.Nodes)
+                Convert(child);
+        }
+
+        private void Convert(Instance instance)
+        {
+            var geometryInstance = instance as GeometryInstance;
+
+            if (geometryInstance != null)
+            {
+                Convert(geometryInstance.Target);
+                return;
+            }
+        }
+
+        private void Convert(Geometry geometry)
+        {
+            foreach (var primitives in geometry.Primitives)
+            {
+                //
+                // HACK: this assumes that position and normal sources are never reused with other semantic
+                //
+
+                foreach (var input in primitives.Inputs)
+                {
+                    if (input.Semantic == Semantic.Position || input.Semantic == Semantic.Normal)
+                        ConvertPosition(input.Source.FloatData, input.Source.Stride);
+                }
+            }
+        }
+
+        private void Convert(Transform transform)
+        {
+            var scale = transform as TransformScale;
+
+            if (scale != null)
+            {
+                ConvertScale(scale.Values, 3);
+
+                if (transform.HasAnimations)
+                    ConvertScaleAnimation(transform);
+
+                return;
+            }
+
+            var rotate = transform as TransformRotate;
+
+            if (rotate != null)
+            {
+                //ConvertPosition(rotate.Values, 3);
+
+                if (transform.HasAnimations)
+                    ConvertRotationAnimation(transform);
+
+                return;
+            }
+
+            var translate = transform as TransformTranslate;
+
+            if (translate != null)
+            {
+                ConvertPosition(translate.Values, 3);
+
+                if (transform.HasAnimations)
+                    ConvertPositionAnimation(transform);
+
+                return;
+            }
+
+            var matrix = transform as TransformMatrix;
+
+            if (matrix != null)
+            {
+                ConvertMatrix(matrix);
+
+                //
+                // TODO: matrix animation???
+                //
+
+                return;
+            }
+        }
+
+        private void ConvertMatrix(TransformMatrix transform)
+        {
+            if (fromUpAxis == Axis.Z && toUpAxis == Axis.Y)
+            {
+                Matrix zm = transform.Matrix;
+                Matrix ym = zm;
+                //Matrix ym = Matrix.CreateRotationX(
+                //    MathHelper.PiOver2) *
+                //zm *
+                //Matrix.CreateRotationX(
+                //    -MathHelper.PiOver2);
+                ym.M12 = zm.M13;
+                ym.M13 = -zm.M12;
+                ym.M21 = zm.M31;
+                ym.M22 = zm.M33;
+                ym.M23 = -zm.M32;
+                ym.M31 = -zm.M21;
+                ym.M32 = -zm.M23;
+                ym.M33 = zm.M22;
+                ym.M42 = zm.M43;
+                ym.M43 = -zm.M42;
+                transform.Matrix = ym;
+            }
+            //else if (fromUpAxis == Axis.Y && toUpAxis == Axis.Z)
+            //{
+            //    rotate.XAxis = new Vector3(1.0f, 0.0f, 0.0f);
+            //    rotate.YAxis = new Vector3(0.0f, 0.0f, -1.0f);
+            //    rotate.ZAxis = new Vector3(0.0f, 1.0f, 0.0f);
+            //}
+            //else if (fromUpAxis == Axis.X && toUpAxis == Axis.Y)
+            //{
+            //    rotate.XAxis = new Vector3(0.0f, 0.0f, -1.0f);
+            //    rotate.YAxis = new Vector3(1.0f, 0.0f, 1.0f);
+            //    rotate.ZAxis = new Vector3(0.0f, 1.0f, 0.0f);
+            //}
+        }
+
+        private void ConvertPosition(float[] values, int stride)
+        {
+            if (!convertedValues.Add(values))
+                return;
+
+            for (int i = 0; i + stride - 1 < values.Length; i += stride)
+                Convert(values, i, f => -f);
+        }
+
+        private void ConvertPositionAnimation(Transform transform)
+        {
+            Convert(transform.Animations, 0, s => s != null ? s.Scale(-1.0f) : null);
+        }
+
+        private void ConvertRotationAnimation(Transform transform)
+        {
+            ConvertPositionAnimation(transform);
+        }
+
+        private void ConvertScale(float[] values, int stride)
+        {
+            for (int i = 0; i + stride - 1 < values.Length; i += stride)
+                Convert(values, i, null);
+        }
+
+        private void ConvertScaleAnimation(Transform transform)
+        {
+            Convert(transform.Animations, 0, null);
+        }
+
+        private void Convert<T>(IList<T> list, int baseIndex, Func<T, T> negate)
+        {
+            T t0 = list[baseIndex + 0];
+            T t1 = list[baseIndex + 1];
+            T t2 = list[baseIndex + 2];
+
+            if (fromUpAxis == Axis.Z && toUpAxis == Axis.Y)
+            {
+                list[baseIndex + 0] = t0;
+                list[baseIndex + 1] = t2;
+                list[baseIndex + 2] = negate != null ? negate(t1) : t1;
+            }
+            else if (fromUpAxis == Axis.Y && toUpAxis == Axis.Z)
+            {
+                list[baseIndex + 0] = t0;
+                list[baseIndex + 1] = negate != null ? negate(t2) : t2;
+                list[baseIndex + 2] = t1;
+            }
+            else if (fromUpAxis == Axis.X && toUpAxis == Axis.Y)
+            {
+                list[baseIndex + 0] = negate != null ? negate(t2) : t2;
+                list[baseIndex + 1] = t0;
+                list[baseIndex + 2] = t1;
+            }
+        }
+    }
+}
Index: /OniSplit/Dae/Converters/FaceConverter.cs
===================================================================
--- /OniSplit/Dae/Converters/FaceConverter.cs	(revision 1114)
+++ /OniSplit/Dae/Converters/FaceConverter.cs	(revision 1114)
@@ -0,0 +1,169 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Collections;
+
+namespace Oni.Dae
+{
+    internal class FaceConverter
+    {
+        private Node root;
+        private int maxEdges = 3;
+
+        public static void Triangulate(Node root)
+        {
+            var converter = new FaceConverter {
+                root = root
+            };
+
+            converter.Convert();
+        }
+
+        private void Convert()
+        {
+            ConvertNode(root);
+        }
+
+        private void ConvertNode(Node node)
+        {
+            foreach (var instance in node.Instances)
+                ConvertInstance(instance);
+
+            foreach (var child in node.Nodes)
+                ConvertNode(child);
+        }
+
+        private void ConvertInstance(Instance instance)
+        {
+            var geometryInstance = instance as GeometryInstance;
+
+            if (geometryInstance != null)
+            {
+                ConvertGeometry(geometryInstance.Target);
+                return;
+            }
+        }
+
+        private void ConvertGeometry(Geometry geometry)
+        {
+            foreach (var primitives in geometry.Primitives)
+            {
+                if (primitives.PrimitiveType != MeshPrimitiveType.Polygons)
+                    continue;
+
+                if (primitives.VertexCounts.All(c => c == 3))
+                    continue;
+
+                ConvertPolygons(geometry, primitives);
+            }
+        }
+
+        private void ConvertPolygons(Geometry geometry, MeshPrimitives primitives)
+        {
+            var positionInput = primitives.Inputs.FirstOrDefault(i => i.Semantic == Semantic.Position);
+
+            if (positionInput == null)
+            {
+                Console.Error.WriteLine("{0}: cannot find position input", geometry.Name);
+                return;
+            }
+
+            var newFaces = new List<int>(primitives.VertexCounts.Count * 2);
+            var newVertexCounts = new List<int>(primitives.VertexCounts.Count * 2);
+            int voffset = 0;
+
+            foreach (int vcount in primitives.VertexCounts)
+            {
+                if (vcount < 3)
+                {
+                    Console.Error.WriteLine("{0}: skipping bad face (line)", geometry.Name);
+                }
+                else if (vcount <= maxEdges)
+                {
+                    for (int i = 0; i < vcount; i++)
+                        newFaces.Add(voffset + i);
+
+                    newVertexCounts.Add(vcount);
+                }
+                else
+                {
+                    ConvertPolygon(geometry, positionInput, voffset, vcount, newFaces, newVertexCounts);
+                }
+
+                voffset += vcount;
+            }
+
+            primitives.VertexCounts.Clear();
+            primitives.VertexCounts.AddRange(newVertexCounts);
+
+            var oldIndices = new int[primitives.Inputs.Count][];
+
+            for (int i = 0; i < primitives.Inputs.Count; i++)
+            {
+                var input = primitives.Inputs[i];
+                oldIndices[i] = input.Indices.ToArray();
+                input.Indices.Clear();
+            }
+
+            for (int i = 0; i < primitives.Inputs.Count; i++)
+            {
+                var ni = primitives.Inputs[i].Indices;
+                var oi = oldIndices[i];
+
+                foreach (int v in newFaces)
+                    ni.Add(oi[v]);
+            }
+        }
+
+        private void ConvertPolygon(Geometry geometry, IndexedInput input, int offset, int vcount, List<int> newFaces, List<int> newVertexCounts)
+        {
+            var points = new Vector3[vcount];
+
+            for (int i = 0; i < vcount; i++)
+                points[i] = Dae.Source.ReadVector3(input.Source, input.Indices[offset + i]);
+
+            int concave = -1;
+
+            for (int i = 0; i < vcount; i++)
+            {
+                Vector3 p0 = points[i];
+                Vector3 p1 = points[(i + 1) % vcount];
+
+                if (Vector3.Dot(p0, p1) < 0.0f)
+                {
+                    concave = i;
+                    break;
+                }
+            }
+
+            if (concave == -1)
+            {
+                for (int i = 0; i < vcount - 2; i++)
+                {
+                    newFaces.Add(offset + 0);
+                    newFaces.Add(offset + 1 + i);
+                    newFaces.Add(offset + 2 + i);
+                    newVertexCounts.Add(3);
+                }
+
+                return;
+            }
+
+            if (vcount == 4)
+            {
+                newFaces.Add(offset + concave);
+                newFaces.Add(offset + (concave + 1) % vcount);
+                newFaces.Add(offset + (concave + 2) % vcount);
+                newVertexCounts.Add(3);
+
+                newFaces.Add(offset + (concave + vcount - 1) % vcount);
+                newFaces.Add(offset + (concave + vcount - 2) % vcount);
+                newFaces.Add(offset + concave);
+                newVertexCounts.Add(3);
+
+                return;
+            }
+
+            Console.Error.WriteLine("{0}: skipping bad face (concave {1}-gon)", geometry.Name, vcount);
+        }
+    }
+}
Index: /OniSplit/Dae/Converters/UnitConverter.cs
===================================================================
--- /OniSplit/Dae/Converters/UnitConverter.cs	(revision 1114)
+++ /OniSplit/Dae/Converters/UnitConverter.cs	(revision 1114)
@@ -0,0 +1,113 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Collections;
+
+namespace Oni.Dae
+{
+    internal class UnitConverter
+    {
+        private Scene scene;
+        private float scale;
+        private Set<float[]> scaledValues;
+
+        public static void Convert(Scene scene, float scale)
+        {
+            var converter = new UnitConverter {
+                scene = scene,
+                scale = scale,
+                scaledValues = new Set<float[]>()
+            };
+
+            converter.Convert();
+        }
+
+        private void Convert()
+        {
+            Convert(scene);
+        }
+
+        private void Convert(Node node)
+        {
+            foreach (var transform in node.Transforms)
+                Convert(transform);
+
+            foreach (var instance in node.Instances)
+                Convert(instance);
+
+            foreach (var child in node.Nodes)
+                Convert(child);
+        }
+
+        private void Convert(Instance instance)
+        {
+            var geometryInstance = instance as GeometryInstance;
+
+            if (geometryInstance != null)
+            {
+                Convert(geometryInstance.Target);
+                return;
+            }
+        }
+
+        private void Convert(Geometry geometry)
+        {
+            foreach (var primitives in geometry.Primitives)
+            {
+                //
+                // TODO: this assumes that position sources are not reused.
+                //
+
+                foreach (var input in primitives.Inputs)
+                {
+                    if (input.Semantic == Semantic.Position)
+                        Scale(input.Source.FloatData, input.Source.Stride);
+                }
+            }
+        }
+
+        private void Convert(Transform transform)
+        {
+            var translate = transform as TransformTranslate;
+
+            if (translate != null)
+            {
+                Scale(translate.Values, 3);
+
+                if (translate.HasAnimations)
+                {
+                    for (int i = 0; i < translate.Animations.Length; i++)
+                    {
+                        Sampler s = translate.Animations[i];
+                        translate.Animations[i] = s == null ? null : s.Scale(scale);
+                    }
+                }
+
+                return;
+            }
+
+            var matrix = transform as TransformMatrix;
+
+            if (matrix != null)
+            {
+                matrix.Values[3] *= scale;
+                matrix.Values[7] *= scale;
+                matrix.Values[11] *= scale;
+
+                return;
+            }
+        }
+
+        private void Scale(float[] values, int stride)
+        {
+            if (!scaledValues.Add(values))
+                return;
+
+            for (int i = 0; i + stride - 1 < values.Length; i += stride)
+            {
+                values[i + 0] *= scale;
+                values[i + 1] *= scale;
+                values[i + 2] *= scale;
+            }
+        }
+    }
+}
Index: /OniSplit/Dae/Effect.cs
===================================================================
--- /OniSplit/Dae/Effect.cs	(revision 1114)
+++ /OniSplit/Dae/Effect.cs	(revision 1114)
@@ -0,0 +1,100 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class Effect : Entity
+    {
+        private readonly List<EffectParameter> parameters;
+        private readonly EffectParameter emission;
+        private readonly EffectParameter ambient;
+        private readonly EffectParameter diffuse;
+        private readonly EffectParameter specular;
+        private readonly EffectParameter shininess;
+        private readonly EffectParameter reflective;
+        private readonly EffectParameter reflectivity;
+        private readonly EffectParameter transparent;
+        private readonly EffectParameter transparency;
+        private readonly EffectParameter indexOfRefraction;
+
+        public Effect()
+        {
+            parameters = new List<EffectParameter>();
+
+            var alphaOne = new Vector4(0.0f, 0.0f, 0.0f, 1.0f);
+
+            emission = new EffectParameter("emission", alphaOne, this);
+            ambient = new EffectParameter("ambient", alphaOne, this);
+            diffuse = new EffectParameter("diffuse", alphaOne, this);
+            specular = new EffectParameter("specular", alphaOne, this);
+            shininess = new EffectParameter("shininess", 20.0f, this);
+            reflective = new EffectParameter("reflective", 1.0f, this);
+            reflectivity = new EffectParameter("reflectivity", Vector4.One, this);
+            transparent = new EffectParameter("transparent", alphaOne, this);
+            transparency = new EffectParameter("transparency", 1.0f, this);
+            indexOfRefraction = new EffectParameter("index_of_refraction", 1.0f, this);
+        }
+
+        public EffectType Type { get; set; }
+
+        public List<EffectParameter> Parameters => parameters;
+
+        public IEnumerable<EffectTexture> Textures
+        {
+            get
+            {
+                foreach (var param in new[] { diffuse, ambient, specular, reflective, transparent, emission })
+                {
+                    var texture = param.Value as EffectTexture;
+
+                    if (texture != null)
+                        yield return texture;
+                }
+            }
+        }
+
+        public EffectParameter Emission => emission;
+
+        public EffectParameter Ambient => ambient;
+
+        public object AmbientValue
+        {
+            get { return ambient.Value; }
+            set { ambient.Value = value; }
+        }
+
+        public EffectParameter Diffuse => diffuse;
+
+        public object DiffuseValue
+        {
+            get { return diffuse.Value; }
+            set { diffuse.Value = value; }
+        }
+
+        public EffectParameter Specular => specular;
+
+        public object SpecularValue
+        {
+            get { return specular.Value; }
+            set { specular.Value = value; }
+        }
+
+        public EffectParameter Shininess => shininess;
+
+        public EffectParameter Reflective => reflective;
+
+        public EffectParameter Reflectivity => reflectivity;
+
+        public EffectParameter Transparent => transparent;
+
+        public object TransparentValue
+        {
+            get { return transparent.Value; }
+            set { transparent.Value = value; }
+        }
+
+        public EffectParameter Transparency => transparency;
+
+        public EffectParameter IndexOfRefraction => indexOfRefraction;
+    }
+}
Index: /OniSplit/Dae/EffectParameter.cs
===================================================================
--- /OniSplit/Dae/EffectParameter.cs	(revision 1114)
+++ /OniSplit/Dae/EffectParameter.cs	(revision 1114)
@@ -0,0 +1,83 @@
+﻿using System;
+
+namespace Oni.Dae
+{
+	internal class EffectParameter
+	{
+		private object value;
+		private string reference;
+
+		public EffectParameter()
+		{
+		}
+
+		public EffectParameter(string sid, object value)
+		{
+			Sid = sid;
+			this.value = value;
+			SetValueOwner(this);
+		}
+
+		public EffectParameter(string sid, object value, Effect parent)
+		{
+			Sid = sid;
+			this.value = value;
+		}
+
+		public string Sid { get; set; }
+		public string Semantic { get; set; }
+
+		public object Value
+		{
+			get
+			{
+				return value;
+			}
+			set
+			{
+				SetValueOwner(null);
+
+				this.value = value;
+
+				if (value != null)
+					reference = null;
+
+				SetValueOwner(this);
+			}
+		}
+
+		private void SetValueOwner(EffectParameter owner)
+		{
+			EffectSampler sampler = value as EffectSampler;
+
+			if (sampler != null)
+			{
+				sampler.Owner = owner;
+				return;
+			}
+
+			EffectSurface surface = value as EffectSurface;
+
+			if (surface != null)
+			{
+				surface.DeclaringParameter = owner;
+				return;
+			}
+		}
+
+		public string Reference
+		{
+			get
+			{
+				return reference;
+			}
+			set
+			{
+				reference = value;
+
+				if (reference != null)
+					this.value = null;
+			}
+		}
+	}
+}
Index: /OniSplit/Dae/EffectSampler.cs
===================================================================
--- /OniSplit/Dae/EffectSampler.cs	(revision 1114)
+++ /OniSplit/Dae/EffectSampler.cs	(revision 1114)
@@ -0,0 +1,22 @@
+﻿namespace Oni.Dae
+{
+    internal class EffectSampler
+    {
+        public EffectSampler()
+        {
+        }
+
+        public EffectSampler(EffectSurface surface)
+        {
+            Surface = surface;
+        }
+
+        public EffectParameter Owner { get; set; }
+        public EffectSurface Surface { get; set; }
+        public EffectSamplerWrap WrapS { get; set; } = EffectSamplerWrap.Wrap;
+        public EffectSamplerWrap WrapT { get; set; } = EffectSamplerWrap.Wrap;
+        public EffectSamplerFilter MinFilter { get; set; }
+        public EffectSamplerFilter MagFilter { get; set; }
+        public EffectSamplerFilter MipFilter { get; set; }
+    }
+}
Index: /OniSplit/Dae/EffectSamplerFilter.cs
===================================================================
--- /OniSplit/Dae/EffectSamplerFilter.cs	(revision 1114)
+++ /OniSplit/Dae/EffectSamplerFilter.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Dae
+{
+    internal enum EffectSamplerFilter
+    {
+        None,
+        Nearest,
+        Linear,
+        NearestMipmapNearest,
+        LinearMipmapNearest,
+        NearestMipmapLinear,
+        LinearMipmapLinear
+    }
+}
Index: /OniSplit/Dae/EffectSamplerWrap.cs
===================================================================
--- /OniSplit/Dae/EffectSamplerWrap.cs	(revision 1114)
+++ /OniSplit/Dae/EffectSamplerWrap.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Dae
+{
+	internal enum EffectSamplerWrap
+	{
+		None,
+		Wrap,
+		Mirror,
+		Clamp,
+		Border
+	}
+}
Index: /OniSplit/Dae/EffectSurface.cs
===================================================================
--- /OniSplit/Dae/EffectSurface.cs	(revision 1114)
+++ /OniSplit/Dae/EffectSurface.cs	(revision 1114)
@@ -0,0 +1,17 @@
+﻿namespace Oni.Dae
+{
+    internal class EffectSurface
+    {
+        public EffectSurface()
+        {
+        }
+
+        public EffectSurface(Image initFrom)
+        {
+            InitFrom = initFrom;
+        }
+
+        public EffectParameter DeclaringParameter { get; set; }
+        public Image InitFrom { get; set; }
+    }
+}
Index: /OniSplit/Dae/EffectTexture.cs
===================================================================
--- /OniSplit/Dae/EffectTexture.cs	(revision 1114)
+++ /OniSplit/Dae/EffectTexture.cs	(revision 1114)
@@ -0,0 +1,19 @@
+﻿namespace Oni.Dae
+{
+    internal class EffectTexture
+    {
+        public EffectTexture()
+        {
+        }
+
+        public EffectTexture(EffectSampler sampler, string texCoordSemantic)
+        {
+            TexCoordSemantic = texCoordSemantic;
+            Sampler = sampler;
+        }
+
+        public string TexCoordSemantic { get; set; }
+        public EffectTextureChannel Channel { get; set; }
+        public EffectSampler Sampler { get; set; }
+    }
+}
Index: /OniSplit/Dae/EffectTextureChannel.cs
===================================================================
--- /OniSplit/Dae/EffectTextureChannel.cs	(revision 1114)
+++ /OniSplit/Dae/EffectTextureChannel.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Dae
+{
+	internal enum EffectTextureChannel
+	{
+		None,
+		Emission,
+		Ambient,
+		Diffuse,
+		Specular,
+		Reflective,
+		Transparent
+	}
+}
Index: /OniSplit/Dae/EffectType.cs
===================================================================
--- /OniSplit/Dae/EffectType.cs	(revision 1114)
+++ /OniSplit/Dae/EffectType.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿namespace Oni.Dae
+{
+	internal enum EffectType
+	{
+		Constant,
+		Lambert,
+		Phong,
+		Blinn
+	}
+}
Index: /OniSplit/Dae/Entity.cs
===================================================================
--- /OniSplit/Dae/Entity.cs	(revision 1114)
+++ /OniSplit/Dae/Entity.cs	(revision 1114)
@@ -0,0 +1,9 @@
+﻿namespace Oni.Dae
+{
+    internal abstract class Entity
+    {
+        public string Id { get; set; }
+        public string Name { get; set; }
+        public string FileName { get; set; }
+    }
+}
Index: /OniSplit/Dae/Geometry.cs
===================================================================
--- /OniSplit/Dae/Geometry.cs	(revision 1114)
+++ /OniSplit/Dae/Geometry.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class Geometry : Entity
+    {
+        public readonly List<MeshPrimitives> primitives = new List<MeshPrimitives>(1);
+        public readonly List<Input> vertices = new List<Input>(1);
+
+        public List<Input> Vertices => vertices;
+        public List<MeshPrimitives> Primitives => primitives;
+    }
+}
Index: /OniSplit/Dae/GeometryInstance.cs
===================================================================
--- /OniSplit/Dae/GeometryInstance.cs	(revision 1114)
+++ /OniSplit/Dae/GeometryInstance.cs	(revision 1114)
@@ -0,0 +1,21 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class GeometryInstance : Instance<Geometry>
+    {
+        private readonly List<MaterialInstance> materials = new List<MaterialInstance>(1);
+
+        public GeometryInstance()
+        {
+        }
+
+        public GeometryInstance(Geometry geometry)
+            : base(geometry)
+        {
+        }
+
+        public List<MaterialInstance> Materials => materials;
+    }
+}
Index: /OniSplit/Dae/IO/DaeReader.cs
===================================================================
--- /OniSplit/Dae/IO/DaeReader.cs	(revision 1114)
+++ /OniSplit/Dae/IO/DaeReader.cs	(revision 1114)
@@ -0,0 +1,1894 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Globalization;
+using System.Xml;
+using Oni.Xml;
+
+namespace Oni.Dae.IO
+{
+    internal class DaeReader
+    {
+        public static string[] CommandLineArgs;
+
+        #region Private data
+        private static readonly string[] emptyStrings = new string[0];
+        private static readonly char[] whiteSpaceChars = new char[] { ' ', '\t' };
+        private static readonly Func<string, int> intConverter = XmlConvert.ToInt32;
+        private static readonly Func<string, float> floatConverter = XmlConvert.ToSingle;
+        private TextWriter error;
+        private TextWriter info;
+        private Scene mainScene;
+        private Dictionary<string, Entity> entities;
+        private XmlReader xml;
+        private Axis upAxis = Axis.Y;
+        private float unit = 1.0f;
+        private List<Action> delayedBindActions;
+        private Uri baseUrl;
+        private string fileName;
+        private List<Scene> scenes = new List<Scene>();
+        private List<Light> lights = new List<Light>();
+        private List<Animation> animations = new List<Animation>();
+        private List<Geometry> geometries = new List<Geometry>();
+        private List<Effect> effects = new List<Effect>();
+        private List<Material> materials = new List<Material>();
+        private List<Image> images = new List<Image>();
+        private List<Camera> cameras = new List<Camera>();
+        #endregion
+
+        #region private class Animation
+
+        private class Animation : Entity
+        {
+            private List<Animation> animations;
+            private readonly List<Sampler> samplers = new List<Sampler>();
+
+            public List<Animation> Animations
+            {
+                get
+                {
+                    if (animations == null)
+                        animations = new List<Animation>();
+
+                    return animations;
+                }
+            }
+
+            public List<Sampler> Samplers => samplers;
+        }
+
+        #endregion
+
+        public static Scene ReadFile(string filePath)
+        {
+            var reader = new DaeReader {
+                baseUrl = new Uri("file://" + Path.GetDirectoryName(filePath).Replace('\\', '/').TrimEnd('/') + "/"),
+                fileName = Path.GetFileName(filePath),
+                delayedBindActions = new List<Action>(),
+                error = Console.Error,
+                info = Console.Out
+            };
+
+            var settings = new XmlReaderSettings {
+                IgnoreWhitespace = true,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
+
+            using (reader.xml = XmlReader.Create(filePath, settings))
+                reader.ReadRoot();
+
+            return reader.mainScene;
+        }
+
+        private void ReadRoot()
+        {
+            while (xml.NodeType != XmlNodeType.Element)
+                xml.Read();
+
+            if (xml.LocalName != "COLLADA")
+                throw new InvalidDataException(string.Format("Unknown root element {0} found", xml.LocalName));
+
+            string version = xml.GetAttribute("version");
+
+            if (version != "1.4.0" && version != "1.4.1")
+                throw new NotSupportedException(string.Format("Unsupported Collada file version {0}", version));
+
+            if (!xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+
+                ReadAsset();
+                ReadContent();
+                ReadExtra();
+            }
+
+            foreach (var action in delayedBindActions)
+                action();
+
+            if (mainScene == null && scenes.Count > 0)
+                mainScene = scenes[0];
+
+            BindNodes(mainScene);
+
+            if (upAxis != Axis.Y)
+                AxisConverter.Convert(mainScene, upAxis, Axis.Y);
+
+            float scale = 1.0f;
+
+            if (CommandLineArgs != null)
+            {
+                string scaleArg = Array.Find(CommandLineArgs, x => x.StartsWith("-dae-scale:", StringComparison.Ordinal));
+
+                if (scaleArg != null)
+                    scale = float.Parse(scaleArg.Substring(11), CultureInfo.InvariantCulture);
+            }
+
+            if (unit != 0.1f || scale != 1.0f)
+                UnitConverter.Convert(mainScene, 10.0f * unit * scale);
+        }
+
+        private void ReadContent()
+        {
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "library_cameras":
+                        ReadLibrary(cameras, "camera", ReadCamera);
+                        break;
+
+                    case "library_images":
+                        ReadLibrary(images, "image", ReadImage);
+                        break;
+
+                    case "library_effects":
+                        ReadLibrary(effects, "effect", ReadEffect);
+                        break;
+
+                    case "library_materials":
+                        ReadLibrary(materials, "material", ReadMaterial);
+                        break;
+
+                    case "library_geometries":
+                        ReadLibrary(geometries, "geometry", ReadGeometry);
+                        break;
+
+                    case "library_nodes":
+                        ReadLibrary(scenes, "node", ReadNode);
+                        break;
+
+                    case "library_visual_scenes":
+                        ReadLibrary(scenes, "visual_scene", ReadScene);
+                        break;
+
+                    case "library_animations":
+                        ReadLibrary(animations, "animation", ReadAnimation);
+                        break;
+
+                    case "library_lights":
+                        ReadLibrary(lights, "light", ReadLight);
+                        break;
+
+                    case "scene":
+                        ReadScene();
+                        break;
+
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+        }
+
+        //
+        // Libraries
+        //
+
+        private void ReadLibrary<T>(ICollection<T> library, string elementName, Action<T> entityReader)
+            where T : Entity, new()
+        {
+            if (xml.SkipEmpty())
+                return;
+
+            xml.ReadStartElement();
+            ReadAsset();
+
+            while (xml.IsStartElement(elementName))
+                ReadEntity<T>(library, entityReader);
+
+            ReadExtra();
+            xml.ReadEndElement();
+        }
+
+        private void ReadEntity<T>(ICollection<T> entityCollection, Action<T> entityReader)
+            where T : Entity, new()
+        {
+            T entity = ReadEntity<T>(entityReader);
+            entityCollection.Add(entity);
+        }
+
+        private T ReadEntity<T>(Action<T> entityReader)
+            where T : Entity, new()
+        {
+            string id = xml.GetAttribute("id");
+
+            var entity = new T {
+                Name = xml.GetAttribute("name"),
+                FileName = fileName
+            };
+
+            AddEntity(id, entity);
+
+            if (string.IsNullOrEmpty(entity.Name))
+                entity.Name = id;
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+                return entity;
+            }
+
+            xml.ReadStartElement();
+            ReadAsset();
+
+            entityReader(entity);
+
+            ReadExtra();
+            xml.ReadEndElement();
+
+            return entity;
+        }
+
+        //
+        // Cameras
+        //
+
+        private void ReadCamera(Camera camera)
+        {
+            ReadAsset();
+
+            xml.ReadStartElement("optics");
+            xml.ReadStartElement("technique_common");
+
+            if (xml.IsStartElement("perspective"))
+                ReadCameraParameters(camera, CameraType.Perspective);
+            else if (xml.IsStartElement("orthographic"))
+                ReadCameraParameters(camera, CameraType.Orthographic);
+            else if (xml.IsStartElement())
+                xml.Skip();
+
+            xml.ReadEndElement();
+
+            while (xml.IsStartElement())
+                xml.Skip();
+
+            xml.ReadEndElement();
+
+            if (xml.IsStartElement("imager"))
+                xml.Skip();
+
+            ReadExtra();
+        }
+
+        private void ReadCameraParameters(Camera camera, CameraType type)
+        {
+            xml.ReadStartElement();
+
+            camera.Type = type;
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "xfov":
+                        camera.XFov = xml.ReadElementContentAsFloat();
+                        break;
+                    case "yfov":
+                        camera.YFov = xml.ReadElementContentAsFloat();
+                        break;
+                    case "xmag":
+                        camera.XMag = xml.ReadElementContentAsFloat();
+                        break;
+                    case "ymag":
+                        camera.YMag = xml.ReadElementContentAsFloat();
+                        break;
+                    case "aspect_ratio":
+                        camera.AspectRatio = xml.ReadElementContentAsFloat();
+                        break;
+                    case "znear":
+                        camera.ZNear = xml.ReadElementContentAsFloat();
+                        break;
+                    case "zfar":
+                        camera.ZFar = xml.ReadElementContentAsFloat();
+                        break;
+
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+        }
+
+        //
+        // Materials
+        //
+
+        private void ReadImage(Image image)
+        {
+            //image.Width = ReadNullableIntAttribute("width");
+            //image.Height = ReadNullableIntAttribute("height");
+            //image.Depth = ReadNullableIntAttribute("depth");
+
+            ReadAsset();
+
+            if (xml.IsStartElement("init_from"))
+            {
+                string filePath = xml.ReadElementContentAsString();
+
+                if (!string.IsNullOrEmpty(filePath))
+                {
+                    var imageUri = new Uri(baseUrl, filePath);
+                    image.FilePath = imageUri.LocalPath;
+                }
+            }
+            else if (xml.IsStartElement("data"))
+            {
+                throw new NotSupportedException("Embedded image data is not supported");
+            }
+            else
+            {
+                throw new InvalidDataException();
+            }
+
+            ReadExtra();
+        }
+
+        private void ReadEffect(Effect effect)
+        {
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "image":
+                        ReadEntity(images, ReadImage);
+                        break;
+                    case "newparam":
+                        ReadEffectParameterDecl(effect);
+                        break;
+                    case "profile_COMMON":
+                        ReadEffectProfileCommon(effect);
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            ReadExtra();
+        }
+
+        private void ReadEffectProfileCommon(Effect effect)
+        {
+            xml.ReadStartElement();
+            ReadAsset();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "image":
+                        ReadEntity(images, ReadImage);
+                        break;
+                    case "newparam":
+                        ReadEffectParameterDecl(effect);
+                        break;
+                    case "technique":
+                        ReadEffectTechniqueCommon(effect);
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            ReadExtra();
+            xml.ReadEndElement();
+        }
+
+        private void ReadEffectTechniqueCommon(Effect effect)
+        {
+            xml.ReadStartElement();
+            ReadAsset();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "image":
+                        ReadEntity(images, ReadImage);
+                        break;
+
+                    case "constant":
+                    case "lambert":
+                    case "phong":
+                    case "blinn":
+                        xml.ReadStartElement();
+                        ReadEffectTechniqueParameters(effect);
+                        xml.ReadEndElement();
+                        break;
+
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            ReadExtra();
+            xml.ReadEndElement();
+        }
+
+        private void ReadEffectParameterDecl(Effect effect)
+        {
+            var parameter = new EffectParameter();
+            parameter.Sid = xml.GetAttribute("sid");
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "semantic":
+                        parameter.Semantic = xml.ReadElementContentAsString();
+                        break;
+                    case "float":
+                        parameter.Value = xml.ReadElementContentAsFloat();
+                        break;
+                    case "float2":
+                        parameter.Value = xml.ReadElementContentAsVector2();
+                        break;
+                    case "float3":
+                        parameter.Value = xml.ReadElementContentAsVector3();
+                        break;
+                    case "surface":
+                        parameter.Value = ReadEffectSurface(effect);
+                        break;
+                    case "sampler2D":
+                        parameter.Value = ReadEffectSampler2D(effect);
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+
+            effect.Parameters.Add(parameter);
+        }
+
+        private EffectSurface ReadEffectSurface(Effect effect)
+        {
+            var surface = new EffectSurface();
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "init_from":
+                        BindId<Image>(xml.ReadElementContentAsString(), image => {
+                            surface.InitFrom = image;
+                        });
+                        break;
+
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+            return surface;
+        }
+
+        private EffectSampler ReadEffectSampler2D(Effect effect)
+        {
+            var sampler = new EffectSampler();
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "source":
+                        string surfaceSid = xml.ReadElementContentAsString();
+                        foreach (var parameter in effect.Parameters)
+                        {
+                            if (parameter.Sid == surfaceSid)
+                                sampler.Surface = parameter.Value as EffectSurface;
+                        }
+                        break;
+                    case "wrap_s":
+                        sampler.WrapS = (EffectSamplerWrap)Enum.Parse(typeof(EffectSamplerWrap), xml.ReadElementContentAsString(), true);
+                        break;
+                    case "wrap_t":
+                        sampler.WrapT = (EffectSamplerWrap)Enum.Parse(typeof(EffectSamplerWrap), xml.ReadElementContentAsString(), true);
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+            return sampler;
+        }
+
+        private void ReadEffectTechniqueParameters(Effect effect)
+        {
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "emission":
+                        ReadColorEffectParameter(effect, effect.Emission, EffectTextureChannel.Emission);
+                        break;
+                    case "ambient":
+                        ReadColorEffectParameter(effect, effect.Ambient, EffectTextureChannel.Ambient);
+                        break;
+                    case "diffuse":
+                        ReadColorEffectParameter(effect, effect.Diffuse, EffectTextureChannel.Diffuse);
+                        break;
+                    case "specular":
+                        ReadColorEffectParameter(effect, effect.Specular, EffectTextureChannel.Specular);
+                        break;
+                    case "shininess":
+                        ReadFloatEffectParameter(effect.Shininess);
+                        break;
+                    case "reflective":
+                        ReadColorEffectParameter(effect, effect.Reflective, EffectTextureChannel.Reflective);
+                        break;
+                    case "reflectivity":
+                        ReadFloatEffectParameter(effect.Reflectivity);
+                        break;
+                    case "transparent":
+                        ReadColorEffectParameter(effect, effect.Transparent, EffectTextureChannel.Transparent);
+                        break;
+                    case "transparency":
+                        ReadFloatEffectParameter(effect.Transparency);
+                        break;
+                    case "index_of_refraction":
+                        ReadFloatEffectParameter(effect.IndexOfRefraction);
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+        }
+
+        private void ReadFloatEffectParameter(EffectParameter parameter)
+        {
+            xml.ReadStartElement();
+
+            if (xml.IsStartElement("float"))
+            {
+                parameter.Sid = xml.GetAttribute("sid");
+                parameter.Value = xml.ReadElementContentAsFloat();
+            }
+            else if (xml.IsStartElement("param"))
+            {
+                parameter.Reference = xml.GetAttribute("ref");
+                xml.Skip();
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadColorEffectParameter(Effect effect, EffectParameter parameter, EffectTextureChannel channel)
+        {
+            xml.ReadStartElement();
+
+            if (xml.IsStartElement("color"))
+            {
+                parameter.Sid = xml.GetAttribute("sid");
+                parameter.Value = xml.ReadElementContentAsVector4();
+            }
+            else if (xml.IsStartElement("param"))
+            {
+                parameter.Sid = null;
+                parameter.Reference = xml.GetAttribute("ref");
+            }
+            else if (xml.IsStartElement("texture"))
+            {
+                parameter.Sid = null;
+
+                string texCoordSymbol = xml.GetAttribute("texcoord");
+                string samplerId = xml.GetAttribute("texture");
+                xml.Skip();
+
+                //
+                // HACK: Maya produces duplicate texture elements, skip them...
+                //
+
+                while (xml.IsStartElement("texture"))
+                    xml.Skip();
+
+                EffectSampler sampler = null;
+
+                foreach (var parameterDecl in effect.Parameters)
+                {
+                    if (parameterDecl.Sid == samplerId)
+                    {
+                        sampler = parameterDecl.Value as EffectSampler;
+                        break;
+                    }
+                }
+
+                if (sampler == null)
+                {
+                    info.WriteLine("COLLADA: cannot find sampler {0} in effect {1}, trying to use image directly", samplerId, effect.Name);
+
+                    var surface = new EffectSurface();
+                    sampler = new EffectSampler(surface);
+
+                    BindId<Image>(samplerId, image => {
+                        surface.InitFrom = image;
+                    });
+                }
+
+                var texture = new EffectTexture {
+                    Channel = channel,
+                    TexCoordSemantic = texCoordSymbol,
+                    Sampler = sampler
+                };
+
+                parameter.Value = texture;
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadMaterial(Material material)
+        {
+            if (!xml.IsStartElement("instance_effect"))
+                return;
+
+            BindUrlAttribute<Effect>("url", effect => {
+                material.Effect = effect;
+            });
+
+            xml.Skip();
+        }
+
+        //
+        // Geometry
+        //
+
+        private void ReadGeometry(Geometry geometry)
+        {
+            if (xml.IsStartElement("mesh"))
+            {
+                ReadMesh(geometry);
+            }
+            else
+            {
+                throw new NotSupportedException(string.Format("Geometry content of type {0} is not supported", xml.LocalName));
+            }
+        }
+
+        //
+        // Mesh
+        //
+
+        private void ReadMesh(Geometry geometry)
+        {
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement("source"))
+                ReadGeometrySource();
+
+            if (xml.IsStartElement("vertices"))
+                ReadMeshVertices(geometry);
+
+            while (xml.IsStartElement())
+            {
+                var primitives = ReadMeshPrimitives(geometry);
+
+                if (primitives == null)
+                    break;
+
+                geometry.Primitives.Add(primitives);
+            }
+
+            ReadExtra();
+            xml.ReadEndElement();
+        }
+
+        //
+        // Mesh Data Source
+        //
+
+        private Source ReadGeometrySource()
+        {
+            string id = xml.GetAttribute("id");
+            string name = xml.GetAttribute("name");
+
+            xml.ReadStartElement();
+            ReadAsset();
+
+            var data = ReadFloatArray();
+            var source = new Source(data, 1) {
+                Name = name
+            };
+
+            if (xml.IsStartElement("technique_common"))
+            {
+                xml.ReadStartElement();
+
+                if (xml.IsStartElement("accessor"))
+                {
+                    source.Stride = ReadIntAttribute("stride", 1);
+
+                    xml.ReadStartElement();
+
+                    while (xml.IsStartElement("param"))
+                        ReadParam();
+
+                    xml.ReadEndElement();
+                }
+
+                xml.ReadEndElement();
+            }
+
+            xml.SkipSequence("technique");
+            xml.ReadEndElement();
+
+            AddEntity(id, source);
+
+            return source;
+        }
+
+        private string ReadParam()
+        {
+            string name = xml.GetAttribute("name");
+            //string sid = xml.GetAttribute("sid");
+            //string type = xml.GetAttribute("type");
+            //string semantic = xml.GetAttribute("semantic");
+
+            xml.Skip();
+
+            return name;
+        }
+
+        //
+        // Mesh Vertices
+        //
+
+        private void ReadMeshVertices(Geometry mesh)
+        {
+            string id = xml.GetAttribute("id");
+
+            for (xml.ReadStartElement(); xml.IsStartElement("input"); xml.Skip())
+            {
+                var semantic = ReadSemanticAttribute();
+
+                if (semantic != Semantic.None && semantic != Semantic.Vertex)
+                {
+                    var input = new Input();
+                    input.Semantic = semantic;
+                    BindUrlAttribute<Source>("source", s => {
+                        input.Source = s;
+                    });
+                    mesh.Vertices.Add(input);
+                }
+            }
+
+            ReadExtra();
+            xml.ReadEndElement();
+        }
+
+        //
+        // Mesh Polygons
+        //
+
+        private MeshPrimitives ReadMeshPrimitives(Geometry mesh)
+        {
+            MeshPrimitives primitives;
+
+            int primitiveCount = ReadIntAttribute("count", 0);
+            int fixedVertexCount = 0;
+            bool isPolygons = false;
+
+            switch (xml.LocalName)
+            {
+                case "lines":
+                    primitives = new MeshPrimitives(MeshPrimitiveType.Lines);
+                    fixedVertexCount = 2;
+                    break;
+                case "triangles":
+                    primitives = new MeshPrimitives(MeshPrimitiveType.Polygons);
+                    fixedVertexCount = 3;
+                    break;
+
+                case "linestrips":
+                    primitives = new MeshPrimitives(MeshPrimitiveType.LineStrips);
+                    isPolygons = true;
+                    break;
+                case "trifans":
+                    primitives = new MeshPrimitives(MeshPrimitiveType.TriangleFans);
+                    isPolygons = true;
+                    break;
+                case "tristrips":
+                    primitives = new MeshPrimitives(MeshPrimitiveType.TriangleStrips);
+                    isPolygons = true;
+                    break;
+                case "polygons":
+                    primitives = new MeshPrimitives(MeshPrimitiveType.Polygons);
+                    isPolygons = true;
+                    break;
+
+                case "polylist":
+                    primitives = new MeshPrimitives(MeshPrimitiveType.Polygons);
+                    break;
+
+                default:
+                    return null;
+            }
+
+            primitives.MaterialSymbol = xml.GetAttribute("material");
+
+            bool vertexFound = false;
+
+            //
+            // Read the inputs
+            //
+
+            for (xml.ReadStartElement(); xml.IsStartElement("input"); xml.Skip())
+            {
+                var semantic = ReadSemanticAttribute();
+
+                if (semantic == Semantic.None)
+                    continue;
+
+                int offset = ReadIntAttribute("offset");
+                string sourceId = xml.GetAttribute("source");
+                int set = ReadIntAttribute("set", -1);
+
+                if (semantic == Semantic.Vertex)
+                {
+                    if (vertexFound)
+                    {
+                        error.WriteLine("Duplicate vertex input found");
+                        continue;
+                    }
+
+                    vertexFound = true;
+
+                    foreach (var vertexInput in mesh.Vertices)
+                    {
+                        primitives.Inputs.Add(new IndexedInput {
+                            Source = vertexInput.Source,
+                            Offset = offset,
+                            Set = set,
+                            Semantic = vertexInput.Semantic
+                        });
+                    }
+                }
+                else
+                {
+                    var input = new IndexedInput {
+                        Offset = offset,
+                        Semantic = semantic,
+                        Set = set
+                    };
+
+                    BindUrl<Source>(sourceId, s => {
+
+                        //
+                        // Ignore inputs with no source data
+                        //
+
+                        if (s.Count > 0)
+                        {
+                            input.Source = s;
+                            primitives.Inputs.Add(input);
+                        }
+                    });
+                }
+            }
+
+            if (!vertexFound)
+                throw new InvalidDataException("no vertex input");
+
+            //
+            // Read vertex counts (if availabled and needed)
+            //
+
+            if (primitiveCount > 0)
+                primitives.VertexCounts.Capacity = primitiveCount;
+
+            int numIndices = 0;
+
+            while (xml.IsStartElement("vcount"))
+            {
+                if (fixedVertexCount != 0 || isPolygons)
+                {
+                    xml.Skip();
+                    continue;
+                }
+
+                foreach (var token in xml.ReadElementContentAsList())
+                {
+                    int count = XmlConvert.ToInt32(token);
+                    numIndices += count;
+                    primitives.VertexCounts.Add(count);
+                }
+            }
+
+            if (fixedVertexCount != 0)
+            {
+                for (int i = 0; i < primitiveCount; i++)
+                    primitives.VertexCounts.Add(fixedVertexCount);
+
+                numIndices = fixedVertexCount * primitiveCount;
+            }
+            else if (!isPolygons)
+            {
+                if (primitives.VertexCounts.Count == 0)
+                    throw new InvalidDataException("no vcount");
+            }
+
+            //
+            // Read input indices
+            // 1. Collect all inputs in an array indexed by input offset
+            //
+
+            var maxOffset = primitives.Inputs.Max(x => x.Offset);
+            var inputIndices = new List<int>[maxOffset + 1];
+
+            foreach (var input in primitives.Inputs)
+            {
+                List<int> indices = inputIndices[input.Offset];
+
+                if (indices == null)
+                {
+                    indices = new List<int>(numIndices);
+                    inputIndices[input.Offset] = indices;
+                }
+            }
+
+            //
+            // 2. Read polygon input indices
+            //
+
+            if (!isPolygons)
+            {
+                while (xml.IsStartElement("p"))
+                    ReadInterleavedInputIndices(inputIndices);
+            }
+            else
+            {
+                while (xml.IsStartElement())
+                {
+                    if (xml.IsStartElement("p"))
+                    {
+                        primitives.VertexCounts.Add(ReadInterleavedInputIndices(inputIndices));
+                    }
+                    else if (xml.IsStartElement("ph"))
+                    {
+                        xml.ReadStartElement();
+
+                        while (xml.IsStartElement())
+                        {
+                            if (xml.LocalName == "p")
+                                primitives.VertexCounts.Add(ReadInterleavedInputIndices(inputIndices));
+                            else
+                                xml.Skip();
+                        }
+
+                        xml.ReadEndElement();
+                    }
+                    else
+                    {
+                        break;
+                    }
+                }
+            }
+
+            foreach (var input in primitives.Inputs)
+                input.Indices.AddRange(inputIndices[input.Offset]);
+
+            ReadExtra();
+            xml.ReadEndElement();
+
+            return primitives;
+        }
+
+        private int ReadInterleavedInputIndices(List<int>[] inputs)
+        {
+            int count = 0;
+            int offset = 0;
+
+            foreach (string token in xml.ReadElementContentAsList())
+            {
+                var input = inputs[offset++];
+
+                if (input != null)
+                    input.Add(XmlConvert.ToInt32(token));
+
+                if (offset >= inputs.Length)
+                {
+                    offset = 0;
+                    count++;
+                }
+            }
+
+            return count;
+        }
+
+        //
+        // Scene
+        //
+
+        private void ReadScene(Scene scene)
+        {
+            while (xml.IsStartElement("node"))
+                ReadEntity(scene.Nodes, ReadNode);
+        }
+
+        private void ReadNode(Node node)
+        {
+            ReadTransforms(node.Transforms);
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "node":
+                        ReadEntity(node.Nodes, ReadNode);
+                        break;
+
+                    case "instance_geometry":
+                        node.Instances.Add(ReadGeometryInstance());
+                        break;
+
+                    case "instance_light":
+                        node.Instances.Add(ReadLightInstance());
+                        break;
+
+                    case "instance_camera":
+                        node.Instances.Add(ReadCameraInstance());
+                        break;
+
+                    case "instance_node":
+                        node.Instances.Add(ReadNodeInstance());
+                        break;
+
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+        }
+
+        private void ReadSimpleInstance<T>(Instance<T> instance)
+            where T : Entity
+        {
+            instance.Sid = xml.GetAttribute("sid");
+            instance.Name = xml.GetAttribute("name");
+            BindUrlAttribute<T>("url", camera => {
+                instance.Target = camera;
+            });
+
+            xml.Skip();
+        }
+
+        private NodeInstance ReadNodeInstance()
+        {
+            var instance = new NodeInstance();
+            ReadSimpleInstance(instance);
+            return instance;
+        }
+
+        private CameraInstance ReadCameraInstance()
+        {
+            var instance = new CameraInstance();
+            ReadSimpleInstance(instance);
+            return instance;
+        }
+
+        private LightInstance ReadLightInstance()
+        {
+            var instance = new LightInstance();
+            ReadSimpleInstance(instance);
+            return instance;
+        }
+
+        //
+        // Node Transforms
+        //
+
+        private void ReadTransforms(ICollection<Transform> transforms)
+        {
+            while (xml.IsStartElement())
+            {
+                Transform transform = null;
+
+                switch (xml.LocalName)
+                {
+                    case "matrix":
+                        transform = new TransformMatrix();
+                        break;
+                    case "rotate":
+                        transform = new TransformRotate();
+                        break;
+                    case "scale":
+                        transform = new TransformScale();
+                        break;
+                    case "translate":
+                        transform = new TransformTranslate();
+                        break;
+
+                    case "skew":
+                    case "lookat":
+                        xml.Skip();
+                        break;
+
+                    default:
+                        return;
+                }
+
+                if (transform != null)
+                {
+                    transform.Sid = xml.GetAttribute("sid");
+                    xml.ReadElementContentAsArray(floatConverter, transform.Values);
+                    transforms.Add(transform);
+                }
+            }
+        }
+
+        //
+        // Instances
+        //
+
+        private void ReadInstances(ICollection<Instance> instances)
+        {
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "instance_geometry":
+                        instances.Add(ReadGeometryInstance());
+                        break;
+
+                    case "instance_camera":
+                    case "instance_controller":
+                    case "instance_light":
+                    case "instance_node":
+                        xml.Skip();
+                        break;
+
+                    default:
+                        return;
+                }
+            }
+        }
+
+        private GeometryInstance ReadGeometryInstance()
+        {
+            var instance = new GeometryInstance {
+                Name = xml.GetAttribute("name"),
+                Sid = xml.GetAttribute("sid"),
+            };
+
+            string url = xml.GetAttribute("url");
+            BindUrl<Geometry>(url, geometry => {
+                instance.Target = geometry;
+            });
+
+            if (!xml.SkipEmpty())
+            {
+                xml.ReadStartElement();
+
+                if (xml.IsStartElement("bind_material"))
+                    ReadBindMaterial(instance, url);
+
+                ReadExtra();
+
+                xml.ReadEndElement();
+            }
+
+            return instance;
+        }
+
+        private void ReadBindMaterial(GeometryInstance geometryInstance, string geometryUrl)
+        {
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                if (xml.LocalName != "technique_common")
+                {
+                    xml.Skip();
+                    continue;
+                }
+
+                xml.ReadStartElement();
+
+                while (xml.IsStartElement())
+                {
+                    if (xml.LocalName == "instance_material")
+                        ReadMaterialInstance(geometryInstance, geometryUrl);
+                    else
+                        xml.Skip();
+                }
+
+                xml.ReadEndElement();
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadMaterialInstance(GeometryInstance geometryInstance, string geometryUrl)
+        {
+            var instance = new MaterialInstance();
+            instance.Symbol = xml.GetAttribute("symbol");
+            BindUrlAttribute<Material>("target", material => {
+                instance.Target = material;
+            });
+            geometryInstance.Materials.Add(instance);
+
+            if (xml.SkipEmpty())
+                return;
+
+            for (xml.ReadStartElement(); xml.IsStartElement(); xml.Skip())
+            {
+                if (xml.LocalName == "bind")
+                {
+                    var binding = new MaterialBinding();
+                    binding.Semantic = xml.GetAttribute("semantic");
+                    string target = xml.GetAttribute("target");
+
+                    BindId<Source>(target, s => {
+                        BindUrl<Geometry>(geometryUrl, g => {
+                            var primitives = g.Primitives.Find(p => p.MaterialSymbol == instance.Symbol);
+
+                            if (primitives == null)
+                                return;
+
+                            var input = primitives.Inputs.Find(i => i.Source == s);
+
+                            if (input == null)
+                                return;
+
+                            binding.VertexInput = input;
+                            instance.Bindings.Add(binding);
+                        });
+                    });
+                }
+                else if (xml.LocalName == "bind_vertex_input")
+                {
+                    var binding = new MaterialBinding();
+                    binding.Semantic = xml.GetAttribute("semantic");
+                    var inputSemantic = ReadSemanticAttribute("input_semantic");
+                    int inputSet = ReadIntAttribute("input_set", 0);
+
+                    BindUrl<Geometry>(geometryUrl, g => {
+                        var primitives = g.Primitives.Find(p => p.MaterialSymbol == instance.Symbol);
+
+                        if (primitives == null)
+                            return;
+
+                        var input = primitives.Inputs.Find(i => i.Semantic == inputSemantic && i.Set == inputSet);
+
+                        if (input == null)
+                            return;
+
+                        binding.VertexInput = input;
+                        instance.Bindings.Add(binding);
+                    });
+                }
+            }
+
+            xml.ReadEndElement();
+        }
+
+        //
+        // Animations
+        //
+
+        private void ReadAnimation(Animation animation)
+        {
+            while (xml.IsStartElement("animation"))
+                ReadEntity(animation.Animations, ReadAnimation);
+
+            while (xml.IsStartElement("source"))
+                ReadAnimationSource();
+
+            while (xml.IsStartElement("sampler"))
+                animation.Samplers.Add(ReadAnimationSampler());
+
+            while (xml.IsStartElement("channel"))
+            {
+                BindAnimationSampler(xml.GetAttribute("source"), xml.GetAttribute("target"));
+                xml.Skip();
+            }
+        }
+
+        private Source ReadAnimationSource()
+        {
+            string id = xml.GetAttribute("id");
+            string name = xml.GetAttribute("name");
+
+            if (string.IsNullOrEmpty(name))
+                name = id;
+
+            xml.ReadStartElement();
+
+            ReadAsset();
+
+            Source source;
+
+            if (xml.IsStartElement("float_array"))
+                source = new Source(ReadFloatArray(), 1);
+            else if (xml.IsStartElement("Name_array"))
+                source = new Source(ReadNameArray(), 1);
+            else
+                throw new NotSupportedException(string.Format("Animation sources of type {0} are not supported", xml.LocalName));
+
+            source.Name = name;
+
+            if (xml.IsStartElement("technique_common"))
+            {
+                xml.ReadStartElement();
+
+                if (xml.IsStartElement("accessor"))
+                {
+                    source.Stride = ReadIntAttribute("stride", 1);
+
+                    xml.ReadStartElement();
+
+                    while (xml.IsStartElement("param"))
+                        ReadParam();
+
+                    xml.ReadEndElement();
+                }
+
+                xml.ReadEndElement();
+            }
+
+            xml.SkipSequence("technique");
+            xml.ReadEndElement();
+
+            AddEntity(id, source);
+
+            return source;
+        }
+
+        private Input ReadAnimationInput()
+        {
+            var input = new Input();
+            input.Semantic = ReadSemanticAttribute();
+            BindUrlAttribute<Source>("source", source => {
+                input.Source = source;
+            });
+
+            xml.Skip();
+
+            return input;
+        }
+
+        private Sampler ReadAnimationSampler()
+        {
+            string id = xml.GetAttribute("id");
+
+            xml.ReadStartElement();
+
+            var sampler = new Sampler();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "input":
+                        sampler.Inputs.Add(ReadAnimationInput());
+                        break;
+
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+
+            AddEntity(id, sampler);
+
+            return sampler;
+        }
+
+        #region private class TargetPath
+
+        private class TargetPath
+        {
+            private string nodeId;
+            private string[] path;
+            private string value;
+
+            private TargetPath()
+            {
+            }
+
+            public static TargetPath Parse(string text)
+            {
+                TargetPath path = new TargetPath();
+                List<string> sids = new List<string>();
+
+                int index = text.IndexOf('/');
+
+                if (index == -1)
+                    index = text.Length;
+
+                path.nodeId = text.Substring(0, index);
+
+                for (int start = index + 1; start < text.Length; start = index + 1)
+                {
+                    index = text.IndexOf('/', start);
+
+                    if (index == -1)
+                    {
+                        index = text.IndexOf('.', start);
+
+                        if (index == -1)
+                        {
+                            sids.Add(text.Substring(start));
+                        }
+                        else
+                        {
+                            sids.Add(text.Substring(start, index - start));
+                            path.value = text.Substring(index + 1);
+                        }
+
+                        break;
+                    }
+
+                    sids.Add(text.Substring(start, index - start));
+                }
+
+                if (sids.Count > 0)
+                    path.path = sids.ToArray();
+
+                return path;
+            }
+
+            public string NodeId => nodeId;
+            public string[] Path => path;
+            public string Value => value;
+        }
+
+        #endregion
+
+        //
+        // Lights
+        //
+
+        private void ReadLight(Light light)
+        {
+            if (!xml.IsStartElement("technique_common"))
+            {
+                xml.Skip();
+                return;
+            }
+
+            xml.ReadStartElement();
+
+            if (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "ambient":
+                        light.Type = LightType.Ambient;
+                        break;
+
+                    case "directional":
+                        light.Type = LightType.Directional;
+                        break;
+
+                    case "point":
+                        light.Type = LightType.Point;
+                        break;
+
+                    case "spot":
+                        light.Type = LightType.Spot;
+                        break;
+                }
+
+                xml.ReadStartElement();
+
+                light.Color = xml.ReadElementContentAsVector3("color");
+
+                if (light.Type == LightType.Point || light.Type == LightType.Spot)
+                {
+                    if (xml.LocalName == "constant_attenuation")
+                        light.ConstantAttenuation = xml.ReadElementContentAsFloat();
+
+                    if (xml.LocalName == "linear_attenuation")
+                        light.LinearAttenuation = xml.ReadElementContentAsFloat();
+
+                    if (light.Type == LightType.Point)
+                    {
+                        light.QuadraticAttenuation = xml.ReadElementContentAsFloat("quadratic_attenuation", string.Empty);
+
+                        if (xml.LocalName == "zfar")
+                            light.ZFar = xml.ReadElementContentAsFloat();
+                    }
+                    else if (light.Type == LightType.Spot)
+                    {
+                        if (xml.LocalName == "quadratic_attenuation")
+                            light.QuadraticAttenuation = xml.ReadElementContentAsFloat();
+
+                        if (xml.LocalName == "falloff_angle")
+                            light.FalloffAngle = xml.ReadElementContentAsFloat();
+
+                        if (xml.LocalName == "falloff_exponent")
+                            light.FalloffExponent = xml.ReadElementContentAsFloat();
+                    }
+                }
+
+                xml.ReadEndElement();
+            }
+
+            xml.ReadEndElement();
+        }
+
+        //
+        // Scene
+        //
+
+        private void ReadScene()
+        {
+            if (!xml.IsStartElement("scene"))
+                return;
+
+            xml.ReadStartElement();
+            xml.SkipSequence("instance_physics_scene");
+
+            if (xml.IsStartElement("instance_visual_scene"))
+            {
+                BindUrlAttribute<Scene>("url", scene => {
+                    mainScene = scene;
+                });
+                xml.Skip();
+            }
+
+            ReadExtra();
+            xml.ReadEndElement();
+        }
+
+        //
+        // Others
+        //
+
+        private void ReadAsset()
+        {
+            if (!xml.IsStartElement("asset"))
+                return;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "up_axis":
+                        upAxis = ReadUpAxis();
+                        break;
+                    case "contributor":
+                        ReadAssetContributor();
+                        break;
+                    case "unit":
+                        unit = XmlConvert.ToSingle(xml.GetAttribute("meter"));
+                        xml.Skip();
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadAssetContributor()
+        {
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadExtra()
+        {
+            while (xml.IsStartElement("extra"))
+                xml.Skip();
+        }
+
+        //
+        // Arrays
+        //
+
+        private float[] ReadFloatArray()
+        {
+            if (!xml.IsStartElement("float_array"))
+                return null;
+
+            var id = xml.GetAttribute("id");
+            var count = ReadIntAttribute("count");
+            var data = new float[count];
+            var index = 0;
+
+            foreach (var token in xml.ReadElementContentAsList())
+            {
+                if (index < data.Length)
+                    data[index++] = XmlConvert.ToSingle(token);
+            }
+
+            return data;
+        }
+
+        private string[] ReadNameArray()
+        {
+            if (!xml.IsStartElement("Name_array"))
+                return null;
+
+            var id = xml.GetAttribute("id");
+            var count = ReadIntAttribute("count");
+            var data = new string[count];
+            var index = 0;
+
+            foreach (var token in xml.ReadElementContentAsList())
+            {
+                if (index < data.Length)
+                    data[index++] = token;
+            }
+
+            return data;
+        }
+
+        //
+        // Attributes
+        //
+
+        private int ReadIntAttribute(string name)
+        {
+            string text = xml.GetAttribute(name);
+
+            if (string.IsNullOrEmpty(text))
+                throw new InvalidDataException(name + " attribute not found");
+
+            return XmlConvert.ToInt32(text);
+        }
+
+        private int ReadIntAttribute(string name, int defaultValue)
+        {
+            string text = xml.GetAttribute(name);
+
+            if (string.IsNullOrEmpty(text))
+                return defaultValue;
+
+            return XmlConvert.ToInt32(text);
+        }
+
+        private int? ReadNullableIntAttribute(string name)
+        {
+            string text = xml.GetAttribute(name);
+
+            if (string.IsNullOrEmpty(text))
+                return null;
+
+            return XmlConvert.ToInt32(text);
+        }
+
+        private Semantic ReadSemanticAttribute()
+        {
+            return ReadSemanticAttribute("semantic");
+        }
+
+        private Semantic ReadSemanticAttribute(string name)
+        {
+            string text = xml.GetAttribute(name);
+
+            switch (text)
+            {
+                case "POSITION":
+                    return Semantic.Position;
+                case "TEXCOORD":
+                    return Semantic.TexCoord;
+                case "NORMAL":
+                    return Semantic.Normal;
+                case "COLOR":
+                    return Semantic.Color;
+                case "VERTEX":
+                    return Semantic.Vertex;
+
+                case "INPUT":
+                    return Semantic.Input;
+                case "IN_TANGENT":
+                    return Semantic.InTangent;
+                case "OUT_TANGENT":
+                    return Semantic.OutTangent;
+                case "INTERPOLATION":
+                    return Semantic.Interpolation;
+                case "OUTPUT":
+                    return Semantic.Output;
+
+                default:
+                    return Semantic.None;
+            }
+        }
+
+        private string[] ReadStringListAttribute(string name)
+        {
+            string text = xml.GetAttribute(name);
+
+            if (string.IsNullOrEmpty(text))
+                return emptyStrings;
+
+            return text.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries);
+        }
+
+        private Axis ReadUpAxis()
+        {
+            switch (xml.ReadElementContentAsString())
+            {
+                case "X_UP":
+                    return Axis.X;
+                case "Z_UP":
+                    return Axis.Z;
+                default:
+                    return Axis.Y;
+            }
+        }
+
+        private void AddEntity(string id, Entity entity)
+        {
+            if (string.IsNullOrEmpty(id))
+                return;
+
+            if (entities == null)
+                entities = new Dictionary<string, Entity>();
+
+            if (entities.ContainsKey(id))
+                error.WriteLine(string.Format("COLLADA error: duplicate id {0}", id));
+            else
+                entities.Add(id, entity);
+
+            entity.Id = id;
+        }
+
+        private T GetEntity<T>(string id) where T : Entity
+        {
+            Entity result;
+
+            if (entities == null
+                || string.IsNullOrEmpty(id)
+                || !entities.TryGetValue(id, out result))
+                return null;
+
+            return result as T;
+        }
+
+        private void BindUrlAttribute<T>(string name, Action<T> action)
+            where T : Entity
+        {
+            BindUrl(xml.GetAttribute(name), action);
+        }
+
+        private void BindUrl<T>(string url, Action<T> action)
+            where T : Entity
+        {
+            if (string.IsNullOrEmpty(url))
+                return;
+
+            if (url[0] != '#')
+                throw new NotSupportedException(string.Format("External reference '{0}' is not supported", url));
+
+            BindId<T>(url.Substring(1), action);
+        }
+
+        private void BindId<T>(string id, Action<T> action)
+            where T : Entity
+        {
+            if (string.IsNullOrEmpty(id))
+                return;
+
+            var entity = GetEntity<T>(id);
+
+            if (entity != null)
+            {
+                action(entity);
+                return;
+            }
+
+            delayedBindActions.Add(delegate {
+                var entity2 = GetEntity<T>(id);
+
+                if (entity2 != null)
+                    action(entity2);
+            });
+        }
+
+        private void BindAnimationSampler(string sourceId, string targetPath)
+        {
+            var path = TargetPath.Parse(targetPath);
+
+            BindUrlAttribute<Sampler>("source", sampler => {
+                var output = sampler.Inputs.Find(i => i.Semantic == Semantic.Output);
+
+                if (output == null || output.Source == null)
+                    return;
+
+                int stride = output.Source.Stride;
+
+                if (stride != 1)
+                {
+                    for (int offset = 0; offset < stride; offset++)
+                    {
+                        var newSampler = sampler.Split(offset);
+
+                        BindId<Node>(path.NodeId, node => {
+                            Transform transform = FindTransform(node, path.Path[0]);
+
+                            if (transform != null)
+                                transform.BindAnimation(string.Format(CultureInfo.InvariantCulture, "({0})", offset), newSampler);
+                        });
+                    }
+                }
+                else
+                {
+                    BindId<Node>(path.NodeId, node => {
+                        var transform = FindTransform(node, path.Path[0]);
+
+                        if (transform != null)
+                            transform.BindAnimation(path.Value, sampler);
+                    });
+                }
+            });
+        }
+
+        private Transform FindTransform(Node node, string sid)
+        {
+            return node.Transforms.Find(t => t.Sid == sid);
+        }
+
+        private void BindNodes(Node node)
+        {
+            foreach (var instance in node.Instances.OfType<NodeInstance>().ToList())
+            {
+                node.Instances.Remove(instance);
+
+                if (instance.Target != node)
+                    node.Nodes.Add(instance.Target);
+            }
+
+            foreach (var child in node.Nodes)
+                BindNodes(child);
+        }
+    }
+}
Index: /OniSplit/Dae/IO/DaeWriter.cs
===================================================================
--- /OniSplit/Dae/IO/DaeWriter.cs	(revision 1114)
+++ /OniSplit/Dae/IO/DaeWriter.cs	(revision 1114)
@@ -0,0 +1,1086 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Globalization;
+using System.Text;
+using System.Xml;
+
+namespace Oni.Dae.IO
+{
+    internal class DaeWriter
+    {
+        #region Private data
+        private XmlWriter xml;
+        private Scene mainScene;
+        private WriteVisitor visitor;
+        private Dictionary<Source, string> writtenSources = new Dictionary<Source, string>();
+        #endregion
+
+        #region private class Animation
+
+        private class Animation : Entity
+        {
+            public readonly List<Source> Sources = new List<Source>();
+            public readonly List<Sampler> Samplers = new List<Sampler>();
+            public readonly List<AnimationChannel> Channels = new List<AnimationChannel>();
+        }
+
+        #endregion
+        #region private class AnimationChannel
+
+        private class AnimationChannel
+        {
+            public readonly Sampler Sampler;
+            public readonly string TargetPath;
+
+            public AnimationChannel(Sampler sampler, string targetPath)
+            {
+                this.Sampler = sampler;
+                this.TargetPath = targetPath;
+            }
+        }
+
+        #endregion
+        #region private class AnimationSource
+
+        private class AnimationSource : Source
+        {
+            public readonly string[] Parameters;
+
+            public AnimationSource(float[] data, string[] parameters)
+                : base(data, parameters.Length)
+            {
+                this.Parameters = parameters;
+            }
+
+            public AnimationSource(string[] data, string[] parameters)
+                : base(data, parameters.Length)
+            {
+                this.Parameters = parameters;
+            }
+        }
+
+        #endregion
+
+        #region private class WriteVisitor
+
+        private class WriteVisitor : Visitor
+        {
+            private readonly Dictionary<Entity, string> entities = new Dictionary<Entity, string>();
+            private readonly Dictionary<string, Entity> ids = new Dictionary<string, Entity>(StringComparer.Ordinal);
+            private readonly Dictionary<string, Sampler> samplers = new Dictionary<string, Sampler>(StringComparer.Ordinal);
+            private readonly Dictionary<string, Source> sources = new Dictionary<string, Source>(StringComparer.Ordinal);
+            private int uniqueEntityId = 1;
+
+            public readonly List<Image> Images = new List<Image>();
+            public readonly List<Effect> Effects = new List<Effect>();
+            public readonly List<Material> Materials = new List<Material>();
+            public readonly List<Geometry> Geometries = new List<Geometry>();
+            public readonly List<Scene> Scenes = new List<Scene>();
+            public readonly List<Animation> Animations = new List<Animation>();
+            public readonly List<Camera> Cameras = new List<Camera>();
+
+            public override void VisitScene(Scene scene)
+            {
+                AddEntity(scene);
+
+                base.VisitScene(scene);
+            }
+
+            public override void VisitNode(Node node)
+            {
+                EnsureId(node);
+
+                foreach (var transform in node.Transforms.Where(t => t.HasAnimations))
+                {
+                    for (int i = 0; i < transform.Animations.Length; i++)
+                    {
+                        var sampler = transform.Animations[i];
+
+                        if (sampler != null)
+                            AddAnimationChannel(sampler, node, transform, i);
+                    }
+                }
+
+                base.VisitNode(node);
+            }
+
+            public override void VisitGeometry(Geometry geometry)
+            {
+                AddEntity(geometry);
+
+                string baseId = IdOf(geometry);
+
+                if (baseId.EndsWith("_geometry", StringComparison.Ordinal))
+                    baseId = baseId.Substring(0, baseId.Length - "_geometry".Length);
+
+                foreach (var input in geometry.Vertices)
+                    EnsureId(input.Source, string.Format(CultureInfo.InvariantCulture, "{0}_{1}", baseId, input.Semantic.ToString().ToLowerInvariant()));
+
+                foreach (var input in geometry.Primitives.SelectMany(p => p.Inputs))
+                    EnsureId(input.Source, string.Format(CultureInfo.InvariantCulture, "{0}_{1}", baseId, input.Semantic.ToString().ToLowerInvariant()));
+
+                base.VisitGeometry(geometry);
+            }
+
+            public override void VisitMaterial(Material material)
+            {
+                AddEntity(material);
+
+                base.VisitMaterial(material);
+            }
+
+            public override void VisitEffect(Effect effect)
+            {
+                AddEntity(effect);
+
+                base.VisitEffect(effect);
+            }
+
+            public override void VisitImage(Image image)
+            {
+                AddEntity(image);
+
+                base.VisitImage(image);
+            }
+
+            public override void VisitCamera(Camera camera)
+            {
+                AddEntity(camera);
+
+                base.VisitCamera(camera);
+            }
+
+            private void AddEntity(Scene scene)
+            {
+                AddEntity(scene, Scenes);
+            }
+
+            private void AddEntity(Image image)
+            {
+                AddEntity(image, Images);
+            }
+
+            private void AddEntity(Effect effect)
+            {
+                AddEntity(effect, Effects);
+            }
+
+            private void AddEntity(Material material)
+            {
+                AddEntity(material, Materials);
+            }
+
+            private void AddEntity(Geometry geometry)
+            {
+                AddEntity(geometry, Geometries);
+            }
+
+            private void AddEntity(Animation animation)
+            {
+                AddEntity(animation, Animations);
+            }
+
+            private void AddEntity(Camera camera)
+            {
+                AddEntity(camera, Cameras);
+            }
+
+            private void AddEntity<T>(T entity, ICollection<T> entityCollection) 
+                where T : Entity
+            {
+                if (EnsureId(entity))
+                    entityCollection.Add(entity);
+            }
+
+            private bool EnsureId(Entity entity)
+            {
+                if (entities.ContainsKey(entity))
+                    return false;
+
+                string name = entity.Name;
+                string id;
+
+                if (string.IsNullOrEmpty(name))
+                {
+                    do
+                    {
+                        id = string.Format(CultureInfo.InvariantCulture, 
+                            "unique_{0}", uniqueEntityId++, entity.GetType().Name.ToLowerInvariant());
+                    }
+                    while (ids.ContainsKey(id));
+                }
+                else
+                {
+                    if (!ids.ContainsKey(name))
+                    {
+                        id = name;
+                    }
+                    else
+                    {
+                        id = string.Format(CultureInfo.InvariantCulture,
+                            "{0}_{1}", name, entity.GetType().Name.ToLowerInvariant());
+
+                        while (ids.ContainsKey(id))
+                        {
+                            id = string.Format(CultureInfo.InvariantCulture,
+                                "{0}_{1}_{2}", name, uniqueEntityId++, entity.GetType().Name.ToLowerInvariant());
+                        }
+                    }
+                }
+
+                entities.Add(entity, id);
+                ids.Add(id, entity);
+
+                return true;
+            }
+
+            private bool EnsureId(Entity entity, string id)
+            {
+                if (entities.ContainsKey(entity))
+                    return false;
+
+                entities.Add(entity, id);
+                ids.Add(id, entity);
+
+                return true;
+            }
+
+            public string IdOf(Entity entity)
+            {
+                string id;
+                entities.TryGetValue(entity, out id);
+                return id;
+            }
+
+            public string UrlOf(Entity entity)
+            {
+                return string.Format("#{0}", IdOf(entity));
+            }
+
+            private void AddAnimationChannel(Sampler sampler, Node node, Transform transform, int valueIndex)
+            {
+                Animation animation;
+
+                if (Animations.Count == 0)
+                {
+                    animation = new Animation();
+                    Animations.Add(animation);
+                }
+                else
+                {
+                    animation = Animations[0];
+                }
+
+                EnsureId(sampler);
+
+                string nodeId = IdOf(node);
+                string samplerId = IdOf(sampler);
+                string valueName = transform.ValueIndexToValueName(valueIndex);
+                Sampler valueSampler;
+
+                if (!samplers.TryGetValue(samplerId + valueName, out valueSampler))
+                {
+                    valueSampler = new Sampler();
+                    EnsureId(valueSampler, string.Format("{0}_{1}_{2}", IdOf(node), transform.Sid, valueName));
+                    animation.Samplers.Add(valueSampler);
+
+                    foreach (var input in sampler.Inputs)
+                    {
+                        var source = input.Source;
+                        EnsureId(source);
+
+                        string sourceId = IdOf(source) + (input.Semantic == Semantic.Output ? valueName : "");
+
+                        if (!sources.TryGetValue(sourceId, out source))
+                        {
+                            source = input.Source;
+
+                            switch (input.Semantic)
+                            {
+                                case Semantic.Input:
+                                    source = new AnimationSource(source.FloatData, new[] { "TIME" });
+                                    break;
+                                case Semantic.Output:
+                                    source = new AnimationSource(source.FloatData, new[] { valueName });
+                                    break;
+                                case Semantic.Interpolation:
+                                    source = new AnimationSource(source.NameData, new[] { "INTERPOLATION" });
+                                    break;
+                                case Semantic.OutTangent:
+                                case Semantic.InTangent:
+                                    source = new AnimationSource(source.FloatData, new[] { "X", "Y" });
+                                    break;
+                                default:
+                                    throw new NotSupportedException(string.Format("Invalid semantic {0} for animation input", input.Semantic));
+                            }
+
+                            sources.Add(sourceId, source);
+
+                            EnsureId(source, string.Format(CultureInfo.InvariantCulture, "{0}_{1}", IdOf(valueSampler), input.Semantic.ToString().ToLowerInvariant()));
+                            animation.Sources.Add(source);
+                        }
+
+                        valueSampler.Inputs.Add(new Input(input.Semantic, source));
+                    }
+                }
+
+                animation.Channels.Add(new AnimationChannel(
+                    valueSampler,
+                    string.Format(CultureInfo.InvariantCulture, "{0}/{1}.{2}", IdOf(node), transform.Sid, valueName)));
+            }
+        }
+
+        #endregion
+
+        public static void WriteFile(string filePath, Scene scene)
+        {
+            var writer = new DaeWriter();
+            writer.visitor = new WriteVisitor();
+            writer.visitor.VisitScene(scene);
+            writer.mainScene = scene;
+
+            var settings = new XmlWriterSettings {
+                CloseOutput = true,
+                ConformanceLevel = ConformanceLevel.Document,
+                Encoding = Encoding.UTF8,
+                Indent = true,
+                IndentChars = "\t"
+            };
+
+            //AxisConverter.Convert(scene, Axis.Y, Axis.Z);
+
+            using (var stream = File.Create(filePath))
+            using (writer.xml = XmlWriter.Create(stream, settings))
+                writer.WriteRoot();
+        }
+
+        private void WriteRoot()
+        {
+            WriteCollada();
+
+            WriteLibrary("library_cameras", visitor.Cameras, WriteCamera);
+            WriteLibrary("library_images", visitor.Images, WriteImage);
+            WriteLibrary("library_effects", visitor.Effects, WriteEffect);
+            WriteLibrary("library_materials", visitor.Materials, WriteMaterial);
+            WriteLibrary("library_geometries", visitor.Geometries, WriteGeometry);
+            WriteLibrary("library_visual_scenes", visitor.Scenes, WriteScene);
+            WriteLibrary("library_animations", visitor.Animations, WriteAnimation);
+
+            WriteScene();
+        }
+
+        private void WriteCollada()
+        {
+            xml.WriteStartDocument();
+            xml.WriteStartElement("COLLADA", "http://www.collada.org/2005/11/COLLADASchema");
+            xml.WriteAttributeString("version", "1.4.0");
+
+            xml.WriteStartElement("asset");
+            xml.WriteStartElement("contributor");
+            //xml.WriteElementString("author", "OniSplit");
+            xml.WriteElementString("authoring_tool", string.Format(CultureInfo.InvariantCulture, "OniSplit v{0}", Utils.Version));
+            xml.WriteEndElement();
+
+            xml.WriteStartElement("unit");
+            xml.WriteAttributeString("meter", "0.1");
+            xml.WriteAttributeString("name", "decimeter");
+            xml.WriteEndElement();
+            xml.WriteElementString("up_axis", "Y_UP");
+            xml.WriteEndElement();
+        }
+
+        private void WriteLibrary<T>(string name, ICollection<T> library, Action<T> entityWriter)
+        {
+            if (library.Count == 0)
+                return;
+
+            xml.WriteStartElement(name);
+
+            foreach (T entity in library)
+                entityWriter(entity);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteScene()
+        {
+            xml.WriteStartElement("scene");
+            xml.WriteStartElement("instance_visual_scene");
+            xml.WriteAttributeString("url", visitor.UrlOf(mainScene));
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+        }
+
+        private void WriteImage(Image image)
+        {
+            BeginEntity("image", image);
+
+            string imageUrl;
+
+            if (Path.IsPathRooted(image.FilePath))
+                imageUrl = "file:///" + image.FilePath.Replace('\\', '/');
+            else
+                imageUrl = image.FilePath.Replace('\\', '/');
+
+            xml.WriteElementString("init_from", imageUrl);
+
+            EndEntity();
+        }
+
+        private void WriteEffect(Effect effect)
+        {
+            BeginEntity("effect", effect);
+            WriteEffectCommonProfile(effect);
+            EndEntity();
+        }
+
+        private void WriteEffectCommonProfile(Effect effect)
+        {
+            xml.WriteStartElement("profile_COMMON");
+
+            foreach (var parameter in effect.Parameters)
+                WriteEffectParameter(parameter);
+
+            WriteEffectTechnique(effect);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteEffectParameter(EffectParameter parameter)
+        {
+            xml.WriteStartElement("newparam");
+            xml.WriteAttributeString("sid", parameter.Sid);
+
+            if (!string.IsNullOrEmpty(parameter.Semantic))
+            {
+                xml.WriteStartElement("semantic");
+                xml.WriteString(parameter.Semantic);
+                xml.WriteEndElement();
+            }
+
+            if (parameter.Value is float)
+            {
+                float value = (float)parameter.Value;
+                xml.WriteElementString("float",
+                    XmlConvert.ToString(value));
+            }
+            else if (parameter.Value is Vector2)
+            {
+                var value = (Vector2)parameter.Value;
+                xml.WriteElementString("float2", string.Format("{0} {1}",
+                    XmlConvert.ToString(value.X), XmlConvert.ToString(value.Y)));
+            }
+            else if (parameter.Value is Vector3)
+            {
+                var value = (Vector3)parameter.Value;
+                xml.WriteElementString("float3", string.Format("{0} {1} {3}",
+                    XmlConvert.ToString(value.X), XmlConvert.ToString(value.Y), XmlConvert.ToString(value.Z)));
+            }
+            else if (parameter.Value is EffectSurface)
+            {
+                var surface = (EffectSurface)parameter.Value;
+                xml.WriteStartElement("surface");
+                xml.WriteAttributeString("type", "2D");
+                xml.WriteElementString("init_from", visitor.IdOf(surface.InitFrom));
+                xml.WriteEndElement();
+            }
+            else if (parameter.Value is EffectSampler)
+            {
+                var sampler = (EffectSampler)parameter.Value;
+                xml.WriteStartElement("sampler2D");
+                xml.WriteStartElement("source");
+                xml.WriteString(sampler.Surface.DeclaringParameter.Sid);
+                xml.WriteEndElement();
+
+                if (sampler.MinFilter != EffectSamplerFilter.None)
+                    xml.WriteElementString("minfilter", sampler.MinFilter.ToString().ToUpperInvariant());
+
+                if (sampler.MagFilter != EffectSamplerFilter.None)
+                    xml.WriteElementString("magfilter", sampler.MagFilter.ToString().ToUpperInvariant());
+
+                if (sampler.MipFilter != EffectSamplerFilter.None)
+                    xml.WriteElementString("mipfilter", sampler.MipFilter.ToString().ToUpperInvariant());
+
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteEffectTechnique(Effect effect)
+        {
+            xml.WriteStartElement("technique");
+            xml.WriteStartElement("phong");
+
+            //WriteEffectTechniqueProperty("emission", effect.Emission);
+            
+            WriteEffectTechniqueProperty("ambient", effect.Ambient);
+            WriteEffectTechniqueProperty("diffuse", effect.Diffuse);
+            WriteEffectTechniqueProperty("specular", effect.Specular);
+
+            //WriteEffectTechniqueProperty("shininess", effect.Shininess);
+            //WriteEffectTechniqueProperty("reflective", effect.Reflective);
+            //WriteEffectTechniqueProperty("reflectivity", effect.Reflectivity);
+    
+            WriteEffectTechniqueProperty("transparent", effect.Transparent);
+
+            //WriteEffectTechniqueProperty("transparency", effect.Transparency);
+            //WriteEffectTechniqueProperty("index_of_refraction", effect.IndexOfRefraction);
+
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+        }
+
+        private void WriteEffectTechniqueProperty(string name, EffectParameter value)
+        {
+            bool isTransparent = name == "transparent";
+
+            if (isTransparent && value.Value == null)
+                return;
+
+            xml.WriteStartElement(name);
+
+            if (isTransparent)
+                xml.WriteAttributeString("opaque", "A_ONE");
+
+            if (value.Reference != null)
+            {
+                xml.WriteStartElement("param");
+                xml.WriteString(value.Reference);
+                xml.WriteEndElement();
+            }
+            else if (value.Value is float)
+            {
+                float flt = (float)value.Value;
+                xml.WriteStartElement("float");
+                xml.WriteAttributeString("sid", value.Sid);
+                xml.WriteString(XmlConvert.ToString(flt));
+                xml.WriteEndElement();
+            }
+            else if (value.Value is Vector4)
+            {
+                var color = (Vector4)value.Value;
+                xml.WriteStartElement("color");
+                xml.WriteAttributeString("sid", value.Sid);
+                xml.WriteString(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}",
+                    XmlConvert.ToString(color.X), XmlConvert.ToString(color.Y), XmlConvert.ToString(color.Z), XmlConvert.ToString(color.W)));
+                xml.WriteEndElement();
+            }
+            else if (value.Value is EffectTexture)
+            {
+                var texture = (EffectTexture)value.Value;
+                xml.WriteStartElement("texture");
+                xml.WriteAttributeString("texture", texture.Sampler.Owner.Sid);
+                xml.WriteAttributeString("texcoord", texture.TexCoordSemantic);
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteMaterial(Material matrial)
+        {
+            BeginEntity("material", matrial);
+
+            xml.WriteStartElement("instance_effect");
+            xml.WriteAttributeString("url", visitor.UrlOf(matrial.Effect));
+            xml.WriteEndElement();
+
+            EndEntity();
+        }
+
+        private void WriteGeometry(Geometry geometry)
+        {
+            BeginEntity("geometry", geometry);
+
+            xml.WriteStartElement("mesh");
+
+            WriteGeometrySources(geometry);
+            WriteGeometryVertices(geometry);
+
+            foreach (var primitives in geometry.Primitives)
+                WriteGeometryPrimitives(geometry, primitives);
+
+            xml.WriteEndElement();
+
+            EndEntity();
+        }
+
+        private void WriteGeometrySources(Geometry geometry)
+        {
+            var sources = new Dictionary<Source, List<Semantic>>();
+
+            foreach (var primitives in geometry.Primitives)
+            {
+                foreach (var input in primitives.Inputs)
+                {
+                    List<Semantic> uses;
+
+                    if (!sources.TryGetValue(input.Source, out uses))
+                    {
+                        uses = new List<Semantic>();
+                        sources.Add(input.Source, uses);
+                    }
+
+                    if (!uses.Contains(input.Semantic))
+                        uses.Add(input.Semantic);
+                }
+            }
+
+            foreach (var pair in sources)
+            {
+                foreach (var semantic in pair.Value)
+                    WriteSource(pair.Key, semantic);
+            }
+        }
+
+        private void WriteGeometryVertices(Geometry geometry)
+        {
+            string baseId = visitor.IdOf(geometry);
+
+            if (baseId.EndsWith("_geometry", StringComparison.Ordinal))
+                baseId = baseId.Substring(0, baseId.Length - "_geometry".Length);
+
+            xml.WriteStartElement("vertices");
+            xml.WriteAttributeString("id", baseId + "_vertices");
+
+            foreach (var input in geometry.Vertices)
+            {
+                xml.WriteStartElement("input");
+                WriteSemanticAttribute("semantic", input.Semantic);
+                xml.WriteAttributeString("source", visitor.UrlOf(input.Source));
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteGeometryPrimitives(Geometry geometry, MeshPrimitives primitives)
+        {
+            switch (primitives.PrimitiveType)
+            {
+                case MeshPrimitiveType.Lines:
+                case MeshPrimitiveType.LineStrips:
+                case MeshPrimitiveType.TriangleFans:
+                case MeshPrimitiveType.TriangleStrips:
+                    throw new NotSupportedException(string.Format("Writing {0} is not supported", primitives.PrimitiveType));
+            }
+
+            bool trianglesOnly = !primitives.VertexCounts.Exists(x => x != 3);
+
+            if (!trianglesOnly)
+                xml.WriteStartElement("polylist");
+            else
+                xml.WriteStartElement("triangles");
+
+            xml.WriteAttributeString("count", XmlConvert.ToString(primitives.VertexCounts.Count));
+
+            if (!string.IsNullOrEmpty(primitives.MaterialSymbol))
+                xml.WriteAttributeString("material", primitives.MaterialSymbol);
+
+            int offset = 0;
+            bool vertexInputWritten = false;
+
+            var inputs = new List<IndexedInput>();
+
+            string baseUrl = visitor.UrlOf(geometry);
+
+            if (baseUrl.EndsWith("_geometry", StringComparison.Ordinal))
+                baseUrl = baseUrl.Substring(0, baseUrl.Length - "_geometry".Length);
+
+            foreach (var input in primitives.Inputs)
+            {
+                if (geometry.Vertices.Any(x => x.Source == input.Source))
+                {
+                    if (!vertexInputWritten)
+                    {
+                        inputs.Add(input);
+
+                        xml.WriteStartElement("input");
+                        xml.WriteAttributeString("semantic", "VERTEX");
+                        xml.WriteAttributeString("source", baseUrl + "_vertices");
+                        xml.WriteAttributeString("offset", XmlConvert.ToString(offset++));
+                        xml.WriteEndElement();
+                    }
+
+                    vertexInputWritten = true;
+                }
+                else
+                {
+                    inputs.Add(input);
+
+                    xml.WriteStartElement("input");
+                    WriteSemanticAttribute("semantic", input.Semantic);
+                    xml.WriteAttributeString("source", visitor.UrlOf(input.Source));
+                    xml.WriteAttributeString("offset", XmlConvert.ToString(offset++));
+
+                    if (input.Set != 0)
+                        xml.WriteAttributeString("set", XmlConvert.ToString(input.Set));
+
+                    xml.WriteEndElement();
+                }
+            }
+
+            if (!trianglesOnly)
+            {
+                xml.WriteStartElement("vcount");
+                xml.WriteWhitespace("\n");
+
+                int vertexCount = 0;
+                int c = 0;
+
+                foreach (int i in primitives.VertexCounts)
+                {
+                    xml.WriteString(XmlConvert.ToString(i) + " ");
+                    vertexCount += i;
+
+                    c++;
+
+                    if (c == 32)
+                    {
+                        xml.WriteWhitespace("\n");
+                        c = 0;
+                    }
+                }
+
+                xml.WriteEndElement();
+            }
+
+            xml.WriteStartElement("p");
+            xml.WriteWhitespace("\n");
+
+            int polygonStartIndex = 0;
+
+            foreach (int vertexCount in primitives.VertexCounts)
+            {
+                for (int index = 0; index < vertexCount; index++)
+                {
+                    foreach (var input in inputs)
+                    {
+                        xml.WriteString(XmlConvert.ToString(input.Indices[polygonStartIndex + index]));
+
+                        if (input != inputs.Last() || index != vertexCount - 1)
+                            xml.WriteWhitespace(" ");
+                    }
+                }
+
+                xml.WriteWhitespace("\n");
+                polygonStartIndex += vertexCount;
+            }
+
+            xml.WriteEndElement();
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteScene(Scene scene)
+        {
+            BeginEntity("visual_scene", scene);
+
+            foreach (var node in scene.Nodes)
+                WriteSceneNode(node);
+
+            EndEntity();
+        }
+
+        private void WriteSceneNode(Node node)
+        {
+            BeginEntity("node", node);
+
+            foreach (var transform in node.Transforms)
+                WriteNodeTransform(transform);
+
+            foreach (var instance in node.Instances)
+            {
+                if (instance is GeometryInstance)
+                    WriteGeometryInstance((GeometryInstance)instance);
+                else if (instance is CameraInstance)
+                    WriteCameraInstance((CameraInstance)instance);
+            }
+
+            foreach (var child in node.Nodes)
+                WriteSceneNode(child);
+
+            EndEntity();
+        }
+
+        private void WriteNodeTransform(Transform transform)
+        {
+            string type;
+
+            if (transform is TransformTranslate)
+                type = "translate";
+            else if (transform is TransformRotate)
+                type = "rotate";
+            else if (transform is TransformScale)
+                type = "scale";
+            else
+                type = "matrix";
+
+            xml.WriteStartElement(type);
+
+            if (!string.IsNullOrEmpty(transform.Sid))
+                xml.WriteAttributeString("sid", transform.Sid);
+
+            var values = new StringBuilder(transform.Values.Length * 16);
+
+            foreach (float value in transform.Values)
+                values.AppendFormat(CultureInfo.InvariantCulture, "{0:f6} ", value);
+
+            if (values.Length > 0)
+                values.Length--;
+
+            xml.WriteValue(values.ToString());
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteCameraInstance(CameraInstance instance)
+        {
+            xml.WriteStartElement("instance_camera");
+            xml.WriteAttributeString("url", visitor.UrlOf(instance.Target));
+            xml.WriteEndElement();
+        }
+
+        private void WriteGeometryInstance(GeometryInstance instance)
+        {
+            xml.WriteStartElement("instance_geometry");
+            xml.WriteAttributeString("url", visitor.UrlOf(instance.Target));
+
+            if (instance.Materials.Count > 0)
+            {
+                xml.WriteStartElement("bind_material");
+                xml.WriteStartElement("technique_common");
+
+                foreach (var matInstance in instance.Materials)
+                    WriteMaterialInstance(matInstance);
+
+                xml.WriteEndElement();
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteMaterialInstance(MaterialInstance matInstance)
+        {
+            xml.WriteStartElement("instance_material");
+            xml.WriteAttributeString("symbol", matInstance.Symbol);
+            xml.WriteAttributeString("target", visitor.UrlOf(matInstance.Target));
+
+            foreach (var binding in matInstance.Bindings)
+            {
+                xml.WriteStartElement("bind_vertex_input");
+                xml.WriteAttributeString("semantic", binding.Semantic);
+                WriteSemanticAttribute("input_semantic", binding.VertexInput.Semantic);
+                xml.WriteAttributeString("input_set", XmlConvert.ToString(binding.VertexInput.Set));
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteAnimation(Animation animation)
+        {
+            BeginEntity("animation", animation);
+
+            foreach (var source in animation.Sources)
+                WriteSource(source, Semantic.None);
+
+            foreach (var sampler in animation.Samplers)
+                WriteAnimationSampler(animation, sampler);
+
+            foreach (var channel in animation.Channels)
+                WriteAnimationChannel(channel);
+
+            EndEntity();
+        }
+
+        private void WriteAnimationSampler(Animation animation, Sampler sampler)
+        {
+            xml.WriteStartElement("sampler");
+            xml.WriteAttributeString("id", visitor.IdOf(sampler));
+
+            foreach (var input in sampler.Inputs)
+            {
+                xml.WriteStartElement("input");
+                WriteSemanticAttribute("semantic", input.Semantic);
+                xml.WriteAttributeString("source", visitor.UrlOf(input.Source));
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteAnimationChannel(AnimationChannel channel)
+        {
+            xml.WriteStartElement("channel");
+            xml.WriteAttributeString("source", visitor.UrlOf(channel.Sampler));
+            xml.WriteAttributeString("target", channel.TargetPath);
+            xml.WriteEndElement();
+        }
+
+        private void BeginEntity(string name, Entity entity)
+        {
+            xml.WriteStartElement(name);
+
+            string id = visitor.IdOf(entity);
+
+            if (!string.IsNullOrEmpty(id))
+                xml.WriteAttributeString("id", id);
+
+            //if (!String.IsNullOrEmpty(entity.Name))
+            //    xml.WriteAttributeString("name", entity.Name);
+        }
+
+        private void EndEntity()
+        {
+            xml.WriteEndElement();
+        }
+
+        private void WriteSource(Source source, Semantic semantic)
+        {
+            if (writtenSources.ContainsKey(source))
+                return;
+
+            string sourceId = visitor.IdOf(source);
+
+            writtenSources.Add(source, sourceId);
+
+            var animationSource = source as AnimationSource;
+
+            if (animationSource != null)
+            {
+                if (source.FloatData != null)
+                    WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, animationSource.Parameters);
+                else
+                    WriteSource(sourceId, source.NameData, x => x, source.Stride, animationSource.Parameters);
+
+                return;
+            }
+
+            switch (semantic)
+            {
+                case Semantic.Position:
+                case Semantic.Normal:
+                    WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, new[] { "X", "Y", "Z" });
+                    break;
+
+                case Semantic.TexCoord:
+                    WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, new[] { "S", "T" });
+                    break;
+
+                case Semantic.Color:
+                    if (source.Stride == 4)
+                        WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, new[] { "R", "G", "B", "A" });
+                    else
+                        WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, new[] { "R", "G", "B" });
+                    break;
+
+                case Semantic.Input:
+                    WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, new[] { "TIME" });
+                    return;
+
+                case Semantic.Output:
+                    WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, new[] { "VALUE" });
+                    return;
+
+                case Semantic.Interpolation:
+                    WriteSource(sourceId, source.NameData, x => x, source.Stride, new[] { "INTERPOLATION" });
+                    return;
+
+                case Semantic.InTangent:
+                case Semantic.OutTangent:
+                    WriteSource(sourceId, source.FloatData, XmlConvert.ToString, source.Stride, new[] { "X", "Y" });
+                    break;
+
+                default:
+                    throw new NotSupportedException(string.Format("Sources with semantic {0} are not supported", semantic));
+            }
+        }
+
+        private void WriteSource<T>(string sourceId, T[] data, Func<T, string> toString, int stride, string[] paramNames)
+        {
+            string arrayId = sourceId + "_array";
+            string type = null;
+
+            if (typeof(T) == typeof(float))
+                type = "float";
+            else if (typeof(T) == typeof(string))
+                type = "Name";
+
+            xml.WriteStartElement("source");
+            xml.WriteAttributeString("id", sourceId);
+
+            xml.WriteStartElement(type + "_array");
+            xml.WriteAttributeString("id", arrayId);
+            xml.WriteAttributeString("count", XmlConvert.ToString(data.Length));
+            xml.WriteWhitespace("\n");
+
+            int valuesPerLine = (stride == 1) ? 10 : stride;
+
+            for (int i = 0; i < data.Length; i++)
+            {
+                xml.WriteString(toString(data[i]));
+
+                if (i != data.Length - 1)
+                {
+                    if (i % valuesPerLine == valuesPerLine - 1)
+                        xml.WriteWhitespace("\n");
+                    else
+                        xml.WriteWhitespace(" ");
+                }
+            }
+
+            xml.WriteEndElement();
+
+            xml.WriteStartElement("technique_common");
+            WriteSourceAccessor<T>(arrayId, data.Length / stride, stride, type, paramNames);
+            xml.WriteEndElement();
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteSourceAccessor<T>(string arrayId, int count, int stride, string type, string[] paramNames)
+        {
+            xml.WriteStartElement("accessor");
+            xml.WriteAttributeString("source", "#" + arrayId);
+            xml.WriteAttributeString("count", XmlConvert.ToString(count));
+            xml.WriteAttributeString("stride", XmlConvert.ToString(stride));
+
+            for (int i = 0; i < stride; i++)
+            {
+                xml.WriteStartElement("param");
+                xml.WriteAttributeString("type", type);
+                xml.WriteAttributeString("name", paramNames[i]);
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteSemanticAttribute(string name, Semantic semantic)
+        {
+            xml.WriteAttributeString(name, semantic.ToString().ToUpperInvariant());
+        }
+
+        private void WriteCamera(Camera camera)
+        {
+            BeginEntity("camera", camera);
+
+            xml.WriteStartElement("optics");
+            xml.WriteStartElement("technique_common");
+            xml.WriteStartElement("perspective");
+            xml.WriteElementString("xfov", XmlConvert.ToString(camera.XFov));
+            xml.WriteElementString("aspect_ratio", XmlConvert.ToString(camera.AspectRatio));
+            xml.WriteElementString("znear", XmlConvert.ToString(camera.ZNear));
+            xml.WriteElementString("zfar", XmlConvert.ToString(camera.ZFar));
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+
+            EndEntity();
+        }
+    }
+}
Index: /OniSplit/Dae/IO/ObjReader.cs
===================================================================
--- /OniSplit/Dae/IO/ObjReader.cs	(revision 1114)
+++ /OniSplit/Dae/IO/ObjReader.cs	(revision 1114)
@@ -0,0 +1,552 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+
+namespace Oni.Dae.IO
+{
+    internal class ObjReader
+    {
+        private struct ObjVertex : IEquatable<ObjVertex>
+        {
+            public int PointIndex;
+            public int TexCoordIndex;
+            public int NormalIndex;
+
+            public ObjVertex(int pointIndex, int uvIndex, int normalIndex)
+            {
+                PointIndex = pointIndex;
+                TexCoordIndex = uvIndex;
+                NormalIndex = normalIndex;
+            }
+
+            public static bool operator ==(ObjVertex v1, ObjVertex v2) =>
+                    v1.PointIndex == v2.PointIndex
+                && v1.TexCoordIndex == v2.TexCoordIndex
+                && v1.NormalIndex == v2.NormalIndex;
+
+            public static bool operator !=(ObjVertex v1, ObjVertex v2) =>
+                    v1.PointIndex != v2.PointIndex
+                || v1.TexCoordIndex != v2.TexCoordIndex
+                || v1.NormalIndex != v2.NormalIndex;
+
+            public bool Equals(ObjVertex v) => this == v;
+            public override bool Equals(object obj) => obj is ObjVertex && Equals((ObjVertex)obj);
+            public override int GetHashCode() => PointIndex ^ TexCoordIndex ^ NormalIndex;
+        }
+
+        private class ObjFace
+        {
+            public string ObjectName;
+            public string[] GroupsNames;
+            public ObjVertex[] Vertices;
+        }
+
+        private class ObjMaterial
+        {
+            private readonly string name;
+            private string textureFilePath;
+            private Material material;
+
+            public ObjMaterial(string name)
+            {
+                this.name = name;
+            }
+
+            public string Name => name;
+
+            public string TextureFilePath
+            {
+                get { return textureFilePath; }
+                set { textureFilePath = value; }
+            }
+
+            public Material Material
+            {
+                get
+                {
+                    if (material == null && TextureFilePath != null)
+                        CreateMaterial();
+
+                    return material;
+                }
+            }
+
+            private void CreateMaterial()
+            {
+                var image = new Image
+                {
+                    FilePath = TextureFilePath,
+                    Name = name + "_img"
+                };
+
+                var effectSurface = new EffectSurface(image);
+                var effectSampler = new EffectSampler(effectSurface);
+                var effectTexture = new EffectTexture
+                {
+                    Sampler = effectSampler,
+                    Channel = EffectTextureChannel.Diffuse,
+                    TexCoordSemantic = "diffuse_TEXCOORD"
+                };
+
+                material = new Material
+                {
+                    Id = name,
+                    Name = name,
+                    Effect = new Effect
+                    {
+                        Id = name + "_fx",
+                        DiffuseValue = effectTexture,
+                        Parameters = {
+                            new EffectParameter("surface", effectSurface),
+                            new EffectParameter("sampler", effectSampler)
+                        }
+                    }
+                };
+            }
+        }
+
+        private class ObjPrimitives
+        {
+            public ObjMaterial Material;
+            public readonly List<ObjFace> Faces = new List<ObjFace>(4);
+        }
+
+        #region Private data
+        private static readonly string[] emptyStrings = new string[0];
+        private static readonly char[] whiteSpaceChars = new char[] { ' ', '\t' };
+        private static readonly char[] vertexSeparator = new char[] { '/' };
+
+        private Scene mainScene;
+
+        private readonly List<Vector3> points = new List<Vector3>();
+        private readonly List<Vector2> texCoords = new List<Vector2>();
+        private readonly List<Vector3> normals = new List<Vector3>();
+
+        private int pointCount;
+        private int normalCount;
+        private int texCoordCount;
+
+        private readonly Dictionary<Vector3, int> pointIndex = new Dictionary<Vector3, int>();
+        private readonly Dictionary<Vector3, int> normalIndex = new Dictionary<Vector3, int>();
+        private readonly Dictionary<Vector2, int> texCoordIndex = new Dictionary<Vector2, int>();
+        private readonly List<int> pointRemap = new List<int>();
+        private readonly List<int> normalRemap = new List<int>();
+        private readonly List<int> texCoordRemap = new List<int>();
+
+        private readonly Dictionary<string, ObjMaterial> materials = new Dictionary<string, ObjMaterial>(StringComparer.Ordinal);
+
+        private string currentObjectName;
+        private string[] currentGroupNames;
+        private readonly List<ObjPrimitives> primitives = new List<ObjPrimitives>();
+        private ObjPrimitives currentPrimitives;
+        #endregion
+
+        public static Scene ReadFile(string filePath)
+        {
+            var reader = new ObjReader();
+            reader.ReadObjFile(filePath);
+            reader.ImportObjects();
+            return reader.mainScene;
+        }
+
+        private void ReadObjFile(string filePath)
+        {
+            mainScene = new Scene();
+
+            foreach (string line in ReadLines(filePath))
+            {
+                var tokens = line.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries);
+
+                switch (tokens[0])
+                {
+                    case "o":
+                        ReadObject(tokens);
+                        break;
+
+                    case "g":
+                        ReadGroup(tokens);
+                        break;
+
+                    case "v":
+                        ReadPoint(tokens);
+                        break;
+
+                    case "vn":
+                        ReadNormal(tokens);
+                        break;
+
+                    case "vt":
+                        ReadTexCoord(tokens);
+                        break;
+
+                    case "f":
+                    case "fo":
+                        ReadFace(tokens);
+                        break;
+
+                    case "mtllib":
+                        ReadMtlLib(filePath, tokens);
+                        break;
+
+                    case "usemtl":
+                        ReadUseMtl(tokens);
+                        break;
+                }
+            }
+        }
+
+        private void ReadPoint(string[] tokens)
+        {
+            var point = new Vector3(
+                float.Parse(tokens[1], CultureInfo.InvariantCulture),
+                float.Parse(tokens[2], CultureInfo.InvariantCulture),
+                float.Parse(tokens[3], CultureInfo.InvariantCulture));
+
+            AddPoint(point);
+
+            pointCount++;
+        }
+
+        private void AddPoint(Vector3 point)
+        {
+            int newIndex;
+
+            if (pointIndex.TryGetValue(point, out newIndex))
+            {
+                pointRemap.Add(newIndex);
+            }
+            else
+            {
+                pointRemap.Add(points.Count);
+                pointIndex.Add(point, points.Count);
+                points.Add(point);
+            }
+        }
+
+        private void ReadNormal(string[] tokens)
+        {
+            var normal = new Vector3(
+                float.Parse(tokens[1], CultureInfo.InvariantCulture),
+                float.Parse(tokens[2], CultureInfo.InvariantCulture),
+                float.Parse(tokens[3], CultureInfo.InvariantCulture));
+
+            AddNormal(normal);
+
+            normalCount++;
+        }
+
+        private void AddNormal(Vector3 normal)
+        {
+            int newIndex;
+
+            if (normalIndex.TryGetValue(normal, out newIndex))
+            {
+                normalRemap.Add(newIndex);
+            }
+            else
+            {
+                normalRemap.Add(normals.Count);
+                normalIndex.Add(normal, normals.Count);
+                normals.Add(normal);
+            }
+        }
+
+        private void ReadTexCoord(string[] tokens)
+        {
+            var texCoord = new Vector2(
+                float.Parse(tokens[1], CultureInfo.InvariantCulture),
+                1.0f - float.Parse(tokens[2], CultureInfo.InvariantCulture));
+
+            AddTexCoord(texCoord);
+
+            texCoordCount++;
+        }
+
+        private void AddTexCoord(Vector2 texCoord)
+        {
+            int newIndex;
+
+            if (texCoordIndex.TryGetValue(texCoord, out newIndex))
+            {
+                texCoordRemap.Add(newIndex);
+            }
+            else
+            {
+                texCoordRemap.Add(texCoords.Count);
+                texCoordIndex.Add(texCoord, texCoords.Count);
+                texCoords.Add(texCoord);
+            }
+        }
+
+        private void ReadFace(string[] tokens)
+        {
+            var faceVertices = ReadVertices(tokens);
+
+            if (currentPrimitives == null)
+                ReadUseMtl(emptyStrings);
+
+            currentPrimitives.Faces.Add(new ObjFace
+            {
+                ObjectName = currentObjectName,
+                GroupsNames = currentGroupNames,
+                Vertices = faceVertices
+            });
+        }
+
+        private ObjVertex[] ReadVertices(string[] tokens)
+        {
+            var vertices = new ObjVertex[tokens.Length - 1];
+
+            for (int i = 0; i < vertices.Length; i++)
+            {
+                //
+                // Read a point/texture/normal index pair
+                //
+
+                var indices = tokens[i + 1].Split(vertexSeparator);
+
+                if (indices.Length == 0 || indices.Length > 3)
+                    throw new InvalidDataException();
+
+                //
+                // Extract indices from file: 0 means "not specified"
+                //
+
+                int pointIndex = int.Parse(indices[0], CultureInfo.InvariantCulture);
+                int texCoordIndex = (indices.Length > 1 && indices[1].Length > 0) ? int.Parse(indices[1], CultureInfo.InvariantCulture) : 0;
+                int normalIndex = (indices.Length > 2 && indices[2].Length > 0) ? int.Parse(indices[2], CultureInfo.InvariantCulture) : 0;
+
+                //
+                // Adjust for negative indices
+                //
+
+                if (pointIndex < 0)
+                    pointIndex = pointCount + pointIndex + 1;
+
+                if (texCoordIndex < 0)
+                    texCoordIndex = texCoordCount + texCoordIndex + 1;
+
+                if (normalIndex < 0)
+                    normalIndex = normalCount + normalIndex + 1;
+
+                //
+                // Convert indices to internal representation: range 0..n and -1 means "not specified".
+                //
+
+                pointIndex = pointIndex - 1;
+                texCoordIndex = texCoordIndex - 1;
+                normalIndex = normalIndex - 1;
+
+                //
+                // Remap indices
+                //
+
+                pointIndex = pointRemap[pointIndex];
+
+                if (texCoordIndex < 0 || texCoordRemap.Count <= texCoordIndex)
+                    texCoordIndex = -1;
+                else
+                    texCoordIndex = texCoordRemap[texCoordIndex];
+
+                if (normalIndex < 0 || normalRemap.Count <= normalIndex)
+                    normalIndex = -1;
+                else
+                    normalIndex = normalRemap[normalIndex];
+
+                vertices[i] = new ObjVertex
+                {
+                    PointIndex = pointIndex,
+                    TexCoordIndex = texCoordIndex,
+                    NormalIndex = normalIndex
+                };
+            }
+
+            return vertices;
+        }
+
+        private void ReadObject(string[] tokens)
+        {
+            currentObjectName = tokens[1];
+        }
+
+        private void ReadGroup(string[] tokens)
+        {
+            currentGroupNames = tokens;
+        }
+
+        private void ReadUseMtl(string[] tokens)
+        {
+            currentPrimitives = new ObjPrimitives();
+
+            if (tokens.Length > 0)
+                materials.TryGetValue(tokens[1], out currentPrimitives.Material);
+
+            primitives.Add(currentPrimitives);
+        }
+
+        private void ReadMtlLib(string objFilePath, string[] tokens)
+        {
+            string materialLibraryFilePath = tokens[1];
+
+            if (Path.GetExtension(materialLibraryFilePath).Length == 0)
+                materialLibraryFilePath += ".mtl";
+
+            var dirPath = Path.GetDirectoryName(objFilePath);
+            var mtlFilePath = Path.Combine(dirPath, materialLibraryFilePath);
+
+            if (!File.Exists(mtlFilePath))
+            {
+                Console.Error.WriteLine("Material file {0} does not exist", mtlFilePath);
+                return;
+            }
+
+            ReadMtlFile(mtlFilePath);
+        }
+
+        private void ReadMtlFile(string filePath)
+        {
+            var dirPath = Path.GetDirectoryName(filePath);
+
+            ObjMaterial currentMaterial = null;
+
+            foreach (string line in ReadLines(filePath))
+            {
+                var tokens = line.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries);
+
+                switch (tokens[0])
+                {
+                    case "newmtl":
+                        currentMaterial = new ObjMaterial(tokens[1]);
+                        materials[currentMaterial.Name] = currentMaterial;
+                        break;
+
+                    case "map_Kd":
+                        string textureFilePath = Path.GetFullPath(Path.Combine(dirPath, tokens[1]));
+
+                        if (File.Exists(textureFilePath))
+                            currentMaterial.TextureFilePath = textureFilePath;
+
+                        break;
+                }
+            }
+        }
+
+        private void ImportObjects()
+        {
+            var inputs = new List<IndexedInput>();
+            IndexedInput positionInput, texCoordInput, normalInput;
+
+            positionInput = new IndexedInput(Semantic.Position, new Source(points));
+            inputs.Add(positionInput);
+
+            if (texCoords.Count > 0)
+            {
+                texCoordInput = new IndexedInput(Semantic.TexCoord, new Source(texCoords));
+                inputs.Add(texCoordInput);
+            }
+            else
+            {
+                texCoordInput = null;
+            }
+
+            if (normals.Count > 0)
+            {
+                normalInput = new IndexedInput(Semantic.Normal, new Source(normals));
+                inputs.Add(normalInput);
+            }
+            else
+            {
+                normalInput = null;
+            }
+
+            var geometry = new Geometry
+            {
+                Vertices = { positionInput }
+            };
+
+            var geometryInstance = new GeometryInstance
+            {
+                Target = geometry
+            };
+
+            foreach (var primitive in primitives.Where(p => p.Faces.Count > 0))
+            {
+                var meshPrimtives = new MeshPrimitives(MeshPrimitiveType.Polygons, inputs);
+
+                foreach (var face in primitive.Faces)
+                {
+                    meshPrimtives.VertexCounts.Add(face.Vertices.Length);
+
+                    foreach (var vertex in face.Vertices)
+                    {
+                        positionInput.Indices.Add(vertex.PointIndex);
+
+                        if (texCoordInput != null)
+                            texCoordInput.Indices.Add(vertex.TexCoordIndex);
+
+                        if (normalInput != null)
+                            normalInput.Indices.Add(vertex.NormalIndex);
+                    }
+                }
+
+                geometry.Primitives.Add(meshPrimtives);
+
+                if (primitive.Material != null && primitive.Material.Material != null)
+                {
+                    meshPrimtives.MaterialSymbol = "mat" + geometryInstance.Materials.Count;
+
+                    geometryInstance.Materials.Add(new MaterialInstance
+                    {
+                        Symbol = meshPrimtives.MaterialSymbol,
+                        Target = primitive.Material.Material,
+                        Bindings = { new MaterialBinding("diffuse_TEXCOORD", texCoordInput) }
+                    });
+                }
+            }
+
+            mainScene.Nodes.Add(new Node
+            {
+                Instances = { geometryInstance }
+            });
+        }
+
+        private Vector3 ComputeFaceNormal(ObjVertex[] vertices)
+        {
+            if (vertices.Length < 3)
+                return Vector3.Up;
+
+            var v1 = points[vertices[0].PointIndex];
+            var v2 = points[vertices[1].PointIndex];
+            var v3 = points[vertices[2].PointIndex];
+
+            return Vector3.Normalize(Vector3.Cross(v2 - v1, v3 - v1));
+        }
+
+        private static IEnumerable<string> ReadLines(string filePath)
+        {
+            using (var reader = File.OpenText(filePath))
+            {
+                for (var line = reader.ReadLine(); line != null; line = reader.ReadLine())
+                {
+                    line = line.Trim();
+
+                    if (line.Length == 0)
+                        continue;
+
+                    int commentStart = line.IndexOf('#');
+
+                    if (commentStart != -1)
+                    {
+                        line = line.Substring(0, commentStart).Trim();
+
+                        if (line.Length == 0)
+                            continue;
+                    }
+
+                    yield return line;
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Dae/IO/ObjWriter.cs
===================================================================
--- /OniSplit/Dae/IO/ObjWriter.cs	(revision 1114)
+++ /OniSplit/Dae/IO/ObjWriter.cs	(revision 1114)
@@ -0,0 +1,260 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using Oni.Collections;
+
+namespace Oni.Dae.IO
+{
+    internal class ObjWriter
+    {
+        private int vBase = 1, vtBase = 1, vnBase = 1;
+        private Set<Material> materials = new Set<Material>();
+        private StreamWriter objWriter;
+        private StreamWriter mtlWriter;
+
+        internal static void WriteFile(string filePath, Scene scene)
+        {
+            var writer = new ObjWriter();
+            writer.Write(filePath, scene);
+        }
+
+        private void Write(string filePath, Scene scene)
+        {
+            using (objWriter = File.CreateText(filePath))
+            {
+                objWriter.WriteLine("# Generated by OniSplit v{0}", Utils.Version);
+                objWriter.WriteLine();
+
+                objWriter.WriteLine("mtllib {0}", Path.ChangeExtension(Path.GetFileName(filePath), ".mtl"));
+                objWriter.WriteLine();
+
+                var rootTransform = Matrix.Identity;
+
+                WriteNode(scene, ref rootTransform);
+            }
+
+            using (mtlWriter = File.CreateText(Path.ChangeExtension(filePath, ".mtl")))
+            {
+                mtlWriter.WriteLine("# Generated by OniSplit v{0}", Utils.Version);
+                mtlWriter.WriteLine();
+
+                WriteMaterialLibrary();
+            }
+        }
+
+        private void WriteNode(Node node, ref Matrix parentTransform)
+        {
+            if (!string.IsNullOrEmpty(node.Name))
+                objWriter.WriteLine("g {0}", node.Name);
+
+            var transform = node.Transforms.ToMatrix() * parentTransform;
+
+            foreach (var geometryInstance in node.GeometryInstances)
+                WriteGeometry(geometryInstance, ref transform);
+
+            foreach (var child in node.Nodes)
+                WriteNode(child, ref transform);
+        }
+
+        private void WriteGeometry(GeometryInstance geometryInstance, ref Matrix transform)
+        {
+            var geometry = geometryInstance.Target;
+
+            foreach (var primitives in geometry.Primitives)
+            {
+                var posInput = WritePositions(primitives, ref transform);
+                var texCoordInput = WriteTexCoords(primitives);
+                var normalInput = WriteNormals(primitives, ref transform);
+
+                WriteUseMaterial(geometryInstance, primitives);
+                WriteFaces(primitives, posInput, texCoordInput, normalInput);
+
+                vBase += posInput.Source.Count;
+                vtBase += texCoordInput == null ? 0 : texCoordInput.Source.Count;
+                vnBase += normalInput == null ? 0: normalInput.Source.Count;
+            }
+        }
+
+        private IndexedInput WritePositions(MeshPrimitives primitives, ref Matrix transform)
+        {
+            var input = primitives.Inputs.FirstOrDefault(i => i.Semantic == Semantic.Position);
+            var source = input.Source;
+
+            for (int i = 0; i < source.Count; i++)
+            {
+                var position = Vector3.Transform(Source.ReadVector3(source, i), ref transform);
+
+                objWriter.Write("v ");
+                objWriter.Write(position.X.ToString(CultureInfo.InvariantCulture));
+                objWriter.Write(' ');
+                objWriter.Write(position.Y.ToString(CultureInfo.InvariantCulture));
+                objWriter.Write(' ');
+                objWriter.Write(position.Z.ToString(CultureInfo.InvariantCulture));
+                objWriter.WriteLine();
+            }
+
+            objWriter.WriteLine();
+
+            return input;
+        }
+
+        private IndexedInput WriteTexCoords(MeshPrimitives primitives)
+        {
+            var input = primitives.Inputs.FirstOrDefault(i => i.Semantic == Semantic.TexCoord);
+
+            if (input == null)
+                return null;
+
+            var source = input.Source;
+            var data = source.FloatData;
+
+            for (int i = 0; i < data.Length; i += source.Stride)
+            {
+                objWriter.Write("vt ");
+                objWriter.Write(data[i + 0].ToString(CultureInfo.InvariantCulture));
+                objWriter.Write(' ');
+                objWriter.Write(data[i + 1].ToString(CultureInfo.InvariantCulture));
+                objWriter.WriteLine();
+            }
+
+            objWriter.WriteLine();
+
+            return input;
+        }
+
+        private IndexedInput WriteNormals(MeshPrimitives primitives, ref Matrix transform)
+        {
+            var input = primitives.Inputs.FirstOrDefault(i => i.Semantic == Semantic.Normal);
+
+            if (input == null)
+                return null;
+
+            var source = input.Source;
+
+            for (int i = 0; i < source.Count; i++)
+            {
+                var normal = Vector3.TransformNormal(Source.ReadVector3(source, i), ref transform);
+
+                objWriter.Write("vn ");
+                objWriter.Write(normal.X.ToString(CultureInfo.InvariantCulture));
+                objWriter.Write(' ');
+                objWriter.Write(normal.Y.ToString(CultureInfo.InvariantCulture));
+                objWriter.Write(' ');
+                objWriter.Write(normal.Z.ToString(CultureInfo.InvariantCulture));
+                objWriter.WriteLine();
+            }
+
+            objWriter.WriteLine();
+
+            return input;
+        }
+
+        private void WriteFaces(MeshPrimitives primitives, IndexedInput posInput, IndexedInput texCoordInput, IndexedInput normalInput)
+        {
+            var positionIndices = posInput.Indices;
+            var texCoordIndices = texCoordInput == null ? null : texCoordInput.Indices;
+            var normalIndices = normalInput == null ? null : normalInput.Indices;
+            int vertexIndex = 0;
+
+            foreach (var vertexCount in primitives.VertexCounts)
+            {
+                objWriter.Write("f");
+
+                for (int i = vertexIndex; i < vertexIndex + vertexCount; i++)
+                {
+                    objWriter.Write(' ');
+                    objWriter.Write((vBase + positionIndices[i]).ToString(CultureInfo.InvariantCulture));
+
+                    if (texCoordIndices != null)
+                    {
+                        objWriter.Write('/');
+                        objWriter.Write((vtBase + texCoordIndices[i]).ToString(CultureInfo.InvariantCulture));
+                    }
+                    else if (normalIndices != null)
+                    {
+                        objWriter.Write('/');
+                    }
+
+                    if (normalIndices != null)
+                    {
+                        objWriter.Write('/');
+                        objWriter.Write((vnBase + normalIndices[i]).ToString(CultureInfo.InvariantCulture));
+                    }
+                }
+
+                objWriter.WriteLine();
+
+                vertexIndex += vertexCount;
+            }
+
+            objWriter.WriteLine();
+        }
+
+        private void WriteUseMaterial(GeometryInstance geometryInstance, MeshPrimitives primitives)
+        {
+            if (string.IsNullOrEmpty(primitives.MaterialSymbol))
+            {
+                objWriter.WriteLine("usemtl");
+            }
+            else
+            {
+                var materialInstance = geometryInstance.Materials.Find(m => m.Symbol == primitives.MaterialSymbol);
+
+                if (materialInstance != null && materialInstance.Target != null)
+                {
+                    objWriter.WriteLine("usemtl {0}", materialInstance.Target.Name);
+                    materials.Add(materialInstance.Target);
+                }
+            }
+        }
+
+        private void WriteMaterialLibrary()
+        {
+            foreach (var material in materials)
+            {
+                mtlWriter.WriteLine("newmtl {0}", material.Name);
+
+                var effect = material.Effect;
+
+                WriteMaterialColor("Ka", effect.Ambient);
+                WriteMaterialColor("Kd", effect.Diffuse);
+
+                mtlWriter.WriteLine("Ks 0 0 0");
+                mtlWriter.WriteLine("Ns 0");
+
+                WriteMaterialTextureMap("map_Kd", effect.Diffuse);
+                WriteMaterialTextureMap("map_Tr", effect.Transparent);
+
+                if (effect.TransparentValue is EffectTexture)
+                    mtlWriter.WriteLine("illum 9");
+                else
+                    mtlWriter.WriteLine("illum 2");
+
+                mtlWriter.WriteLine();
+            }
+        }
+
+        private void WriteMaterialColor(string mtlCommand, EffectParameter effectParam)
+        {
+            if (effectParam == null || !(effectParam.Value is Vector4))
+                return;
+
+            var color = (Vector4)effectParam.Value;
+            mtlWriter.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}", mtlCommand, color.X, color.Y, color.Z));
+        }
+
+        private void WriteMaterialTextureMap(string mtlCommand, EffectParameter effectParam)
+        {
+            if (effectParam == null)
+                return;
+
+            var texture = effectParam.Value as EffectTexture;
+
+            if (texture == null)
+                return;
+
+            mtlWriter.WriteLine("{0} {1}", mtlCommand, texture.Sampler.Surface.InitFrom.FilePath);
+        }
+    }
+}
Index: /OniSplit/Dae/Image.cs
===================================================================
--- /OniSplit/Dae/Image.cs	(revision 1114)
+++ /OniSplit/Dae/Image.cs	(revision 1114)
@@ -0,0 +1,16 @@
+﻿namespace Oni.Dae
+{
+    internal class Image : Entity
+    {
+        public Image()
+        {
+        }
+
+        public Image(string filePath)
+        {
+            FilePath = filePath;
+        }
+
+        public string FilePath { get; set; }
+    }
+}
Index: /OniSplit/Dae/IndexedInput.cs
===================================================================
--- /OniSplit/Dae/IndexedInput.cs	(revision 1114)
+++ /OniSplit/Dae/IndexedInput.cs	(revision 1114)
@@ -0,0 +1,26 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class IndexedInput : Input
+    {
+        private readonly List<int> indices = new List<int>();
+
+        public IndexedInput()
+        {
+        }
+
+        public IndexedInput(Semantic semantic, Source source)
+            : base(semantic, source)
+        {
+        }
+
+        // Currently only used by readers/writers, probably not needed for the rest
+        internal int Offset { get; set; }
+
+        public int Set { get; set; }
+
+        public List<int> Indices => indices;
+    }
+}
Index: /OniSplit/Dae/Input.cs
===================================================================
--- /OniSplit/Dae/Input.cs	(revision 1114)
+++ /OniSplit/Dae/Input.cs	(revision 1114)
@@ -0,0 +1,26 @@
+﻿using System;
+
+namespace Oni.Dae
+{
+    internal class Input
+    {
+        public Input()
+        {
+        }
+
+        public Input(Semantic semantic, Source source)
+        {
+            if (semantic == Semantic.None)
+                throw new ArgumentException("Semantic None is not a valid value", "semantic");
+
+            if (source == null)
+                throw new ArgumentNullException("source");
+
+            Semantic = semantic;
+            Source = source;
+        }
+
+        public Semantic Semantic { get; set; }
+        public Source Source { get; set; }
+    }
+}
Index: /OniSplit/Dae/Instance.cs
===================================================================
--- /OniSplit/Dae/Instance.cs	(revision 1114)
+++ /OniSplit/Dae/Instance.cs	(revision 1114)
@@ -0,0 +1,24 @@
+﻿namespace Oni.Dae
+{
+    internal abstract class Instance
+    {
+        public string Sid { get; set; }
+        public string Name { get; set; }
+    }
+
+    internal abstract class Instance<T> : Instance
+    {
+        private T target;
+
+        public Instance()
+        {
+        }
+
+        public Instance(T target)
+        {
+            Target = target;
+        }
+
+        public T Target { get; set; }
+    }
+}
Index: /OniSplit/Dae/Light.cs
===================================================================
--- /OniSplit/Dae/Light.cs	(revision 1114)
+++ /OniSplit/Dae/Light.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Dae
+{
+    internal class Light : Entity
+    {
+        public LightType Type { get; set; }
+        public Vector3 Color { get; set; }
+        public float ConstantAttenuation { get; set; }
+        public float LinearAttenuation { get; set; }
+        public float QuadraticAttenuation { get; set; }
+        public float FalloffAngle { get; set; }
+        public float FalloffExponent { get; set; }
+        public float ZFar { get; set; }
+    }
+}
Index: /OniSplit/Dae/LightInstance.cs
===================================================================
--- /OniSplit/Dae/LightInstance.cs	(revision 1114)
+++ /OniSplit/Dae/LightInstance.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Dae
+{
+	internal class LightInstance : Instance<Light>
+	{
+        public LightInstance()
+        {
+        }
+
+        public LightInstance(Light light)
+            : base(light)
+        {
+        }
+	}
+}
Index: /OniSplit/Dae/LightType.cs
===================================================================
--- /OniSplit/Dae/LightType.cs	(revision 1114)
+++ /OniSplit/Dae/LightType.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Dae
+{
+	internal enum LightType
+	{
+		Invalid,
+		Ambient,
+		Directional,
+		Point,
+		Spot
+	}
+}
Index: /OniSplit/Dae/Material.cs
===================================================================
--- /OniSplit/Dae/Material.cs	(revision 1114)
+++ /OniSplit/Dae/Material.cs	(revision 1114)
@@ -0,0 +1,7 @@
+﻿namespace Oni.Dae
+{
+	internal class Material : Entity
+	{
+		public Effect Effect { get; set; }
+	}
+}
Index: /OniSplit/Dae/MaterialBinding.cs
===================================================================
--- /OniSplit/Dae/MaterialBinding.cs	(revision 1114)
+++ /OniSplit/Dae/MaterialBinding.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿namespace Oni.Dae
+{
+    internal class MaterialBinding
+    {
+        public MaterialBinding()
+        {
+        }
+
+        public MaterialBinding(string semantic, IndexedInput input)
+        {
+            Semantic = semantic;
+            VertexInput = input;
+        }
+
+        public string Semantic { get; set; }
+        public IndexedInput VertexInput { get; set; }
+    }
+}
Index: /OniSplit/Dae/MaterialInstance.cs
===================================================================
--- /OniSplit/Dae/MaterialInstance.cs	(revision 1114)
+++ /OniSplit/Dae/MaterialInstance.cs	(revision 1114)
@@ -0,0 +1,24 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class MaterialInstance : Instance<Material>
+    {
+        private readonly List<MaterialBinding> bindings = new List<MaterialBinding>();
+
+        public MaterialInstance()
+        {
+        }
+
+        public MaterialInstance(string symbol, Material material)
+            : base(material)
+        {
+            Symbol = symbol;
+        }
+
+        public string Symbol { get; set; }
+
+        public List<MaterialBinding> Bindings => bindings;
+    }
+}
Index: /OniSplit/Dae/MeshPrimitiveType.cs
===================================================================
--- /OniSplit/Dae/MeshPrimitiveType.cs	(revision 1114)
+++ /OniSplit/Dae/MeshPrimitiveType.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Dae
+{
+	internal enum MeshPrimitiveType
+	{
+		Lines,
+		LineStrips,
+		Polygons,
+		TriangleFans,
+		TriangleStrips,
+	}
+}
Index: /OniSplit/Dae/MeshPrimitives.cs
===================================================================
--- /OniSplit/Dae/MeshPrimitives.cs	(revision 1114)
+++ /OniSplit/Dae/MeshPrimitives.cs	(revision 1114)
@@ -0,0 +1,34 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class MeshPrimitives
+    {
+        private readonly MeshPrimitiveType primitiveType;
+        private readonly List<IndexedInput> inputs;
+        private readonly List<int> vertexCounts;
+
+        public MeshPrimitives(MeshPrimitiveType primitiveType)
+        {
+            this.primitiveType = primitiveType;
+            this.inputs = new List<IndexedInput>(3);
+            this.vertexCounts = new List<int>();
+        }
+
+        public MeshPrimitives(MeshPrimitiveType primitiveType, IEnumerable<IndexedInput> inputs)
+        {
+            this.primitiveType = primitiveType;
+            this.inputs = new List<IndexedInput>(inputs);
+            this.vertexCounts = new List<int>();
+        }
+
+        public MeshPrimitiveType PrimitiveType => primitiveType;
+
+        public string MaterialSymbol { get; set; }
+
+        public List<IndexedInput> Inputs => inputs;
+
+        public List<int> VertexCounts => vertexCounts;
+    }
+}
Index: /OniSplit/Dae/Node.cs
===================================================================
--- /OniSplit/Dae/Node.cs	(revision 1114)
+++ /OniSplit/Dae/Node.cs	(revision 1114)
@@ -0,0 +1,56 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class Node : Entity
+    {
+        private TransformCollection transforms;
+        private List<Instance> instances;
+        private List<Node> nodes;
+
+        public TransformCollection Transforms
+        {
+            get
+            {
+                if (transforms == null)
+                    transforms = new TransformCollection();
+
+                return transforms;
+            }
+        }
+
+        public List<Instance> Instances
+        {
+            get
+            {
+                if (instances == null)
+                    instances = new List<Instance>();
+
+                return instances;
+            }
+        }
+
+        public IEnumerable<GeometryInstance> GeometryInstances
+        {
+            get
+            {
+                if (instances == null)
+                    return new GeometryInstance[0];
+
+                return instances.OfType<GeometryInstance>();
+            }
+        }
+
+        public List<Node> Nodes
+        {
+            get
+            {
+                if (nodes == null)
+                    nodes = new List<Node>();
+
+                return nodes;
+            }
+        }
+    }
+}
Index: /OniSplit/Dae/NodeInstance.cs
===================================================================
--- /OniSplit/Dae/NodeInstance.cs	(revision 1114)
+++ /OniSplit/Dae/NodeInstance.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Dae
+{
+    internal class NodeInstance : Instance<Node>
+    {
+        public NodeInstance()
+        {
+        }
+
+        public NodeInstance(Node node)
+            : base(node)
+        {
+        }
+    }
+}
Index: /OniSplit/Dae/Reader.cs
===================================================================
--- /OniSplit/Dae/Reader.cs	(revision 1114)
+++ /OniSplit/Dae/Reader.cs	(revision 1114)
@@ -0,0 +1,20 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Dae
+{
+    internal class Reader
+    {
+        public static Scene ReadFile(string filePath)
+        {
+            string type = Path.GetExtension(filePath);
+
+            if (string.Equals(type, ".dae", StringComparison.OrdinalIgnoreCase))
+                return IO.DaeReader.ReadFile(filePath);
+            else if (string.Equals(type, ".obj", StringComparison.OrdinalIgnoreCase))
+                return IO.ObjReader.ReadFile(filePath);
+            else
+                throw new NotSupportedException($"Unsupported 3D file type {type}");
+        }
+    }
+}
Index: /OniSplit/Dae/Sampler.cs
===================================================================
--- /OniSplit/Dae/Sampler.cs	(revision 1114)
+++ /OniSplit/Dae/Sampler.cs	(revision 1114)
@@ -0,0 +1,266 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class Sampler : Entity
+    {
+        private readonly List<Input> inputs = new List<Input>();
+        private float outputScale = 1.0f;
+
+        public List<Input> Inputs => inputs;
+
+        public int FrameCount
+        {
+            get
+            {
+                var input = inputs.Find(i => i.Semantic == Semantic.Input);
+
+                if (input == null)
+                    return 0;
+
+                return FMath.RoundToInt32(input.Source.FloatData.Last() * 60.0f) + 1;
+            }
+        }
+
+        public Sampler Scale(float scale)
+        {
+            var newSampler = new Sampler
+            {
+                outputScale = scale
+            };
+
+            newSampler.inputs.AddRange(inputs);
+
+            return newSampler;
+        }
+
+        public Sampler Split(int offset)
+        {
+            var newSampler = new Sampler();
+
+            foreach (var input in inputs)
+            {
+                var source = input.Source;
+
+                switch (input.Semantic)
+                {
+                    case Semantic.Input:
+                        newSampler.inputs.Add(input);
+                        break;
+
+                    case Semantic.Interpolation:
+                        newSampler.inputs.Add(input);
+                        break;
+
+                    case Semantic.Output:
+                        {
+                            var data = new float[source.Count];
+
+                            for (int i = 0; i < data.Length; i++)
+                                data[i] = source.FloatData[i * source.Stride + offset];
+
+                            newSampler.inputs.Add(new Input
+                            {
+                                Source = new Source(data, 1),
+                                Semantic = input.Semantic
+                            });
+                        }
+                        break;
+
+                    case Semantic.InTangent:
+                    case Semantic.OutTangent:
+                        {
+                            var data = new float[source.Count * 2];
+
+                            for (int i = 0; i < data.Length; i++)
+                            {
+                                data[i + 0] = source.FloatData[i * source.Stride];
+                                data[i + 1] = source.FloatData[i * source.Stride + (offset + 1)];
+                            }
+
+                            newSampler.inputs.Add(new Input
+                            {
+                                Source = new Source(data, 2),
+                                Semantic = input.Semantic
+                            });
+                        }
+                        break;
+                }
+            }
+
+            return newSampler;
+        }
+
+        public float[] Sample() => Sample(0, FrameCount - 1);
+
+        public float[] Sample(int start, int end)
+        {
+            var result = Sample(start, end, 0);
+
+            if (outputScale != 1.0f)
+            {
+                for (int i = 0; i < result.Length; i++)
+                    result[i] *= outputScale;
+            }
+
+            return result;
+        }
+
+        private float[] Sample(int start, int end, int offset)
+        {
+            float[] input = null;
+            float[] output = null;
+            int outputStride = 1;
+            Vector2[] inTangent = null;
+            Vector2[] outTangent = null;
+            string[] interpolation = null;
+
+            foreach (var i in inputs)
+            {
+                switch (i.Semantic)
+                {
+                    case Semantic.Input:
+                        input = i.Source.FloatData;
+                        break;
+
+                    case Semantic.Output:
+                        output = i.Source.FloatData;
+                        outputStride = i.Source.Stride;
+                        break;
+
+                    case Semantic.InTangent:
+                        inTangent = FloatArrayToVector2Array(i.Source.FloatData);
+                        break;
+
+                    case Semantic.OutTangent:
+                        outTangent = FloatArrayToVector2Array(i.Source.FloatData);
+                        break;
+
+                    case Semantic.Interpolation:
+                        interpolation = i.Source.NameData;
+                        break;
+                }
+            }
+
+            if (offset >= outputStride)
+                throw new ArgumentException("The offset must be less than the output stride", "offset");
+
+            float[] result = new float[end - start + 1];
+
+            if (input == null || output == null || interpolation == null)
+            {
+                //
+                // If we don't have enough data to sample then we just return 0 for all frames.
+                //
+
+                return result;
+            }
+
+            if (output.Length == outputStride)
+            {
+                //
+                // If the output contains only one element then use that for all frames.
+                //
+
+                for (int i = 0; i < result.Length; i++)
+                    result[i] = output[offset];
+
+                return result;
+            }
+
+            float inputFirst = input.First();
+            float outputFirst = output[offset];
+
+            float inputLast = input.Last();
+            float outputLast = output[output.Length - outputStride + offset];
+
+            for (int frame = 0; frame < result.Length; frame++)
+            {
+                float t = (frame + start) / 60.0f;
+
+                if (t <= inputFirst)
+                {
+                    result[frame] = outputFirst;
+                    continue;
+                }
+
+                if (t >= inputLast)
+                {
+                    result[frame] = outputLast;
+                    continue;
+                }
+
+                int index = Array.BinarySearch(input, t);
+
+                if (index >= 0)
+                {
+                    result[frame] = output[index * outputStride + offset];
+                    continue;
+                }
+
+                index = ~index;
+
+                if (index == 0)
+                {
+                    result[frame] = outputFirst;
+                    continue;
+                }
+
+                if (index * outputStride + offset >= output.Length)
+                {
+                    result[frame] = outputLast;
+                    continue;
+                }
+
+                var p0 = new Vector2(input[index - 1], output[(index - 1) * outputStride + offset]);
+                var p1 = new Vector2(input[index], output[index * outputStride + offset]);
+
+                float s = (t - p0.X) / (p1.X - p0.X);
+
+                switch (interpolation[index - 1])
+                {
+                    default:
+                        Console.Error.WriteLine("Interpolation type '{0}' is not supported, using LINEAR", interpolation[index - 1]);
+                        goto case "LINEAR";
+
+                    case "LINEAR":
+                        result[frame] = p0.Y + s * (p1.Y - p0.Y);
+                        break;
+
+                    case "BEZIER":
+                        if (inTangent == null || outTangent == null)
+                            throw new System.IO.InvalidDataException("Bezier interpolation was specified but in/out tangents are not present");
+
+                        var c0 = outTangent[index - 1];
+                        var c1 = inTangent[index];
+
+                        float invS = 1.0f - s;
+
+                        result[frame] =
+                              p0.Y * invS * invS * invS
+                            + 3.0f * c0.Y * invS * invS * s
+                            + 3.0f * c1.Y * invS * s * s
+                            + p1.Y * s * s * s;
+
+                        break;
+                }
+            }
+
+            return result;
+        }
+
+        private static Vector2[] FloatArrayToVector2Array(float[] array)
+        {
+            var result = new Vector2[array.Length / 2];
+
+            for (int i = 0; i < result.Length; i++)
+            {
+                result[i].X = array[i * 2 + 0];
+                result[i].Y = array[i * 2 + 1];
+            }
+
+            return result;
+        }
+    }
+}
Index: /OniSplit/Dae/Scene.cs
===================================================================
--- /OniSplit/Dae/Scene.cs	(revision 1114)
+++ /OniSplit/Dae/Scene.cs	(revision 1114)
@@ -0,0 +1,6 @@
+﻿namespace Oni.Dae
+{
+	internal class Scene : Node
+	{
+	}
+}
Index: /OniSplit/Dae/Semantic.cs
===================================================================
--- /OniSplit/Dae/Semantic.cs	(revision 1114)
+++ /OniSplit/Dae/Semantic.cs	(revision 1114)
@@ -0,0 +1,19 @@
+﻿namespace Oni.Dae
+{
+	internal enum Semantic
+	{
+		None,
+
+		Position,
+		Normal,
+		TexCoord,
+		Color,
+		Vertex,
+
+		Input,
+		InTangent,
+		OutTangent,
+		Interpolation,
+		Output,
+	}
+}
Index: /OniSplit/Dae/Source.cs
===================================================================
--- /OniSplit/Dae/Source.cs	(revision 1114)
+++ /OniSplit/Dae/Source.cs	(revision 1114)
@@ -0,0 +1,125 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class Source : Entity
+    {
+        private float[] floatData;
+        private string[] nameData;
+        private int stride;
+        private int count;
+
+        public Source(IEnumerable<float> data, int stride)
+        {
+            this.floatData = data.ToArray();
+            this.stride = stride;
+            this.count = floatData.Length / stride;
+        }
+
+        public Source(float[] data, int stride)
+        {
+            this.floatData = (float[])data.Clone();
+            this.stride = stride;
+            this.count = data.Length / stride;
+        }
+
+        public Source(string[] data, int stride)
+        {
+            this.nameData = (string[])data.Clone();
+            this.stride = stride;
+            this.count = data.Length / stride;
+        }
+
+        public Source(IList<Vector2> data)
+        {
+            int count = data.Count;
+            float[] array = new float[count * 2];
+
+            for (int i = 0; i < count; i++)
+            {
+                array[i * 2 + 0] = data[i].X;
+                array[i * 2 + 1] = 1.0f - data[i].Y;
+            }
+
+            this.floatData = array;
+            this.count = count;
+            this.stride = 2;
+        }
+
+        public Source(IList<Vector3> data)
+        {
+            int count = data.Count;
+            float[] array = new float[count * 3];
+
+            for (int i = 0; i < count; i++)
+            {
+                array[i * 3 + 0] = data[i].X;
+                array[i * 3 + 1] = data[i].Y;
+                array[i * 3 + 2] = data[i].Z;
+            }
+
+            this.floatData = array;
+            this.count = count;
+            this.stride = 3;
+        }
+
+        public string[] NameData => nameData;
+        public float[] FloatData => floatData;
+        public int Count => count;
+
+        public int Stride
+        {
+            get
+            {
+                return stride;
+            }
+            set
+            {
+                stride = value;
+
+                if (floatData != null)
+                    count = floatData.Length / stride;
+                else
+                    count = nameData.Length / stride;
+            }
+        }
+
+        public static Vector2 ReaderVector2(Source source, int index)
+        {
+            var data = source.floatData;
+            var i = index * source.stride;
+
+            return new Vector2(data[i + 0], data[i + 1]);
+        }
+
+        public static Vector2 ReadTexCoord(Source source, int index)
+        {
+            var data = source.floatData;
+            var i = index * source.stride;
+
+            return new Vector2(data[i + 0], 1.0f - data[i + 1]);
+        }
+
+        public static Vector3 ReadVector3(Source source, int index)
+        {
+            var data = source.floatData;
+            var i = index * source.stride;
+
+            return new Vector3(data[i + 0], data[i + 1], data[i + 2]);
+        }
+
+        public static Vector4 ReadVector4(Source source, int index)
+        {
+            var data = source.floatData;
+            var i = index * source.stride;
+
+            return new Vector4(data[i + 0], data[i + 1], data[i + 2], data[i + 3]);
+        }
+
+        public static Imaging.Color ReadColor(Source source, int index)
+        {
+            return new Imaging.Color(ReadVector4(source, index));
+        }
+    }
+}
Index: /OniSplit/Dae/Transform.cs
===================================================================
--- /OniSplit/Dae/Transform.cs	(revision 1114)
+++ /OniSplit/Dae/Transform.cs	(revision 1114)
@@ -0,0 +1,98 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal abstract class Transform
+    {
+        private string sid;
+        private readonly float[] values;
+        private Sampler[] animations;
+
+        protected Transform(int valueCount)
+        {
+            this.values = new float[valueCount];
+        }
+
+        protected Transform(string sid, int valueCount)
+        {
+            this.sid = sid;
+            this.values = new float[valueCount];
+        }
+
+        public string Sid
+        {
+            get { return sid; }
+            set { sid = value; }
+        }
+
+        public float[] Values => values;
+        public bool HasAnimations => animations != null;
+
+        public Sampler[] Animations
+        {
+            get
+            {
+                if (animations == null)
+                    animations = new Sampler[values.Length];
+
+                return animations;
+            }
+        }
+
+        protected Sampler GetAnimation(int index)
+        {
+            if (animations == null)
+                return null;
+
+            return animations[index];
+        }
+
+        public void BindAnimation(string valueName, Sampler animation)
+        {
+            if (string.IsNullOrEmpty(valueName))
+            {
+                for (int i = 0; i < values.Length; i++)
+                    BindAnimation(i, animation);
+            }
+            else
+            {
+                int valueIndex = ParseValueIndex(valueName);
+
+                if (valueIndex != -1)
+                    BindAnimation(valueIndex, animation);
+            }
+        }
+
+        private void BindAnimation(int index, Sampler animation)
+        {
+            if (animation.Inputs.Count == 0 || animation.Inputs[0].Source.Count == 0)
+                animation = null;
+
+            if (animation == null && !HasAnimations)
+                return;
+
+            Animations[index] = animation;
+        }
+
+        private int ParseValueIndex(string name)
+        {
+            if (name[0] == '(')
+            {
+                int end = name.IndexOf(')', 1);
+
+                if (end == -1)
+                    return -1;
+
+                return int.Parse(name.Substring(1, end - 1).Trim());
+            }
+
+            return ValueNameToValueIndex(name);
+        }
+
+        public abstract int ValueNameToValueIndex(string name);
+        public abstract string ValueIndexToValueName(int index);
+
+        public abstract Matrix ToMatrix();
+    }
+}
Index: /OniSplit/Dae/TransformCollection.cs
===================================================================
--- /OniSplit/Dae/TransformCollection.cs	(revision 1114)
+++ /OniSplit/Dae/TransformCollection.cs	(revision 1114)
@@ -0,0 +1,38 @@
+﻿using System.Collections.Generic;
+
+namespace Oni.Dae
+{
+    internal class TransformCollection : List<Transform>
+    {
+        public Matrix ToMatrix()
+        {
+            var matrix = Matrix.Identity;
+
+            foreach (var transform in Utils.Reverse(this))
+                matrix *= transform.ToMatrix();
+
+            return matrix;
+        }
+
+        public TransformScale Scale(string sid, Vector3 scale)
+        {
+            var transform = new TransformScale(sid, scale);
+            Add(transform);
+            return transform;
+        }
+
+        public TransformRotate Rotate(string sid, Vector3 axis, float angle)
+        {
+            var transform = new TransformRotate(sid, axis, angle);
+            Add(transform);
+            return transform;
+        }
+
+        public TransformTranslate Translate(string sid, Vector3 translate)
+        {
+            var transform = new TransformTranslate(sid, translate);
+            Add(transform);
+            return transform;
+        }
+    }
+}
Index: /OniSplit/Dae/TransformMatrix.cs
===================================================================
--- /OniSplit/Dae/TransformMatrix.cs	(revision 1114)
+++ /OniSplit/Dae/TransformMatrix.cs	(revision 1114)
@@ -0,0 +1,26 @@
+﻿using System;
+
+namespace Oni.Dae
+{
+    internal class TransformMatrix : Transform
+    {
+        public TransformMatrix()
+            : base(16)
+        {
+        }
+
+        public Matrix Matrix
+        {
+            get { return new Matrix(Values); }
+            set { value.CopyTo(Values); }
+        }
+
+        public override Matrix ToMatrix() => Matrix;
+        public override int ValueNameToValueIndex(string name) => -1;
+
+        public override string ValueIndexToValueName(int index)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}
Index: /OniSplit/Dae/TransformRotate.cs
===================================================================
--- /OniSplit/Dae/TransformRotate.cs	(revision 1114)
+++ /OniSplit/Dae/TransformRotate.cs	(revision 1114)
@@ -0,0 +1,48 @@
+﻿using System;
+
+namespace Oni.Dae
+{
+    internal class TransformRotate : Transform
+    {
+        private static readonly string[] valueNames = new[] { "X", "Y", "Z", "ANGLE" };
+
+        public TransformRotate()
+            : base(4)
+        {
+        }
+
+        public TransformRotate(Vector3 axis, float angle)
+            : base(4)
+        {
+            Axis = axis;
+            Angle = angle;
+        }
+
+        public TransformRotate(string sid, Vector3 axis, float angle)
+            : base(sid, 4)
+        {
+            Axis = axis;
+            Angle = angle;
+        }
+
+        public Vector3 Axis
+        {
+            get { return new Vector3(Values); }
+            set { value.CopyTo(Values); }
+        }
+
+        public float Angle
+        {
+            get { return Values[3]; }
+            set { Values[3] = value; }
+        }
+
+        public Sampler AngleAnimation => GetAnimation(3);
+
+        public override Matrix ToMatrix() => Matrix.CreateFromAxisAngle(Axis, MathHelper.ToRadians(Angle));
+        public Quaternion ToQuaternion() => Quaternion.CreateFromAxisAngle(Axis, MathHelper.ToRadians(Angle));
+
+        public override int ValueNameToValueIndex(string name) => Array.FindIndex(valueNames, x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase));
+        public override string ValueIndexToValueName(int index) => valueNames[index];
+    }
+}
Index: /OniSplit/Dae/TransformScale.cs
===================================================================
--- /OniSplit/Dae/TransformScale.cs	(revision 1114)
+++ /OniSplit/Dae/TransformScale.cs	(revision 1114)
@@ -0,0 +1,31 @@
+﻿using System;
+
+namespace Oni.Dae
+{
+    internal class TransformScale : Transform
+    {
+        private static readonly string[] valueNames = new[] { "X", "Y", "Z" };
+
+        public TransformScale()
+            : base(3)
+        {
+        }
+
+        public TransformScale(string sid, Vector3 scale)
+            : base(sid, 3)
+        {
+            Scale = scale;
+        }
+
+        public Vector3 Scale
+        {
+            get { return new Vector3(Values); }
+            set { value.CopyTo(Values); }
+        }
+
+        public override Matrix ToMatrix() => Matrix.CreateScale(Scale);
+
+        public override int ValueNameToValueIndex(string name) => Array.FindIndex(valueNames, x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase));
+        public override string ValueIndexToValueName(int index) => valueNames[index];
+    }
+}
Index: /OniSplit/Dae/TransformTranslate.cs
===================================================================
--- /OniSplit/Dae/TransformTranslate.cs	(revision 1114)
+++ /OniSplit/Dae/TransformTranslate.cs	(revision 1114)
@@ -0,0 +1,37 @@
+﻿using System;
+
+namespace Oni.Dae
+{
+    internal class TransformTranslate : Transform
+    {
+        private static readonly string[] valueNames = new[] { "X", "Y", "Z" };
+
+        public TransformTranslate()
+            : base(3)
+        {
+        }
+
+        public TransformTranslate(Vector3 translation)
+            : base(3)
+        {
+            Translation = translation;
+        }
+
+        public TransformTranslate(string sid, Vector3 translation)
+            : base(sid, 3)
+        {
+            Translation = translation;
+        }
+
+        public Vector3 Translation
+        {
+            get { return new Vector3(Values); }
+            set { value.CopyTo(Values); }
+        }
+
+        public override Matrix ToMatrix() => Matrix.CreateTranslation(Translation);
+
+        public override int ValueNameToValueIndex(string name) => Array.FindIndex(valueNames, x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase));
+        public override string ValueIndexToValueName(int index) => valueNames[index];
+    }
+}
Index: /OniSplit/Dae/Visitor.cs
===================================================================
--- /OniSplit/Dae/Visitor.cs	(revision 1114)
+++ /OniSplit/Dae/Visitor.cs	(revision 1114)
@@ -0,0 +1,146 @@
+﻿namespace Oni.Dae
+{
+    internal class Visitor
+    {
+        public virtual void VisitScene(Scene scene)
+        {
+            foreach (var node in scene.Nodes)
+                VisitNode(node);
+        }
+
+        public virtual void VisitNode(Node node)
+        {
+            foreach (var transform in node.Transforms)
+                VisitTransform(transform);
+
+            foreach (var instance in node.Instances)
+            {
+                if (instance is GeometryInstance)
+                    VisitGeometryInstance((GeometryInstance)instance);
+                else if (instance is LightInstance)
+                    VisitLightInstance((LightInstance)instance);
+                else if (instance is CameraInstance)
+                    VisitCameraInstance((CameraInstance)instance);
+            }
+
+            foreach (var child in node.Nodes)
+                VisitNode(child);
+        }
+
+        public virtual void VisitGeometryInstance(GeometryInstance instance)
+        {
+            foreach (var materialInstance in instance.Materials)
+                VisitMaterialInstance(materialInstance);
+
+            VisitGeometry(instance.Target);
+        }
+
+        public virtual void VisitGeometry(Geometry geometry)
+        {
+            foreach (var input in geometry.Vertices)
+                VisitInput(input);
+
+            foreach (var primitives in geometry.Primitives)
+                VisitMeshPrimitives(primitives);
+        }
+
+        public virtual void VisitMeshPrimitives(MeshPrimitives primitives)
+        {
+            foreach (var input in primitives.Inputs)
+                VisitInput(input);
+        }
+
+        public virtual void VisitMaterialInstance(MaterialInstance instance)
+        {
+            VisitMaterial(instance.Target);
+        }
+
+        public virtual void VisitMaterial(Material material)
+        {
+            VisitEffect(material.Effect);
+        }
+
+        public virtual void VisitLightInstance(LightInstance instance)
+        {
+            VisitLight(instance.Target);
+        }
+
+        public virtual void VisitLight(Light light)
+        {
+        }
+
+        public virtual void VisitCameraInstance(CameraInstance instance)
+        {
+            VisitCamera(instance.Target);
+        }
+
+        public virtual void VisitCamera(Camera camera)
+        {
+        }
+
+        public virtual void VisitTransform(Transform transform)
+        {
+            if (transform.HasAnimations)
+            {
+                foreach (var sampler in transform.Animations.Where(s => s != null))
+                    VisitSampler(sampler);
+            }
+        }
+
+        public virtual void VisitSampler(Sampler sampler)
+        {
+            foreach (var input in sampler.Inputs)
+                VisitInput(input);
+        }
+
+        public virtual void VisitEffect(Effect effect)
+        {
+            foreach (var parameter in effect.Parameters)
+                VisitEffectParameter(parameter);
+
+            VisitEffectParameter(effect.Ambient);
+            VisitEffectParameter(effect.Diffuse);
+            VisitEffectParameter(effect.Emission);
+            VisitEffectParameter(effect.IndexOfRefraction);
+            VisitEffectParameter(effect.Reflective);
+            VisitEffectParameter(effect.Shininess);
+            VisitEffectParameter(effect.Specular);
+            VisitEffectParameter(effect.Transparency);
+            VisitEffectParameter(effect.Transparent);
+        }
+
+        public virtual void VisitEffectParameter(EffectParameter parameter)
+        {
+            if (parameter.Value is EffectTexture)
+                VisitEffectTexture((EffectTexture)parameter.Value);
+        }
+
+        public virtual void VisitEffectTexture(EffectTexture texture)
+        {
+            VisitEffectSampler(texture.Sampler);
+        }
+
+        public virtual void VisitEffectSampler(EffectSampler sampler)
+        {
+            VisitEffectSurface(sampler.Surface);
+        }
+
+        public virtual void VisitEffectSurface(EffectSurface surface)
+        {
+            VisitImage(surface.InitFrom);
+        }
+
+        public virtual void VisitInput(Input input)
+        {
+            VisitSource(input.Source);
+        }
+
+        public virtual void VisitSource(Source source)
+        {
+        }
+
+        public virtual void VisitImage(Image image)
+        {
+        }
+    }
+}
Index: /OniSplit/Dae/Writer.cs
===================================================================
--- /OniSplit/Dae/Writer.cs	(revision 1114)
+++ /OniSplit/Dae/Writer.cs	(revision 1114)
@@ -0,0 +1,20 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Dae
+{
+    internal class Writer
+    {
+        public static void WriteFile(string filePath, Scene scene)
+        {
+            string type = Path.GetExtension(filePath);
+
+            if (string.Equals(type, ".dae", StringComparison.OrdinalIgnoreCase))
+                IO.DaeWriter.WriteFile(filePath, scene);
+            else if (string.Equals(type, ".obj", StringComparison.OrdinalIgnoreCase))
+                IO.ObjWriter.WriteFile(filePath, scene);
+            else
+                throw new NotSupportedException(string.Format("Unsupported 3D file type {0}", type));
+        }
+    }
+}
Index: /OniSplit/DaeExporter.cs
===================================================================
--- /OniSplit/DaeExporter.cs	(revision 1114)
+++ /OniSplit/DaeExporter.cs	(revision 1114)
@@ -0,0 +1,296 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni
+{
+    internal class DaeExporter : Exporter
+    {
+        private readonly bool noAnimation;
+        private readonly List<string> animationNames = new List<string>();
+        private readonly string geometryName;
+        private readonly string fileType;
+
+        public DaeExporter(string[] args, InstanceFileManager fileManager, string outputDirPath, string fileType)
+            : base(fileManager, outputDirPath)
+        {
+            foreach (string arg in args)
+            {
+                if (arg == "-noanim")
+                    noAnimation = true;
+                else if (arg.StartsWith("-anim:", StringComparison.Ordinal))
+                    animationNames.Add(arg.Substring(6));
+                else if (arg.StartsWith("-geom:", StringComparison.Ordinal))
+                    geometryName = arg.Substring(6);
+            }
+
+            this.fileType = fileType;
+        }
+
+        protected override void ExportFile(string sourceFilePath)
+        {
+            string extension = Path.GetExtension(sourceFilePath);
+
+            if (string.Equals(extension, ".xml", StringComparison.OrdinalIgnoreCase))
+            {
+                var sceneExporter = new SceneExporter(InstanceFileManager, OutputDirPath);
+                sceneExporter.ExportScene(sourceFilePath);
+                return;
+            }
+
+            base.ExportFile(sourceFilePath);
+        }
+
+        protected override List<InstanceDescriptor> GetSupportedDescriptors(InstanceFile file)
+        {
+            var descriptors = new List<InstanceDescriptor>();
+            descriptors.AddRange(file.GetNamedDescriptors(TemplateTag.ONCC));
+            descriptors.AddRange(file.GetNamedDescriptors(TemplateTag.TRBS));
+            descriptors.AddRange(file.GetNamedDescriptors(TemplateTag.M3GM));
+            descriptors.AddRange(file.GetNamedDescriptors(TemplateTag.AKEV));
+            descriptors.AddRange(file.GetNamedDescriptors(TemplateTag.OBAN));
+            descriptors.AddRange(file.GetNamedDescriptors(TemplateTag.OFGA));
+            descriptors.AddRange(file.GetNamedDescriptors(TemplateTag.ONWC));
+            return descriptors;
+        }
+
+        protected override void ExportInstance(InstanceDescriptor descriptor)
+        {
+            var tag = descriptor.Template.Tag;
+
+            if (tag == TemplateTag.AKEV)
+            {
+                var mesh = Akira.AkiraDatReader.Read(descriptor);
+                Akira.AkiraDaeWriter.Write(mesh, descriptor.Name, OutputDirPath, fileType);
+                return;
+            }
+
+            var scene = new Dae.Scene();
+            scene.Name = descriptor.Name;
+
+            var textureWriter = new Motoko.TextureDaeWriter(OutputDirPath);
+            var geometryWriter = new Motoko.GeometryDaeWriter(textureWriter);
+            var bodyWriter = new Totoro.BodyDaeWriter(geometryWriter);
+
+            if (tag == TemplateTag.OFGA)
+                ExportObjectGeometry(descriptor, scene, geometryWriter);
+            else if (tag == TemplateTag.OBAN)
+                ExportObjectAnimation(descriptor, scene, geometryWriter);
+            else if (tag == TemplateTag.ONCC)
+                ExportCharacterBody(descriptor, scene, bodyWriter);
+            else if (tag == TemplateTag.TRBS)
+                ExportCharacterBodySet(descriptor, scene, bodyWriter);
+            else if (tag == TemplateTag.M3GM)
+                ExportGeometry(descriptor, scene, geometryWriter);
+            else if (tag == TemplateTag.ONWC)
+                ExportWeaponGeometry(descriptor, scene, geometryWriter);
+
+            if (scene.Nodes.Count > 0)
+            {
+                string filePath = Path.Combine(OutputDirPath, descriptor.Name + "." + fileType);
+
+                Dae.Writer.WriteFile(filePath, scene);
+            }
+        }
+
+        private void ExportObjectGeometry(InstanceDescriptor descriptor, Dae.Scene scene, Motoko.GeometryDaeWriter geometryWriter)
+        {
+            var geometry = Physics.ObjectDatReader.ReadObjectGeometry(descriptor);
+            var root = new Dae.Node();
+
+            foreach (var objNode in geometry.Geometries)
+                root.Nodes.Add(geometryWriter.WriteNode(objNode.Geometry, objNode.Geometry.Name));
+
+            scene.Nodes.Add(root);
+        }
+
+        private void ExportObjectAnimation(InstanceDescriptor descriptor, Dae.Scene scene, Motoko.GeometryDaeWriter geometryWriter)
+        {
+            var animation = Physics.ObjectDatReader.ReadAnimation(descriptor);
+            Dae.Node node;
+
+            if (geometryName == "camera")
+            {
+                node = new Dae.Node
+                {
+                    Name = descriptor.Name + "_camera",
+                    Instances = {
+                        new Dae.CameraInstance {
+                            Target = new Dae.Camera {
+                                XFov = 45.0f,
+                                AspectRatio = 4.0f / 3.0f,
+                                ZNear = 1.0f,
+                                ZFar = 10000.0f
+                            }
+                        }}
+                };
+            }
+            else if (geometryName != null)
+            {
+                var file = InstanceFileManager.OpenFile(geometryName);
+
+                if (file == null)
+                {
+                    Console.Error.WriteLine("Cannot fine file {0}", geometryName);
+                    node = new Dae.Node();
+                }
+                else
+                {
+                    var geom = Motoko.GeometryDatReader.Read(file.Descriptors[0]);
+                    node = geometryWriter.WriteNode(geom, geom.Name);
+                }
+            }
+            else
+            {
+                node = new Dae.Node();
+            }
+
+            scene.Nodes.Add(node);
+
+            ExportAnimation(node, new List<Physics.ObjectAnimationKey>(animation.Keys));
+        }
+
+        private void ExportGeometry(InstanceDescriptor descriptor, Dae.Scene scene, Motoko.GeometryDaeWriter geometryWriter)
+        {
+            var animations = new List<Physics.ObjectAnimation>(animationNames.Count);
+
+            foreach (string animationFilePath in animationNames)
+            {
+                var file = InstanceFileManager.OpenFile(animationFilePath);
+
+                if (file == null)
+                {
+                    Console.Error.WriteLine("Cannot find animation {0}", animationFilePath);
+                    continue;
+                }
+
+                animations.Add(Physics.ObjectDatReader.ReadAnimation(file.Descriptors[0]));
+            }
+
+            ExportGeometry(scene, geometryWriter, descriptor, animations);
+        }
+
+        private void ExportCharacterBodySet(InstanceDescriptor descriptor, Dae.Scene scene, Totoro.BodyDaeWriter bodyWriter)
+        {
+            var body = Totoro.BodyDatReader.Read(descriptor);
+            var node = bodyWriter.Write(body, noAnimation, null);
+
+            scene.Nodes.Add(node);
+        }
+
+        private void ExportCharacterBody(InstanceDescriptor descriptor, Dae.Scene scene, Totoro.BodyDaeWriter bodyWriter)
+        {
+            var animationName = animationNames.Count > 0 ? animationNames[0] : null;
+            var characterClass = Game.CharacterClass.Read(descriptor, animationName);
+
+            var body = Totoro.BodyDatReader.Read(characterClass.Body);
+            var textures = characterClass.Textures;
+            var pelvis = bodyWriter.Write(body, noAnimation, textures);
+
+            scene.Nodes.Add(pelvis);
+
+            var animation = noAnimation ? null : characterClass.Animation;
+
+            if (animation != null)
+            {
+                var anim = Totoro.AnimationDatReader.Read(animation);
+
+                Totoro.AnimationDaeWriter.Write(pelvis, anim);
+            }
+        }
+
+        private void ExportWeaponGeometry(InstanceDescriptor descriptor, Dae.Scene scene, Motoko.GeometryDaeWriter geometryWriter)
+        {
+            var weaponClass = Game.WeaponClass.Read(descriptor);
+
+            if (weaponClass.Geometry != null)
+                ExportGeometry(weaponClass.Geometry, scene, geometryWriter);
+        }
+
+        private static void ExportGeometry(Dae.Scene scene, Motoko.GeometryDaeWriter geometryWriter, InstanceDescriptor m3gm, List<Physics.ObjectAnimation> animations)
+        {
+            var geometry = Motoko.GeometryDatReader.Read(m3gm);
+
+            if (animations != null && animations.Count > 0)
+            {
+                geometry.HasTransform = true;
+                geometry.Transform = Matrix.CreateScale(animations[0].Keys[0].Scale);
+            }
+
+            var node = geometryWriter.WriteNode(geometry, m3gm.Name);
+            scene.Nodes.Add(node);
+
+            if (animations != null && animations.Count > 0)
+            {
+                var frames = new List<Physics.ObjectAnimationKey>();
+                int offset = 0;
+
+                foreach (var animation in animations)
+                {
+                    foreach (var key in animation.Keys)
+                    {
+                        frames.Add(new Physics.ObjectAnimationKey
+                        {
+                            Translation = key.Translation,
+                            Rotation = key.Rotation,
+                            Time = key.Time + offset
+                        });
+                    }
+
+                    offset += animation.Length;
+                }
+
+                ExportAnimation(node, frames);
+            }
+        }
+
+        private static void ExportAnimation(Dae.Node node, List<Physics.ObjectAnimationKey> frames)
+        {
+            var times = new float[frames.Count];
+            var interpolations = new string[times.Length];
+            var positions = new Vector3[frames.Count];
+            var angles = new Vector3[frames.Count];
+
+            for (int i = 0; i < times.Length; ++i)
+                times[i] = frames[i].Time / 60.0f;
+
+            for (int i = 0; i < interpolations.Length; i++)
+                interpolations[i] = "LINEAR";
+
+            for (int i = 0; i < frames.Count; i++)
+                positions[i] = frames[i].Translation;
+
+            for (int i = 0; i < frames.Count; i++)
+                angles[i] = frames[i].Rotation.ToEulerXYZ();
+
+            var translate = node.Transforms.Translate("translate", positions[0]);
+            var rotateX = node.Transforms.Rotate("rotX", Vector3.UnitX, angles[0].X);
+            var rotateY = node.Transforms.Rotate("rotY", Vector3.UnitY, angles[0].Y);
+            var rotateZ = node.Transforms.Rotate("rotZ", Vector3.UnitZ, angles[0].Z);
+
+            WriteSampler(times, interpolations, i => positions[i].X, translate, "X");
+            WriteSampler(times, interpolations, i => positions[i].Y, translate, "Y");
+            WriteSampler(times, interpolations, i => positions[i].Z, translate, "Z");
+            WriteSampler(times, interpolations, i => angles[i].X, rotateX, "ANGLE");
+            WriteSampler(times, interpolations, i => angles[i].Y, rotateY, "ANGLE");
+            WriteSampler(times, interpolations, i => angles[i].Z, rotateZ, "ANGLE");
+        }
+
+        private static void WriteSampler(float[] times, string[] interpolations, Func<int, float> getValue, Dae.Transform transform, string targetName)
+        {
+            var values = new float[times.Length];
+
+            for (int i = 0; i < values.Length; ++i)
+                values[i] = getValue(i);
+
+            transform.BindAnimation(targetName, new Dae.Sampler
+            {
+                Inputs = {
+                    new Dae.Input(Dae.Semantic.Input, new Dae.Source(times, 1)),
+                    new Dae.Input(Dae.Semantic.Output, new Dae.Source(values, 1)),
+                    new Dae.Input(Dae.Semantic.Interpolation, new Dae.Source(interpolations, 1))
+                }
+            });
+        }
+    }
+}
Index: /OniSplit/DatPacker.cs
===================================================================
--- /OniSplit/DatPacker.cs	(revision 1114)
+++ /OniSplit/DatPacker.cs	(revision 1114)
@@ -0,0 +1,127 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Collections;
+
+namespace Oni
+{
+    internal sealed class DatPacker
+    {
+        private readonly List<string> inputPaths = new List<string>();
+        private string targetFilePath;
+        private bool targetBigEndian;
+        private long targetTemplateChecksum;
+
+        public List<string> InputPaths => inputPaths;
+
+        public string TargetFilePath
+        {
+            get { return targetFilePath; }
+            set { targetFilePath = value; }
+        }
+
+        public bool TargetBigEndian
+        {
+            get { return targetBigEndian; }
+            set { targetBigEndian = value; }
+        }
+
+        public long TargetTemplateChecksum
+        {
+            get { return targetTemplateChecksum; }
+            set { targetTemplateChecksum = value; }
+        }
+
+        public void Pack(InstanceFileManager fileManager, IEnumerable<string> filePaths)
+        {
+            var inputs = new List<InstanceFile>();
+            var seenFilePaths = new Set<string>(StringComparer.OrdinalIgnoreCase);
+
+            foreach (string filePath in filePaths)
+            {
+                if (seenFilePaths.Add(filePath))
+                    inputs.Add(fileManager.OpenFile(filePath));
+            }
+
+            inputs.Reverse();
+
+            var descriptors = GetImportedDescriptors(inputs);
+
+            if (descriptors.Count > 0)
+            {
+                var writer = InstanceFileWriter.CreateV31(targetTemplateChecksum, targetBigEndian);
+                writer.AddDescriptors(descriptors, true);
+
+                Console.WriteLine("Writing {0}", targetFilePath);
+
+                writer.Write(targetFilePath);
+            }
+        }
+
+        public void Import(InstanceFileManager fileManager, string[] inputDirPaths)
+        {
+            Console.WriteLine("Reading files from {0}", string.Join(";", inputDirPaths));
+
+            var inputsFiles = fileManager.OpenDirectories(inputDirPaths);
+            var descriptors = GetImportedDescriptors(inputsFiles);
+
+            if (descriptors.Count > 0)
+            {
+                var writer = InstanceFileWriter.CreateV31(targetTemplateChecksum, targetBigEndian);
+                writer.AddDescriptors(descriptors, true);
+
+                Console.WriteLine("Writing {0}", targetFilePath);
+
+                writer.Write(targetFilePath);
+            }
+        }
+
+        private static List<InstanceDescriptor> GetImportedDescriptors(List<InstanceFile> inputFiles)
+        {
+            var namedDescriptors = new Set<string>(StringComparer.Ordinal);
+            var ignored = new Set<InstanceDescriptor>();
+
+            foreach (var file in inputFiles)
+            {
+                foreach (var descriptor in file.GetNamedDescriptors())
+                {
+                    if (namedDescriptors.Contains(descriptor.FullName))
+                    {
+                        //Console.Error.WriteLine("WARNING: More than one instance has name {0}, ignoring.", descriptor.FullName);
+                        ignored.Add(descriptor);
+                    }
+                    else
+                    {
+                        namedDescriptors.Add(descriptor.FullName);
+                    }
+                }
+            }
+
+            inputFiles.Sort((x, y) => string.Compare(x.Descriptors[0].FullName, y.Descriptors[0].FullName, StringComparison.Ordinal));
+
+            var descriptors = new List<InstanceDescriptor>(4096);
+
+            foreach (var file in inputFiles)
+            {
+                foreach (var descriptor in file.Descriptors)
+                {
+                    if (ignored.Contains(descriptor))
+                        continue;
+
+                    if (descriptor.HasName)
+                    {
+                        if (descriptor.IsPlaceholder && namedDescriptors.Contains(descriptor.FullName))
+                            continue;
+
+                        namedDescriptors.Add(descriptor.FullName);
+                    }
+
+                    descriptors.Add(descriptor);
+                }
+            }
+
+            descriptors.Sort((x, y) => x.Template.IsLeaf.CompareTo(y.Template.IsLeaf));
+
+            return descriptors;
+        }
+    }
+}
Index: /OniSplit/DatUnpacker.cs
===================================================================
--- /OniSplit/DatUnpacker.cs	(revision 1114)
+++ /OniSplit/DatUnpacker.cs	(revision 1114)
@@ -0,0 +1,17 @@
+﻿namespace Oni
+{
+    internal sealed class DatUnpacker : Exporter
+    {
+        public DatUnpacker(InstanceFileManager fileManager, string outputDirPath)
+            : base(fileManager, outputDirPath)
+        {
+        }
+
+        protected override void ExportInstance(InstanceDescriptor descriptor)
+        {
+            var referencedDescriptors = descriptor.GetReferencedDescriptors();
+            var writer = InstanceFileWriter.CreateV32(referencedDescriptors);
+            writer.Write(CreateFileName(descriptor, ".oni"));
+        }
+    }
+}
Index: /OniSplit/DatWriter.cs
===================================================================
--- /OniSplit/DatWriter.cs	(revision 1114)
+++ /OniSplit/DatWriter.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿namespace Oni
+{
+    internal sealed class DatWriter : Importer
+    {
+        public DatWriter()
+        {
+            BeginImport();
+        }
+    }
+}
Index: /OniSplit/Exporter.cs
===================================================================
--- /OniSplit/Exporter.cs	(revision 1114)
+++ /OniSplit/Exporter.cs	(revision 1114)
@@ -0,0 +1,73 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+
+namespace Oni
+{
+    internal abstract class Exporter
+    {
+        private readonly InstanceFileManager fileManager;
+        private readonly string outputDirPath;
+        private readonly Dictionary<string, string> fileNames;
+        private Regex nameFilter;
+
+        protected Exporter(InstanceFileManager fileManager, string outputDirPath)
+        {
+            this.fileManager = fileManager;
+            this.outputDirPath = outputDirPath;
+            this.fileNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+
+        public InstanceFileManager InstanceFileManager => fileManager;
+
+        public string OutputDirPath => outputDirPath;
+
+        public Regex NameFilter
+        {
+            get { return nameFilter; }
+            set { nameFilter = value; }
+        }
+
+        public void ExportFiles(IEnumerable<string> sourceFilePaths)
+        {
+            Directory.CreateDirectory(outputDirPath);
+
+            foreach (var sourceFilePath in sourceFilePaths)
+                ExportFile(sourceFilePath);
+
+            Flush();
+        }
+
+        protected virtual void ExportFile(string sourceFilePath)
+        {
+            Console.WriteLine(sourceFilePath);
+
+            var file = fileManager.OpenFile(sourceFilePath);
+            var descriptors = GetSupportedDescriptors(file);
+
+            if (nameFilter != null)
+                descriptors = descriptors.FindAll(x => x.HasName && nameFilter.IsMatch(x.FullName));
+
+            foreach (var descriptor in descriptors)
+                ExportInstance(descriptor);
+        }
+
+        protected abstract void ExportInstance(InstanceDescriptor descriptor);
+
+        protected virtual void Flush()
+        {
+        }
+
+        protected virtual List<InstanceDescriptor> GetSupportedDescriptors(InstanceFile file)
+        {
+            return file.GetNamedDescriptors();
+        }
+
+        protected string CreateFileName(InstanceDescriptor descriptor, string fileExtension)
+        {
+            string name = Importer.EncodeFileName(descriptor.FullName, fileNames);
+            return Path.Combine(outputDirPath, name + fileExtension);
+        }
+    }
+}
Index: /OniSplit/Game/CharacterClass.cs
===================================================================
--- /OniSplit/Game/CharacterClass.cs	(revision 1114)
+++ /OniSplit/Game/CharacterClass.cs	(revision 1114)
@@ -0,0 +1,122 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Game
+{
+    internal class CharacterClass
+    {
+        public InstanceDescriptor Body;
+        public InstanceDescriptor[] Textures;
+        public IEnumerable<InstanceDescriptor> Animations;
+        public InstanceDescriptor Animation;
+
+        public static CharacterClass Read(InstanceDescriptor descriptor)
+        {
+            return Read(descriptor, null);
+        }
+
+        public static CharacterClass Read(InstanceDescriptor descriptor, string animationName)
+        {
+            if (descriptor.Template.Tag != TemplateTag.ONCC)
+                throw new ArgumentException(string.Format("The specified descriptor has a wrong template {0}", descriptor.Template.Tag), "descriptor");
+
+            var character = new CharacterClass();
+
+            InstanceDescriptor trma;
+            InstanceDescriptor trac;
+
+            using (var reader = descriptor.OpenRead(0xC34))
+            {
+                character.Body = reader.ReadInstance();
+                trma = reader.ReadInstance();
+                reader.Skip(0x44);
+                trac = reader.ReadInstance();
+            }
+
+            if (trma != null)
+            {
+                using (var reader = trma.OpenRead(22))
+                    character.Textures = reader.ReadInstanceArray(reader.ReadUInt16());
+            }
+
+            var animationList = new List<InstanceDescriptor>();
+
+            while (trac != null)
+            {
+                InstanceDescriptor childCollection;
+
+                using (var reader = trac.OpenRead(16))
+                {
+                    childCollection = reader.ReadInstance();
+                    reader.Skip(2);
+                    int count = reader.ReadUInt16();
+
+                    for (int i = 0; i < count; i++)
+                    {
+                        reader.Skip(8);
+                        var tram = reader.ReadInstance();
+
+                        if (tram != null)
+                            animationList.Add(tram);
+                    }
+                }
+
+                trac = childCollection;
+            }
+
+            character.Animations = animationList;
+
+            if (string.Equals(Path.GetExtension(animationName), ".oni", StringComparison.OrdinalIgnoreCase))
+            {
+                var file = descriptor.File.FileManager.OpenFile(animationName);
+
+                if (file != null && file.Descriptors[0].Template.Tag == TemplateTag.TRAM)
+                    character.Animation = file.Descriptors[0];
+            }
+            else
+            {
+                if (!string.IsNullOrEmpty(animationName) && !animationName.StartsWith("TRAM", StringComparison.Ordinal))
+                    animationName = "TRAM" + animationName;
+
+                foreach (var tram in animationList)
+                {
+                    using (var animReader = tram.OpenRead(0x15A))
+                    {
+                        int type = animReader.ReadInt16();
+                        animReader.Skip(2);
+                        int fromState = animReader.ReadInt16();
+                        int toState = animReader.ReadInt16();
+                        animReader.Skip(6);
+                        int varient = animReader.ReadInt16();
+
+                        if (!string.IsNullOrEmpty(animationName))
+                        {
+                            if (tram.FullName == animationName)
+                            {
+                                character.Animation = tram;
+                                break;
+                            }
+                        }
+                        else
+                        {
+                            if (type == 6
+                                && fromState == 7
+                                && toState == 7
+                                && varient == 0)
+                            {
+                                character.Animation = tram;
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                if (!string.IsNullOrEmpty(animationName) && character.Animation == null)
+                    Console.Error.WriteLine("Animation {0} was not found", animationName);
+            }
+
+            return character;
+        }
+    }
+}
Index: /OniSplit/Game/WeaponClass.cs
===================================================================
--- /OniSplit/Game/WeaponClass.cs	(revision 1114)
+++ /OniSplit/Game/WeaponClass.cs	(revision 1114)
@@ -0,0 +1,24 @@
+﻿using System;
+
+namespace Oni.Game
+{
+    internal class WeaponClass
+    {
+        private InstanceDescriptor geometry;
+
+        public static WeaponClass Read(InstanceDescriptor descriptor)
+        {
+            if (descriptor.Template.Tag != TemplateTag.ONWC)
+                throw new ArgumentException(string.Format("The specified descriptor has a wrong template {0}", descriptor.Template.Tag), "descriptor");
+
+            var weapon = new WeaponClass();
+
+            using (var reader = descriptor.OpenRead(0x58))
+                weapon.geometry = reader.ReadInstance();
+
+            return weapon;
+        }
+
+        public InstanceDescriptor Geometry => geometry;
+    }
+}
Index: /OniSplit/Imaging/Color.cs
===================================================================
--- /OniSplit/Imaging/Color.cs	(revision 1114)
+++ /OniSplit/Imaging/Color.cs	(revision 1114)
@@ -0,0 +1,254 @@
+﻿using System;
+using System.Globalization;
+
+namespace Oni.Imaging
+{
+    internal struct Color : IEquatable<Color>
+    {
+        private byte b, g, r, a;
+
+        public Color(byte r, byte g, byte b)
+            : this(r, g, b, 255)
+        {
+        }
+
+        public Color(byte r, byte g, byte b, byte a)
+        {
+            this.b = b;
+            this.g = g;
+            this.r = r;
+            this.a = a;
+        }
+
+        public Color(Vector3 v)
+        {
+            r = (byte)(v.X * 255.0f);
+            g = (byte)(v.Y * 255.0f);
+            b = (byte)(v.Z * 255.0f);
+            a = 255;
+        }
+
+        public Color(Vector4 v)
+        {
+            r = (byte)(v.X * 255.0f);
+            g = (byte)(v.Y * 255.0f);
+            b = (byte)(v.Z * 255.0f);
+            a = (byte)(v.W * 255.0f);
+        }
+
+        public bool IsTransparent => a != 255;
+
+        public byte R => r;
+        public byte G => g;
+        public byte B => b;
+        public byte A => a;
+
+        public int ToBgra32() => b | (g << 8) | (r << 16) | (a << 24);
+        public int ToBgr565() => (b >> 3) | ((g & 0xfc) << 3) | ((r & 0xf8) << 8);
+
+        public Vector3 ToVector3() => new Vector3(r, g, b) / 255.0f;
+        public Vector4 ToVector4() => new Vector4(r, g, b, a) / 255.0f;
+
+        public static bool operator ==(Color a, Color b) => a.Equals(b);
+        public static bool operator !=(Color a, Color b) => !a.Equals(b);
+
+        public bool Equals(Color color) => r == color.r && g == color.g && b == color.b && a == color.a;
+        public override bool Equals(object obj) => obj is Color && Equals((Color)obj);
+        public override int GetHashCode() => r.GetHashCode() ^ g.GetHashCode() ^ b.GetHashCode() ^ a.GetHashCode();
+
+        public override string ToString() => string.Format(CultureInfo.InvariantCulture, "{{R:{0} G:{1} B:{2} A:{3}}}", new object[] { r, g, b, a });
+
+        public static Color ReadBgra4444(byte[] data, int index)
+        {
+            int pixel = data[index + 0];
+            byte b = (byte)((pixel << 4) & 0xf0);
+            byte g = (byte)(pixel & 0xf0);
+            pixel = data[index + 1];
+            byte r = (byte)((pixel << 4) & 0xf0);
+            byte a = (byte)(pixel & 0xf0);
+
+            return new Color(r, g, b, a);
+        }
+
+        public static void WriteBgra4444(Color color, byte[] data, int index)
+        {
+            data[index + 0] = (byte)((color.b >> 4) | (color.g & 0xf0));
+            data[index + 1] = (byte)((color.r >> 4) | (color.a & 0xf0));
+        }
+
+        public static Color ReadBgrx5551(byte[] data, int index)
+        {
+            int pixel = data[index + 0] | (data[index + 1] << 8);
+            byte b = (byte)((pixel << 3) & 0xf8);
+            byte g = (byte)((pixel >> 2) & 0xf8);
+            byte r = (byte)((pixel >> 7) & 0xf8);
+
+            return new Color(r, g, b);
+        }
+
+        public static void WriteBgrx5551(Color color, byte[] data, int index)
+        {
+            data[index + 0] = (byte)((color.b >> 3) | ((color.g & 0x38) << 2));
+            data[index + 1] = (byte)((color.g >> 6) | ((color.r & 0xf8) >> 1) | 0x80);
+        }
+
+        public static Color ReadBgra5551(byte[] data, int index)
+        {
+            int pixel = data[index + 0] | (data[index + 1] << 8);
+            byte b = (byte)((pixel << 3) & 0xf8);
+            byte g = (byte)((pixel >> 2) & 0xf8);
+            byte r = (byte)((pixel >> 7) & 0xf8);
+            byte a = (byte)((pixel >> 15) * 255);
+
+            return new Color(r, g, b, a);
+        }
+
+        public static void WriteBgra5551(Color color, byte[] data, int index)
+        {
+            data[index + 0] = (byte)((color.b >> 3) | ((color.g & 0x38) << 2));
+            data[index + 1] = (byte)((color.g >> 6) | ((color.r & 0xf8) >> 1) | (color.a & 0x80));
+        }
+
+        public static Color ReadBgr565(byte[] data, int index)
+        {
+            int pixel = data[index + 0] | (data[index + 1] << 8);
+            byte b = (byte)((pixel << 3) & 0xf8);
+            byte g = (byte)((pixel >> 3) & 0xfc);
+            byte r = (byte)((pixel >> 8) & 0xf8);
+
+            return new Color(r, g, b);
+        }
+
+        public static void WriteBgr565(Color color, byte[] data, int index)
+        {
+            data[index + 0] = (byte)((color.b >> 3) | ((color.g & 0x1C) << 3));
+            data[index + 1] = (byte)((color.g >> 5) | (color.r & 0xf8));
+        }
+
+        public static Color ReadBgrx(byte[] data, int index)
+        {
+            byte b = data[index + 0];
+            byte g = data[index + 1];
+            byte r = data[index + 2];
+
+            return new Color(r, g, b);
+        }
+
+        public static void WriteBgrx(Color color, byte[] data, int index)
+        {
+            data[index + 0] = color.b;
+            data[index + 1] = color.g;
+            data[index + 2] = color.r;
+            data[index + 3] = 255;
+        }
+
+        public static Color ReadBgra(byte[] data, int index)
+        {
+            byte b = data[index + 0];
+            byte g = data[index + 1];
+            byte r = data[index + 2];
+            byte a = data[index + 3];
+
+            return new Color(r, g, b, a);
+        }
+
+        public static void WriteBgra(Color color, byte[] data, int index)
+        {
+            data[index + 0] = color.b;
+            data[index + 1] = color.g;
+            data[index + 2] = color.r;
+            data[index + 3] = color.a;
+        }
+
+        public static Color ReadRgbx(byte[] data, int index)
+        {
+            byte r = data[index + 0];
+            byte g = data[index + 1];
+            byte b = data[index + 2];
+
+            return new Color(r, g, b);
+        }
+
+        public static void WriteRgbx(Color color, byte[] data, int index)
+        {
+            data[index + 0] = color.r;
+            data[index + 1] = color.g;
+            data[index + 2] = color.b;
+            data[index + 3] = 255;
+        }
+
+        public static Color ReadRgba(byte[] data, int index)
+        {
+            byte r = data[index + 0];
+            byte g = data[index + 1];
+            byte b = data[index + 2];
+            byte a = data[index + 3];
+
+            return new Color(r, g, b, a);
+        }
+
+        public static void WriteRgba(Color color, byte[] data, int index)
+        {
+            data[index + 0] = color.r;
+            data[index + 1] = color.g;
+            data[index + 2] = color.b;
+            data[index + 3] = color.a;
+        }
+
+        public static Color Lerp(Color x, Color y, float amount)
+        {
+            byte r = (byte)MathHelper.Lerp(x.r, y.r, amount);
+            byte g = (byte)MathHelper.Lerp(x.g, y.g, amount);
+            byte b = (byte)MathHelper.Lerp(x.b, y.b, amount);
+            byte a = (byte)MathHelper.Lerp(x.a, y.a, amount);
+
+            return new Color(r, g, b, a);
+        }
+
+        private static readonly Color black = new Color(0, 0, 0, 255);
+        private static readonly Color white = new Color(255, 255, 255, 255);
+        private static readonly Color transparent = new Color(0, 0, 0, 0);
+
+        public static Color White => white;
+        public static Color Black => black;
+        public static Color Transparent => transparent;
+
+        public static bool TryParse(string s, out Color color)
+        {
+            color = new Color();
+            color.a = 255;
+
+            if (string.IsNullOrEmpty(s))
+                return false;
+
+            var tokens = s.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
+
+            if (tokens.Length < 3 || tokens.Length > 4)
+                return false;
+
+            if (!byte.TryParse(tokens[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.r))
+                return false;
+
+            if (!byte.TryParse(tokens[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.g))
+                return false;
+
+            if (!byte.TryParse(tokens[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.b))
+                return false;
+
+            if (tokens.Length > 3 && !byte.TryParse(tokens[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out color.a))
+                return false;
+
+            return true;
+        }
+
+        public static Color Parse(string s)
+        {
+            Color c;
+
+            if (!TryParse(s, out c))
+                throw new FormatException(string.Format("'{0}' is not a color", s));
+
+            return c;
+        }
+    }
+}
Index: /OniSplit/Imaging/DdsHeader.cs
===================================================================
--- /OniSplit/Imaging/DdsHeader.cs	(revision 1114)
+++ /OniSplit/Imaging/DdsHeader.cs	(revision 1114)
@@ -0,0 +1,315 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Imaging
+{
+    internal class DdsHeader
+    {
+        #region Private data
+
+        private enum FOURCC
+        {
+            FOURCC_NONE = 0,
+            FOURCC_DXT1 = 0x31545844
+        }
+
+        [Flags]
+        private enum DDS_FLAGS
+        {
+            DDSD_CAPS = 0x00000001,
+            DDSD_HEIGHT = 0x00000002,
+            DDSD_WIDTH = 0x00000004,
+            DDSD_PITCH = 0x00000008,
+            DDSD_PIXELFORMAT = 0x00001000,
+            DDSD_MIPMAPCOUNT = 0x00020000,
+            DDSD_LINEARSIZE = 0x00080000,
+            DDSD_DEPTH = 0x00800000
+        }
+
+        [Flags]
+        private enum DDP_FLAGS
+        {
+            DDPF_RGB = 0x00000040,
+            DDPF_FOURCC = 0x00000004,
+            DDPF_ALPHAPIXELS = 0x00000001
+        }
+
+        [Flags]
+        private enum DDS_CAPS
+        {
+            DDSCAPS_TEXTURE = 0x00001000,
+            DDSCAPS_MIPMAP = 0x00400000,
+            DDSCAPS_COMPLEX = 0x00000008
+        }
+
+        [Flags]
+        private enum DDS_CAPS2
+        {
+            DDSCAPS2_CUBEMAP = 0x00000200,
+            DDSCAPS2_VOLUME = 0x00200000
+        }
+
+        private const int DDS_MAGIC = 0x20534444;
+
+        private DDS_FLAGS flags;
+        private int height;
+        private int width;
+        private int linearSize;
+        private int depth;
+        private int mipmapCount;
+        private DDP_FLAGS formatFlags;
+        private FOURCC fourCC;
+        private int rgbBitCount;
+        private uint rBitMask;
+        private uint gBitMask;
+        private uint bBitMask;
+        private uint aBitMask;
+        private DDS_CAPS caps;
+        private DDS_CAPS2 caps2;
+        #endregion
+
+        public int Width => width;
+        public int Height => height;
+        public int MipmapCount => mipmapCount;
+
+        public SurfaceFormat GetSurfaceFormat()
+        {
+            if (fourCC == FOURCC.FOURCC_DXT1)
+                return SurfaceFormat.DXT1;
+
+            if (rgbBitCount == 32)
+            {
+                if (rBitMask == 0x00ff0000 && gBitMask == 0x0000ff00 && bBitMask == 0x000000ff)
+                {
+                    if ((formatFlags & DDP_FLAGS.DDPF_ALPHAPIXELS) == 0)
+                        return SurfaceFormat.BGRX;
+
+                    if (aBitMask == 0xff000000)
+                        return SurfaceFormat.BGRA;
+                }
+            }
+            else if (rgbBitCount == 16)
+            {
+                if (rBitMask == 0x7c00 && gBitMask == 0x03e0 && bBitMask == 0x001f)
+                {
+                    if ((formatFlags & DDP_FLAGS.DDPF_ALPHAPIXELS) == 0)
+                        return SurfaceFormat.BGRX5551;
+
+                    if (aBitMask == 0x8000)
+                        return SurfaceFormat.BGRA5551;
+                }
+                else if (rBitMask == 0x0f00 && gBitMask == 0x00f0 && bBitMask == 0x000f)
+                {
+                    if ((formatFlags & DDP_FLAGS.DDPF_ALPHAPIXELS) != 0)
+                        return SurfaceFormat.BGRA4444;
+                }
+            }
+
+            throw new NotSupportedException(string.Format("Unsupported pixel format {0} {1} {2} {3} {4} {5} {6}",
+                formatFlags,
+                fourCC, rgbBitCount,
+                rBitMask, gBitMask, bBitMask, aBitMask));
+        }
+
+        public static DdsHeader Read(BinaryReader reader)
+        {
+            if (reader.ReadInt32() != DDS_MAGIC)
+                throw new InvalidDataException("Not a DDS file");
+
+            var header = new DdsHeader();
+
+            if (reader.ReadInt32() != 124)
+                throw new InvalidDataException("Invalid DDS header size");
+
+            header.flags = (DDS_FLAGS)reader.ReadInt32();
+
+            var requiredFlags = DDS_FLAGS.DDSD_CAPS | DDS_FLAGS.DDSD_HEIGHT | DDS_FLAGS.DDSD_WIDTH | DDS_FLAGS.DDSD_PIXELFORMAT;
+
+            if ((header.flags & requiredFlags) != requiredFlags)
+                throw new InvalidDataException(string.Format("Invalid DDS header flags ({0})", header.flags));
+
+            header.height = reader.ReadInt32();
+            header.width = reader.ReadInt32();
+
+            if (header.width == 0 || header.height == 0)
+                throw new InvalidDataException("DDS file has 0 width or height");
+
+            header.linearSize = reader.ReadInt32();
+            header.depth = reader.ReadInt32();
+
+            if ((header.flags & DDS_FLAGS.DDSD_MIPMAPCOUNT) != 0)
+            {
+                header.mipmapCount = reader.ReadInt32();
+            }
+            else
+            {
+                reader.ReadInt32();
+                header.mipmapCount = 1;
+            }
+
+            reader.Position += 44;
+
+            if (reader.ReadInt32() != 32)
+                throw new InvalidDataException("Invalid DDS pixel format size");
+
+            header.formatFlags = (DDP_FLAGS)reader.ReadInt32();
+
+            if ((header.formatFlags & DDP_FLAGS.DDPF_FOURCC) != 0)
+            {
+                header.fourCC = (FOURCC)reader.ReadInt32();
+            }
+            else
+            {
+                reader.ReadInt32();
+                header.fourCC = FOURCC.FOURCC_NONE;
+            }
+
+            header.rgbBitCount = reader.ReadInt32();
+            header.rBitMask = reader.ReadUInt32();
+            header.gBitMask = reader.ReadUInt32();
+            header.bBitMask = reader.ReadUInt32();
+            header.aBitMask = reader.ReadUInt32();
+
+            header.caps = (DDS_CAPS)reader.ReadInt32();
+            header.caps2 = (DDS_CAPS2)reader.ReadInt32();
+
+            reader.Position += 12;
+
+            if (header.fourCC == FOURCC.FOURCC_NONE)
+            {
+                if (header.rgbBitCount != 16 && header.rgbBitCount != 32)
+                    throw new NotSupportedException(string.Format("Unsupported RGB bit count {0}", header.rgbBitCount));
+            }
+            else if (header.fourCC != FOURCC.FOURCC_DXT1)
+            {
+                throw new NotSupportedException(string.Format("Unsupported FOURCC {0}", header.fourCC));
+            }
+
+            return header;
+        }
+
+        public static DdsHeader Create(IList<Surface> surfaces)
+        {
+            var header = new DdsHeader();
+
+            int width = surfaces[0].Width;
+            int height = surfaces[0].Height;
+            var format = surfaces[0].Format;
+
+            header.flags = DDS_FLAGS.DDSD_CAPS | DDS_FLAGS.DDSD_HEIGHT | DDS_FLAGS.DDSD_WIDTH | DDS_FLAGS.DDSD_PIXELFORMAT;
+            header.width = width;
+            header.height = height;
+            header.caps = DDS_CAPS.DDSCAPS_TEXTURE;
+
+            switch (format)
+            {
+                case SurfaceFormat.BGRA4444:
+                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
+                    header.rgbBitCount = 16;
+                    header.aBitMask = 0xf000;
+                    header.rBitMask = 0x0f00;
+                    header.gBitMask = 0x00f0;
+                    header.bBitMask = 0x000f;
+                    break;
+                case SurfaceFormat.BGRX5551:
+                case SurfaceFormat.BGRA5551:
+                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
+                    header.rgbBitCount = 16;
+                    header.aBitMask = 0x8000;
+                    header.rBitMask = 0x7c00;
+                    header.gBitMask = 0x03e0;
+                    header.bBitMask = 0x001f;
+                    break;
+                case SurfaceFormat.BGRA:
+                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
+                    header.rgbBitCount = 32;
+                    header.aBitMask = 0xff000000;
+                    header.rBitMask = 0x00ff0000;
+                    header.gBitMask = 0x0000ff00;
+                    header.bBitMask = 0x000000ff;
+                    break;
+                case SurfaceFormat.BGRX:
+                    header.formatFlags = DDP_FLAGS.DDPF_RGB;
+                    header.rgbBitCount = 32;
+                    header.rBitMask = 0x00ff0000;
+                    header.gBitMask = 0x0000ff00;
+                    header.bBitMask = 0x000000ff;
+                    break;
+                case SurfaceFormat.RGBA:
+                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
+                    header.rgbBitCount = 32;
+                    header.aBitMask = 0x000000ff;
+                    header.rBitMask = 0x0000ff00;
+                    header.gBitMask = 0x00ff0000;
+                    header.bBitMask = 0xff000000;
+                    break;
+                case SurfaceFormat.RGBX:
+                    header.formatFlags = DDP_FLAGS.DDPF_RGB;
+                    header.rgbBitCount = 32;
+                    header.rBitMask = 0x0000ff00;
+                    header.gBitMask = 0x00ff0000;
+                    header.bBitMask = 0xff000000;
+                    break;
+                case SurfaceFormat.DXT1:
+                    header.formatFlags = DDP_FLAGS.DDPF_FOURCC;
+                    header.fourCC = FOURCC.FOURCC_DXT1;
+                    break;
+            }
+
+            switch (format)
+            {
+                case SurfaceFormat.BGRA4444:
+                case SurfaceFormat.BGRX5551:
+                case SurfaceFormat.BGRA5551:
+                    header.flags |= DDS_FLAGS.DDSD_PITCH;
+                    header.linearSize = width * 2;
+                    break;
+                case SurfaceFormat.BGRX:
+                case SurfaceFormat.BGRA:
+                case SurfaceFormat.RGBA:
+                case SurfaceFormat.RGBX:
+                    header.flags |= DDS_FLAGS.DDSD_PITCH;
+                    header.linearSize = width * 4;
+                    break;
+                case SurfaceFormat.DXT1:
+                    header.flags |= DDS_FLAGS.DDSD_LINEARSIZE;
+                    header.linearSize = Math.Max(1, width / 4) * Math.Max(1, height / 4) * 8;
+                    break;
+            }
+
+            if (surfaces.Count > 1)
+            {
+                header.flags |= DDS_FLAGS.DDSD_MIPMAPCOUNT;
+                header.mipmapCount = surfaces.Count;
+                header.caps |= DDS_CAPS.DDSCAPS_COMPLEX | DDS_CAPS.DDSCAPS_MIPMAP;
+            }
+
+            return header;
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write(DDS_MAGIC);
+            writer.Write(124);
+            writer.Write((int)flags);
+            writer.Write(height);
+            writer.Write(width);
+            writer.Write(linearSize);
+            writer.Write(depth);
+            writer.Write(mipmapCount);
+            writer.BaseStream.Seek(44, SeekOrigin.Current);
+            writer.Write(32);
+            writer.Write((int)formatFlags);
+            writer.Write((int)fourCC);
+            writer.Write(rgbBitCount);
+            writer.Write(rBitMask);
+            writer.Write(gBitMask);
+            writer.Write(bBitMask);
+            writer.Write(aBitMask);
+            writer.Write((int)caps);
+            writer.Write((int)caps2);
+            writer.BaseStream.Seek(12, SeekOrigin.Current);
+        }
+    }
+}
Index: /OniSplit/Imaging/DdsReader.cs
===================================================================
--- /OniSplit/Imaging/DdsReader.cs	(revision 1114)
+++ /OniSplit/Imaging/DdsReader.cs	(revision 1114)
@@ -0,0 +1,40 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Imaging
+{
+	internal static class DdsReader
+	{
+		public static List<Surface> Read(string filePath, bool noMipMaps)
+		{
+            var surfaces = new List<Surface>();
+
+            using (var reader = new BinaryReader(filePath))
+			{
+                var header = DdsHeader.Read(reader);
+				var format = header.GetSurfaceFormat();
+
+				for (int i = 0; i < header.MipmapCount; i++)
+				{
+					int width = Math.Max(header.Width >> i, 1);
+					int height = Math.Max(header.Height >> i, 1);
+
+					if (format == SurfaceFormat.DXT1)
+					{
+						width = Math.Max(width, 4);
+						height = Math.Max(height, 4);
+					}
+
+                    var surface = new Surface(width, height, format);
+					reader.Read(surface.Data, 0, surface.Data.Length);
+					surfaces.Add(surface);
+
+                    if (noMipMaps)
+                        break;
+				}
+			}
+
+			return surfaces;
+		}
+	}
+}
Index: /OniSplit/Imaging/DdsWriter.cs
===================================================================
--- /OniSplit/Imaging/DdsWriter.cs	(revision 1114)
+++ /OniSplit/Imaging/DdsWriter.cs	(revision 1114)
@@ -0,0 +1,23 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Imaging
+{
+    internal class DdsWriter
+    {
+        public static void Write(IList<Surface> surfaces, string filePath)
+        {
+            using (var stream = File.Create(filePath))
+            using (var writer = new BinaryWriter(stream))
+            {
+                var header = DdsHeader.Create(surfaces);
+
+                header.Write(writer);
+
+                foreach (var surface in surfaces)
+                    writer.Write(surface.Data);
+            }
+        }
+    }
+}
Index: /OniSplit/Imaging/Dxt1.cs
===================================================================
--- /OniSplit/Imaging/Dxt1.cs	(revision 1114)
+++ /OniSplit/Imaging/Dxt1.cs	(revision 1114)
@@ -0,0 +1,144 @@
+﻿using System;
+
+namespace Oni.Imaging
+{
+	internal static class Dxt1
+	{
+		public static Surface Decompress(Surface src, SurfaceFormat dstFormat)
+		{
+            var dst = new Surface(src.Width, src.Height, dstFormat);
+			var colors = new Color[4];
+			int srcOffset = 0;
+
+			for (int y = 0; y < dst.Height; y += 4)
+			{
+				for (int x = 0; x < dst.Width; x += 4)
+				{
+					colors[0] = Color.ReadBgr565(src.Data, srcOffset);
+					srcOffset += 2;
+
+					colors[1] = Color.ReadBgr565(src.Data, srcOffset);
+					srcOffset += 2;
+
+					if (colors[0].ToBgr565() > colors[1].ToBgr565())
+					{
+						colors[2] = Color.Lerp(colors[0], colors[1], 1.0f / 3.0f);
+						colors[3] = Color.Lerp(colors[0], colors[1], 2.0f / 3.0f);
+					}
+					else
+					{
+						colors[2] = Color.Lerp(colors[0], colors[1], 0.5f);
+						colors[3] = Color.Transparent;
+					}
+
+					for (int y2 = 0; y2 < 4; y2++)
+					{
+						int packedLookup = src.Data[srcOffset++];
+
+						for (int x2 = 0; x2 < 4; x2++)
+						{
+							dst[x + x2, y + y2] = colors[packedLookup & 3];
+							packedLookup >>= 2;
+						}
+					}
+				}
+			}
+
+			return dst;
+		}
+
+		public static Surface Compress(Surface src)
+		{
+			var dst = new Surface(Utils.Align4(src.Width), Utils.Align4(src.Height), SurfaceFormat.DXT1);
+
+			var block = new Vector3[16];
+			var colors = new Vector3[4];
+			var lookup = new int[16];
+			int dstOffset = 0;
+            int height = dst.Height;
+            int width = dst.Width;
+
+			for (int y = 0; y < height; y += 4)
+			{
+				for (int x = 0; x < width; x += 4)
+				{
+					for (int by = 0; by < 4; by++)
+					{
+						for (int bx = 0; bx < 4; bx++)
+							block[by * 4 + bx] = src[x + bx, y + by].ToVector3();
+					}
+
+					CompressBlock(block, lookup, colors);
+
+					Color.WriteBgr565(new Color(colors[0]), dst.Data, dstOffset);
+					dstOffset += 2;
+
+					Color.WriteBgr565(new Color(colors[1]), dst.Data, dstOffset);
+					dstOffset += 2;
+
+					for (int by = 0; by < 4; by++)
+					{
+						int packedLookup = 0;
+
+						for (int bx = 3; bx >= 0; bx--)
+							packedLookup = (packedLookup << 2) | lookup[by * 4 + bx];
+
+						dst.Data[dstOffset++] = (byte)packedLookup;
+					}
+				}
+			}
+
+			return dst;
+		}
+
+		private static void CompressBlock(Vector3[] block, int[] lookup, Vector3[] colors)
+		{
+			colors[0] = block[0];
+			colors[1] = block[0];
+
+			for (int i = 1; i < block.Length; i++)
+			{
+				colors[0] = Vector3.Min(colors[0], block[i]);
+				colors[1] = Vector3.Max(colors[1], block[i]);
+			}
+
+			int maxColor;
+
+			if (new Color(colors[0]).ToBgr565() > new Color(colors[1]).ToBgr565())
+			{
+				colors[2] = Vector3.Lerp(colors[0], colors[1], 1.0f / 3.0f);
+				colors[3] = Vector3.Lerp(colors[0], colors[1], 2.0f / 3.0f);
+				maxColor = 4;
+			}
+			else
+			{
+				colors[2] = Vector3.Lerp(colors[0], colors[1], 0.5f);
+				maxColor = 3;
+			}
+
+			for (int i = 0; i < block.Length; i++)
+			{
+				lookup[i] = LookupNearest(colors, block[i], maxColor);
+			}
+		}
+
+		private static int LookupNearest(Vector3[] colors, Vector3 pixel, int maxColor)
+		{
+			int index = 0;
+			float ds = Vector3.DistanceSquared(pixel, colors[0]);
+
+			for (int i = 1; i < maxColor; i++)
+			{
+				float newDs = Vector3.DistanceSquared(pixel, colors[i]);
+
+				if (newDs < ds)
+				{
+					ds = newDs;
+					index = i;
+				}
+			}
+
+			return index;
+		}
+	}
+}
Index: /OniSplit/Imaging/Point.cs
===================================================================
--- /OniSplit/Imaging/Point.cs	(revision 1114)
+++ /OniSplit/Imaging/Point.cs	(revision 1114)
@@ -0,0 +1,27 @@
+﻿namespace Oni.Imaging
+{
+    internal struct Point
+    {
+        public static readonly Point UnitX = new Point(1, 0);
+        public static readonly Point UnitY = new Point(0, 1);
+
+        public int X;
+        public int Y;
+
+        public Point(int x, int y)
+        {
+            X = x;
+            Y = y;
+        }
+
+        public static Point operator +(Point p0, Point p1) => new Point(p0.X + p1.X, p0.Y + p1.Y);
+        public static Point operator -(Point p0, Point p1) => new Point(p0.X - p1.X, p0.Y - p1.Y);
+
+        public static bool operator ==(Point p0, Point p1) => p0.X == p1.X && p0.Y == p1.Y;
+        public static bool operator !=(Point p0, Point p1) => p0.X != p1.X || p0.Y != p1.Y;
+
+        public override bool Equals(object obj) => obj is Point && ((Point)obj) == this;
+
+        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
+    }
+}
Index: /OniSplit/Imaging/Surface.cs
===================================================================
--- /OniSplit/Imaging/Surface.cs	(revision 1114)
+++ /OniSplit/Imaging/Surface.cs	(revision 1114)
@@ -0,0 +1,426 @@
+﻿using System;
+
+namespace Oni.Imaging
+{
+    internal class Surface
+    {
+        private int width;
+        private int height;
+        private int stride;
+        private int pixelSize;
+        private SurfaceFormat format;
+        private byte[] data;
+
+        public Surface(int width, int height)
+            : this(width, height, SurfaceFormat.RGBA)
+        {
+        }
+
+        public Surface(int width, int height, SurfaceFormat format)
+        {
+            if (format == SurfaceFormat.DXT1)
+            {
+                width = Math.Max(width, 4);
+                height = Math.Max(height, 4);
+            }
+
+            this.width = width;
+            this.height = height;
+            this.format = format;
+
+            pixelSize = GetPixelSize(format);
+            stride = pixelSize * width;
+            data = new byte[GetDataSize(width, height, format)];
+        }
+
+        public Surface(int width, int height, SurfaceFormat format, byte[] data)
+        {
+            if (format == SurfaceFormat.DXT1)
+            {
+                width = Math.Max(width, 4);
+                height = Math.Max(height, 4);
+            }
+
+            this.width = width;
+            this.height = height;
+            this.format = format;
+            this.data = data;
+
+            pixelSize = GetPixelSize(format);
+            stride = pixelSize * width;
+        }
+
+        private static int GetDataSize(int width, int height, SurfaceFormat format)
+        {
+            switch (format)
+            {
+                case SurfaceFormat.BGRA4444:
+                case SurfaceFormat.BGRX5551:
+                case SurfaceFormat.BGRA5551:
+                    return width * height * 2;
+
+                case SurfaceFormat.BGRX:
+                case SurfaceFormat.BGRA:
+                case SurfaceFormat.RGBX:
+                case SurfaceFormat.RGBA:
+                    return width * height * 4;
+
+                case SurfaceFormat.DXT1:
+                    return width * height / 2;
+
+                default:
+                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
+            }
+        }
+
+        private static int GetPixelSize(SurfaceFormat format)
+        {
+            switch (format)
+            {
+                case SurfaceFormat.BGRA4444:
+                case SurfaceFormat.BGRX5551:
+                case SurfaceFormat.BGRA5551:
+                    return 2;
+
+                case SurfaceFormat.BGRX:
+                case SurfaceFormat.BGRA:
+                case SurfaceFormat.RGBX:
+                case SurfaceFormat.RGBA:
+                    return 4;
+
+                case SurfaceFormat.DXT1:
+                    return 2;
+
+                default:
+                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
+            }
+        }
+
+        public int Width => width;
+        public int Height => height;
+        public SurfaceFormat Format => format;
+        public byte[] Data => data;
+
+        public bool HasAlpha
+        {
+            get
+            {
+                switch (format)
+                {
+                    case SurfaceFormat.BGRA:
+                    case SurfaceFormat.RGBA:
+                    case SurfaceFormat.BGRA4444:
+                    case SurfaceFormat.BGRA5551:
+                        return true;
+
+                    default:
+                        return false;
+                }
+            }
+        }
+
+        public void CleanupAlpha()
+        {
+            if (format != SurfaceFormat.BGRA5551 && format != SurfaceFormat.RGBA && format != SurfaceFormat.BGRA)
+                return;
+
+            if (!HasTransparentPixels())
+            {
+                switch (format)
+                {
+                    case SurfaceFormat.BGRA5551:
+                        format = SurfaceFormat.BGRX5551;
+                        break;
+                    case SurfaceFormat.BGRA:
+                        format = SurfaceFormat.BGRX;
+                        break;
+                    case SurfaceFormat.RGBA:
+                        format = SurfaceFormat.RGBX;
+                        break;
+                }
+            }
+        }
+
+        public bool HasTransparentPixels()
+        {
+            for (int y = 0; y < height; y++)
+            {
+                for (int x = 0; x < width; x++)
+                {
+                    Color c = GetPixel(x, y);
+
+                    if (c.A != 255)
+                        return true;
+                }
+            }
+
+            return false;
+        }
+
+        public Color this[int x, int y]
+        {
+            get
+            {
+                if (x < 0 || width <= x || y < 0 || height <= y)
+                    return Color.Black;
+
+                return GetPixel(x, y);
+            }
+            set
+            {
+                if (x < 0 || width <= x || y < 0 || height <= y)
+                    return;
+
+                SetPixel(x, y, value);
+            }
+        }
+
+        public void FlipVertical()
+        {
+            var temp = new byte[stride];
+
+            for (int y = 0; y < height / 2; y++)
+            {
+                int ry = height - y - 1;
+
+                Array.Copy(data, y * stride, temp, 0, stride);
+                Array.Copy(data, ry * stride, data, y * stride, stride);
+                Array.Copy(temp, 0, data, ry * stride, stride);
+            }
+        }
+
+        public void FlipHorizontal()
+        {
+            for (int y = 0; y < height; y++)
+            {
+                for (int x = 0; x < width / 2; x++)
+                {
+                    int rx = width - x - 1;
+
+                    Color c1 = GetPixel(x, y);
+                    Color c2 = GetPixel(rx, y);
+                    SetPixel(x, y, c2);
+                    SetPixel(rx, y, c1);
+                }
+            }
+        }
+
+        public void Rotate90()
+        {
+            for (int x = 0; x < width; x++)
+            {
+                for (int y = 0; y < height; y++)
+                {
+                    if (x <= y)
+                        continue;
+
+                    var c1 = GetPixel(x, y);
+                    var c2 = GetPixel(y, x);
+                    SetPixel(x, y, c2);
+                    SetPixel(y, x, c1);
+                }
+            }
+        }
+
+        public Surface Convert(SurfaceFormat dstFormat)
+        {
+            Surface dst;
+
+            if (format == dstFormat)
+            {
+                dst = new Surface(width, height, dstFormat, (byte[])data.Clone());
+            }
+            else if (dstFormat == SurfaceFormat.DXT1)
+            {
+                dst = Dxt1.Compress(this);
+            }
+            else if (format == SurfaceFormat.DXT1)
+            {
+                dst = Dxt1.Decompress(this, dstFormat);
+            }
+            else
+            {
+                dst = new Surface(width, height, dstFormat);
+
+                for (int y = 0; y < height; y++)
+                {
+                    for (int x = 0; x < width; x++)
+                        dst.SetPixel(x, y, GetPixel(x, y));
+                }
+            }
+
+            return dst;
+        }
+
+        public Surface Resize(int newWidth, int newHeight)
+        {
+            if (newWidth > width || newHeight > height)
+                throw new NotImplementedException();
+
+            var dst = new Surface(newWidth, newHeight, format);
+
+            if (newWidth * 2 == width && newHeight * 2 == height)
+            {
+                Halfsize(dst);
+                return dst;
+            }
+
+            float sx = (float)width / dst.width;
+            float sy = (float)height / dst.height;
+
+            for (int dstY = 0; dstY < dst.height; dstY++)
+            {
+                float top = dstY * sy;
+                float bottom = top + sy;
+
+                int yTop = (int)top;
+                int yBottom = (int)(bottom - 0.001f);
+
+                float topWeight = 1.0f - (top - yTop);
+                float bottomWeight = bottom - yBottom;
+
+                for (int dstX = 0; dstX < dst.width; dstX++)
+                {
+                    float left = dstX * sx;
+                    float right = left + sx;
+
+                    int xLeft = (int)left;
+                    int xRight = (int)(right - 0.001f);
+
+                    float leftWeight = 1.0f - (left - xLeft);
+                    float rightWeight = right - xRight;
+
+                    var sum = GetVector4(xLeft, yTop) * (leftWeight * topWeight);
+                    sum += GetVector4(xRight, yTop) * (rightWeight * topWeight);
+                    sum += GetVector4(xLeft, yBottom) * (leftWeight * bottomWeight);
+                    sum += GetVector4(xRight, yBottom) * (rightWeight * bottomWeight);
+
+                    for (int y = yTop + 1; y < yBottom; y++)
+                    {
+                        sum += GetVector4(xLeft, y) * leftWeight;
+                        sum += GetVector4(xRight, y) * rightWeight;
+                    }
+
+                    for (int x = xLeft + 1; x < xRight; x++)
+                    {
+                        sum += GetVector4(x, yTop) * topWeight;
+                        sum += GetVector4(x, yBottom) * bottomWeight;
+                    }
+
+                    for (int y = yTop + 1; y < yBottom; y++)
+                    {
+                        for (int x = xLeft + 1; x < xRight; x++)
+                            sum += GetVector4(x, y);
+                    }
+
+                    float area = (right - left) * (bottom - top);
+
+                    dst.SetPixel(dstX, dstY, new Color(sum / area));
+                }
+            }
+
+            return dst;
+        }
+
+        private void Halfsize(Surface dst)
+        {
+            int halfWidth = dst.width;
+            int halfHeight = dst.height;
+
+            for (int dstY = 0; dstY < halfHeight; dstY++)
+            {
+                int yTop = dstY * 2;
+                int yBottom = yTop + 1;
+
+                for (int dstX = 0; dstX < halfWidth; dstX++)
+                {
+                    int xLeft = dstX * 2;
+                    int xRight = xLeft + 1;
+
+                    var sum = GetVector4(xLeft, yTop);
+                    sum += GetVector4(xRight, yTop);
+                    sum += GetVector4(xLeft, yBottom);
+                    sum += GetVector4(xRight, yBottom);
+
+                    dst.SetPixel(dstX, dstY, new Color(sum / 4.0f));
+                }
+            }
+        }
+
+        private Vector4 GetVector4(int x, int y)
+        {
+            return GetPixel(x, y).ToVector4();
+        }
+
+        private Color GetPixel(int x, int y)
+        {
+            int i = x * pixelSize + y * stride;
+
+            switch (format)
+            {
+                case SurfaceFormat.BGRA4444:
+                    return Color.ReadBgra4444(data, i);
+                case SurfaceFormat.BGRX5551:
+                    return Color.ReadBgrx5551(data, i);
+                case SurfaceFormat.BGRA5551:
+                    return Color.ReadBgra5551(data, i);
+                case SurfaceFormat.BGR565:
+                    return Color.ReadBgr565(data, i);
+                case SurfaceFormat.BGRX:
+                    return Color.ReadBgrx(data, i);
+                case SurfaceFormat.BGRA:
+                    return Color.ReadBgra(data, i);
+                case SurfaceFormat.RGBX:
+                    return Color.ReadRgbx(data, i);
+                case SurfaceFormat.RGBA:
+                    return Color.ReadRgba(data, i);
+                default:
+                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
+            }
+        }
+
+        private void SetPixel(int x, int y, Color color)
+        {
+            int i = x * pixelSize + y * stride;
+
+            switch (format)
+            {
+                case SurfaceFormat.BGRA4444:
+                    Color.WriteBgra4444(color, data, i);
+                    return;
+                case SurfaceFormat.BGRX5551:
+                    Color.WriteBgrx5551(color, data, i);
+                    return;
+                case SurfaceFormat.BGRA5551:
+                    Color.WriteBgra5551(color, data, i);
+                    return;
+                case SurfaceFormat.BGR565:
+                    Color.WriteBgr565(color, data, i);
+                    return;
+                case SurfaceFormat.BGRX:
+                    Color.WriteBgrx(color, data, i);
+                    return;
+                case SurfaceFormat.BGRA:
+                    Color.WriteBgra(color, data, i);
+                    return;
+                case SurfaceFormat.RGBX:
+                    Color.WriteRgbx(color, data, i);
+                    return;
+                case SurfaceFormat.RGBA:
+                    Color.WriteRgba(color, data, i);
+                    return;
+                default:
+                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
+            }
+        }
+
+        public void Fill(int x, int y, int width, int height, Color color)
+        {
+            for (int px = x; px < x + width; px++)
+            {
+                for (int py = y; py < y + height; py++)
+                    SetPixel(px, py, color);
+            }
+        }
+    }
+}
Index: /OniSplit/Imaging/SurfaceFormat.cs
===================================================================
--- /OniSplit/Imaging/SurfaceFormat.cs	(revision 1114)
+++ /OniSplit/Imaging/SurfaceFormat.cs	(revision 1114)
@@ -0,0 +1,15 @@
+﻿namespace Oni.Imaging
+{
+    internal enum SurfaceFormat
+    {
+        BGRA4444,
+        BGRX5551,
+        BGRA5551,
+        BGR565,
+        BGRX,
+        BGRA,
+        RGBX,
+        RGBA,
+        DXT1
+    }
+}
Index: /OniSplit/Imaging/SysReader.cs
===================================================================
--- /OniSplit/Imaging/SysReader.cs	(revision 1114)
+++ /OniSplit/Imaging/SysReader.cs	(revision 1114)
@@ -0,0 +1,40 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Runtime.InteropServices;
+
+namespace Oni.Imaging
+{
+	internal static class SysReader
+	{
+		public static Surface Read(string filePath)
+		{
+			using (Bitmap bmp = new Bitmap(filePath, false))
+			{
+				SurfaceFormat surfaceFormat;
+				PixelFormat pixelFormat;
+
+				if (bmp.RawFormat == ImageFormat.Jpeg || bmp.RawFormat == ImageFormat.Bmp)
+				{
+					surfaceFormat = SurfaceFormat.BGRX;
+					pixelFormat = PixelFormat.Format32bppRgb;
+				}
+				else
+				{
+					surfaceFormat = SurfaceFormat.BGRA;
+					pixelFormat = PixelFormat.Format32bppArgb;
+				}
+
+				var surface = new Surface(bmp.Width, bmp.Height, surfaceFormat);
+                var rc = new Rectangle(0, 0, bmp.Width, bmp.Height);
+                
+                var data = bmp.LockBits(rc, ImageLockMode.ReadOnly, pixelFormat);
+				Marshal.Copy(data.Scan0, surface.Data, 0, surface.Data.Length);
+				bmp.UnlockBits(data);
+
+                return surface;
+			}
+		}
+	}
+}
Index: /OniSplit/Imaging/SysWriter.cs
===================================================================
--- /OniSplit/Imaging/SysWriter.cs	(revision 1114)
+++ /OniSplit/Imaging/SysWriter.cs	(revision 1114)
@@ -0,0 +1,43 @@
+﻿using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Oni.Imaging
+{
+    internal class SysWriter
+    {
+        public static void Write(Surface surface, string filePath)
+        {
+            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
+
+            surface = surface.Convert(SurfaceFormat.BGRA);
+
+            using (var bitmap = new Bitmap(surface.Width, surface.Height, PixelFormat.Format32bppArgb))
+            {
+                var rc = new Rectangle(0, 0, surface.Width, surface.Height);
+                
+                var pixels = bitmap.LockBits(rc, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
+                Marshal.Copy(surface.Data, 0, pixels.Scan0, surface.Data.Length);
+                bitmap.UnlockBits(pixels);
+
+                switch (Path.GetExtension(filePath))
+                {
+                    case ".png":
+                        bitmap.Save(filePath, ImageFormat.Png);
+                        break;
+                    case ".jpg":
+                        bitmap.Save(filePath, ImageFormat.Jpeg);
+                        break;
+                    case ".bmp":
+                        bitmap.Save(filePath, ImageFormat.Bmp);
+                        break;
+                    case ".tif":
+                        bitmap.Save(filePath, ImageFormat.Tiff);
+                        break;
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Imaging/TgaHeader.cs
===================================================================
--- /OniSplit/Imaging/TgaHeader.cs	(revision 1114)
+++ /OniSplit/Imaging/TgaHeader.cs	(revision 1114)
@@ -0,0 +1,163 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Imaging
+{
+    internal class TgaHeader
+    {
+        #region Private data
+        private bool hasColorMap;
+        private TgaImageType imageType;
+        private int colorMapIndex;
+        private int colorMapLength;
+        private int colorMapEntrySize;
+        private int width;
+        private int height;
+        private int pixelDepth;
+        private int imageDescriptor;
+        private bool xFlip;
+        private bool yFlip;
+        private bool hasAlpha;
+        #endregion
+
+        public TgaImageType ImageType => imageType;
+        public int Width => width;
+        public int Height => height;
+        public int PixelSize => pixelDepth / 8;
+        public bool XFlip => xFlip;
+        public bool YFlip => yFlip;
+
+        public static TgaHeader Read(BinaryReader reader)
+        {
+            int idLength = reader.ReadByte();
+
+            var header = new TgaHeader();
+            header.hasColorMap = (reader.ReadByte() != 0);
+            header.imageType = (TgaImageType)reader.ReadByte();
+            header.colorMapIndex = reader.ReadUInt16();
+            header.colorMapLength = reader.ReadUInt16();
+            header.colorMapEntrySize = reader.ReadByte();
+            reader.ReadUInt16(); // x origin
+            reader.ReadUInt16(); // y origin
+            header.width = reader.ReadUInt16();
+            header.height = reader.ReadUInt16();
+            header.pixelDepth = reader.ReadByte();
+            header.imageDescriptor = reader.ReadByte();
+
+            if (!Enum.IsDefined(typeof(TgaImageType), header.ImageType) || header.ImageType == TgaImageType.None)
+                throw new NotSupportedException(string.Format("Unsupported TGA image type {0}", header.ImageType));
+
+            if (header.Width == 0 || header.Height == 0)
+                throw new InvalidDataException("Invalid TGA file");
+
+            if (header.ImageType == TgaImageType.TrueColor
+                && (header.pixelDepth != 16
+                    && header.pixelDepth != 24
+                    && header.pixelDepth != 32))
+            {
+                throw new InvalidDataException(string.Format("Invalid true color pixel depth {0}", header.pixelDepth));
+            }
+
+            if (header.hasColorMap)
+            {
+                if (header.colorMapEntrySize != 16
+                    && header.colorMapEntrySize != 24
+                    && header.colorMapEntrySize != 32)
+                {
+                    throw new InvalidDataException(string.Format("Invalid color map entry size {0}", header.colorMapEntrySize));
+                }
+
+                if (header.ImageType != TgaImageType.ColorMapped
+                    && header.ImageType != TgaImageType.RleColorMapped)
+                {
+                    //
+                    // We have a color map but the image type does not use it so we'll just skip it.
+                    //
+
+                    reader.Position += header.colorMapLength * header.colorMapEntrySize / 8;
+                }
+            }
+
+            //
+            // Skip the identification field because we don't need it.
+            //
+
+            reader.Position += idLength;
+
+            if (header.pixelDepth == 32)
+                header.hasAlpha = ((header.imageDescriptor & 0x0f) == 8);
+            else if (header.pixelDepth == 16)
+                header.hasAlpha = ((header.imageDescriptor & 0x0f) == 1);
+            else
+                header.hasAlpha = false;
+
+            header.xFlip = ((header.imageDescriptor & 16) == 16);
+            header.yFlip = ((header.imageDescriptor & 32) == 32);
+
+            return header;
+        }
+
+        public static TgaHeader Create(int width, int height, TgaImageType imageType)
+        {
+            return new TgaHeader {
+                imageType = imageType,
+                width = width,
+                height = height,
+                pixelDepth = 32,
+                imageDescriptor = 8
+            };
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write((byte)0);
+            writer.Write((byte)(hasColorMap ? 1 : 0));
+            writer.Write((byte)imageType);
+            writer.Write((ushort)colorMapIndex);
+            writer.Write((ushort)colorMapLength);
+            writer.Write((byte)colorMapEntrySize);
+            writer.Write((ushort)0);
+            writer.Write((ushort)0);
+            writer.Write((ushort)width);
+            writer.Write((ushort)height);
+            writer.Write((byte)pixelDepth);
+            writer.Write((byte)imageDescriptor);
+        }
+
+        public SurfaceFormat GetSurfaceFormat()
+        {
+            switch (pixelDepth)
+            {
+                case 16:
+                    return hasAlpha ? SurfaceFormat.BGRA5551 : SurfaceFormat.BGRX5551;
+                case 24:
+                    return SurfaceFormat.BGRX;
+                default:
+                case 32:
+                    return hasAlpha ? SurfaceFormat.BGRA : SurfaceFormat.BGRX;
+            }
+        }
+
+        public Color GetPixel(byte[] src, int srcOffset)
+        {
+            switch (pixelDepth)
+            {
+                case 16:
+                    if (hasAlpha)
+                        return Color.ReadBgra5551(src, srcOffset);
+                    else
+                        return Color.ReadBgrx5551(src, srcOffset);
+
+                case 24:
+                    return Color.ReadBgrx(src, srcOffset);
+
+                default:
+                case 32:
+                    if (hasAlpha)
+                        return Color.ReadBgra(src, srcOffset);
+                    else
+                        return Color.ReadBgrx(src, srcOffset);
+            }
+        }
+    }
+}
Index: /OniSplit/Imaging/TgaImageType.cs
===================================================================
--- /OniSplit/Imaging/TgaImageType.cs	(revision 1114)
+++ /OniSplit/Imaging/TgaImageType.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Imaging
+{
+    internal enum TgaImageType : byte
+	{
+		None = 0,
+		ColorMapped = 1,
+		TrueColor = 2,
+		BlackAndWhite = 3,
+		RleColorMapped = 9,
+		RleTrueColor = 10,
+		RleBlackAndWhite = 11,
+	}
+}
Index: /OniSplit/Imaging/TgaReader.cs
===================================================================
--- /OniSplit/Imaging/TgaReader.cs	(revision 1114)
+++ /OniSplit/Imaging/TgaReader.cs	(revision 1114)
@@ -0,0 +1,104 @@
+﻿using System;
+
+namespace Oni.Imaging
+{
+    internal static class TgaReader
+	{
+		public static Surface Read(string filePath)
+		{
+            using (var reader = new BinaryReader(filePath))
+			{
+				var header = TgaHeader.Read(reader);
+
+				Surface surface;
+
+				switch (header.ImageType)
+				{
+					case TgaImageType.TrueColor:
+						surface = LoadTrueColor(header, reader);
+						break;
+
+					case TgaImageType.RleTrueColor:
+						surface = LoadRleTrueColor(header, reader);
+						break;
+
+					default:
+						throw new NotSupportedException(string.Format("Invalid or unsupported TGA image type {0}", header.ImageType));
+				}
+
+				if (header.XFlip)
+					surface.FlipHorizontal();
+
+				if (!header.YFlip)
+					surface.FlipVertical();
+
+                return surface;
+			}
+		}
+
+		private static Surface LoadTrueColor(TgaHeader header, BinaryReader reader)
+		{
+			int pixelSize = header.PixelSize;
+			var format = header.GetSurfaceFormat();
+
+			var dst = new Surface(header.Width, header.Height, format);
+			var src = reader.ReadBytes(header.Width * header.Height * pixelSize);
+			int srcOffset = 0;
+
+			for (int y = 0; y < header.Height; y++)
+			{
+				for (int x = 0; x < header.Width; x++)
+				{
+					dst[x, y] = header.GetPixel(src, srcOffset);
+					srcOffset += pixelSize;
+				}
+			}
+
+			return dst;
+		}
+
+		private static Surface LoadRleTrueColor(TgaHeader header, BinaryReader reader)
+		{
+			int pixelSize = header.PixelSize;
+			var format = header.GetSurfaceFormat();
+
+            var dst = new Surface(header.Width, header.Height, format);
+
+			var src = reader.ReadBytes(reader.Length - reader.Position);
+			int srcOffset = 0;
+
+			int y = 0;
+			int x = 0;
+
+			var color = Color.Black;
+
+			while (y < header.Height)
+			{
+				int packetType = src[srcOffset++];
+
+				int packetPixelCount = (packetType & 127) + 1;
+				bool isRle = ((packetType & 128) != 0);
+
+				for (int i = 0; i < packetPixelCount && y < header.Height; i++)
+				{
+					if (i == 0 || !isRle)
+					{
+						color = header.GetPixel(src, srcOffset);
+						srcOffset += pixelSize;
+					}
+
+					dst[x, y] = color;
+					x++;
+
+					if (x == header.Width)
+					{
+						x = 0;
+						y++;
+					}
+				}
+			}
+
+			return dst;
+		}
+	}
+}
Index: /OniSplit/Imaging/TgaWriter.cs
===================================================================
--- /OniSplit/Imaging/TgaWriter.cs	(revision 1114)
+++ /OniSplit/Imaging/TgaWriter.cs	(revision 1114)
@@ -0,0 +1,133 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Imaging
+{
+    internal static class TgaWriter
+    {
+        public static void Write(Surface surface, string filePath)
+        {
+            surface = surface.Convert(SurfaceFormat.BGRA);
+
+            var imageType = TgaImageType.TrueColor;
+            byte[] rleData = null;
+
+            if (surface.Width > 2 && surface.Height > 2)
+            {
+                rleData = Rle32Compress(surface.Width, surface.Height, surface.Data);
+
+                if (rleData.Length > surface.Data.Length)
+                    rleData = null;
+                else
+                    imageType = TgaImageType.RleTrueColor;
+            }
+
+            var header = TgaHeader.Create(surface.Width, surface.Height, imageType);
+
+            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
+
+            using (var stream = File.Create(filePath))
+            using (var writer = new BinaryWriter(stream))
+            {
+                header.Write(writer);
+
+                if (rleData != null)
+                    writer.Write(rleData, 0, rleData.Length);
+                else
+                    writer.Write(surface.Data, 0, surface.Data.Length);
+            }
+        }
+
+        private static byte[] Rle32Compress(int width, int height, byte[] sourceData)
+        {
+            var result = new List<byte>();
+
+            for (int y = height - 1; y >= 0; y--)
+            {
+                int lineOffset = y * width * 4;
+                int lastPixel = BitConverter.ToInt32(sourceData, y * width * 4);
+                int runStart = 0;
+                byte packetType = 64;
+
+                for (int x = 1; x < width; x++)
+                {
+                    int pixel = BitConverter.ToInt32(sourceData, x * 4 + lineOffset);
+
+                    if (pixel == lastPixel)
+                    {
+                        if (packetType == 0)
+                        {
+                            Rle32WritePackets(result, packetType, sourceData, runStart, x - 1, lineOffset);
+                            runStart = x - 1;
+                        }
+
+                        packetType = 128;
+                    }
+                    else
+                    {
+                        if (packetType == 128)
+                        {
+                            Rle32WritePackets(result, packetType, sourceData, runStart, x, lineOffset);
+                            runStart = x;
+                        }
+
+                        packetType = 0;
+                    }
+
+                    lastPixel = pixel;
+
+                    if (x == width - 1)
+                    {
+                        Rle32WritePackets(result, packetType, sourceData, runStart, width, lineOffset);
+                    }
+                }
+            }
+
+            return result.ToArray();
+        }
+
+        private static void Rle32WritePackets(List<byte> result, byte packetType, byte[] sourceData, int xStart, int xStop, int lineOffset)
+        {
+            int count = xStop - xStart;
+
+            if (count == 0)
+                return;
+
+            int startOffset = (xStart * 4) + lineOffset;
+
+            if (packetType == 128)
+            {
+                for (; count > 128; count -= 128)
+                {
+                    result.Add((byte)(packetType | 127));
+
+                    for (int i = 0; i < 4; i++)
+                        result.Add(sourceData[startOffset + i]);
+                }
+
+                result.Add((byte)(packetType | (count - 1)));
+
+                for (int i = 0; i < 4; i++)
+                    result.Add(sourceData[startOffset + i]);
+            }
+            else
+            {
+                for (; count > 128; count -= 128)
+                {
+                    result.Add((byte)(packetType | 127));
+
+                    for (int i = 0; i < 4 * 128; i++)
+                        result.Add(sourceData[startOffset + i]);
+
+                    startOffset += 4 * 128;
+                }
+
+                result.Add((byte)(packetType | (count - 1)));
+
+                for (int i = 0; i < 4 * count; i++)
+                    result.Add(sourceData[startOffset + i]);
+            }
+        }
+    }
+}
Index: /OniSplit/Importer.cs
===================================================================
--- /OniSplit/Importer.cs	(revision 1114)
+++ /OniSplit/Importer.cs	(revision 1114)
@@ -0,0 +1,127 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+
+namespace Oni
+{
+    internal abstract class Importer
+    {
+        private readonly ImporterFile file;
+        private Dictionary<string, ImporterTask> dependencies;
+
+        public Importer()
+        {
+            this.file = new ImporterFile();
+        }
+
+        protected Importer(long templateChecksum)
+        {
+            this.file = new ImporterFile(templateChecksum);
+        }
+
+        public ImporterFile ImporterFile => file;
+
+        public virtual void Import(string filePath, string outputDirPath)
+        {
+        }
+
+        public virtual void BeginImport()
+        {
+            file.BeginImport();
+
+            dependencies = new Dictionary<string, ImporterTask>();
+        }
+
+        public BinaryWriter RawWriter => file.RawWriter;
+
+        public void AddDependency(string filePath, TemplateTag type)
+        {
+            if (!dependencies.ContainsKey(filePath))
+                dependencies[filePath] = new ImporterTask(filePath, type);
+        }
+
+        public ICollection<ImporterTask> Dependencies
+        {
+            get
+            {
+                if (dependencies == null)
+                    return new ImporterTask[0];
+
+                return dependencies.Values;
+            }
+        }
+
+        public ImporterDescriptor CreateInstance(TemplateTag tag, string name = null) => file.CreateInstance(tag, name);
+
+        public int WriteRawPart(byte[] data) => file.WriteRawPart(data);
+
+        public int WriteRawPart(string text) => file.WriteRawPart(text);
+
+        public void Write(string outputDirPath) => file.Write(outputDirPath);
+
+        protected static string MakeInstanceName(TemplateTag tag, string name)
+        {
+            string tagName = tag.ToString();
+
+            if (!name.StartsWith(tagName, StringComparison.Ordinal))
+                name = tagName + name;
+
+            return name;
+        }
+
+        public static string EncodeFileName(string name, Dictionary<string, string> fileNames = null)
+        {
+            foreach (char c in Path.GetInvalidFileNameChars())
+                name = name.Replace(c.ToString(), string.Format(CultureInfo.InvariantCulture, "%{0:X2}", (int)c));
+
+            if (fileNames != null)
+            {
+                string existingName;
+
+                while (fileNames.TryGetValue(name, out existingName))
+                {
+                    int i = 0;
+
+                    if (name.Length > 4)
+                        i = 4;
+
+                    while (i < name.Length && char.ToLowerInvariant(name[i]) != char.ToLowerInvariant(existingName[i]))
+                        i++;
+
+                    name = name.Substring(0, i) + string.Format(CultureInfo.InvariantCulture, "%{0:X2}", (int)name[i]) + name.Substring(i + 1);
+                }
+
+                fileNames[name] = name;
+            }
+
+            return name;
+        }
+
+        public static string DecodeFileName(string fileName)
+        {
+            fileName = Path.GetFileNameWithoutExtension(fileName);
+
+            var buffer = new StringBuilder();
+
+            for (int i, startIndex = 0; startIndex != -1; startIndex = i)
+            {
+                i = fileName.IndexOf('%', startIndex);
+
+                if (i == -1)
+                {
+                    buffer.Append(fileName, startIndex, fileName.Length - startIndex);
+                }
+                else
+                {
+                    buffer.Append(fileName, startIndex, i - startIndex);
+                    buffer.Append((char)Int32.Parse(fileName.Substring(i + 1, 2), NumberStyles.HexNumber));
+                    i += 3;
+                }
+            }
+
+            return buffer.ToString();
+        }
+    }
+}
Index: /OniSplit/ImporterDescriptor.cs
===================================================================
--- /OniSplit/ImporterDescriptor.cs	(revision 1114)
+++ /OniSplit/ImporterDescriptor.cs	(revision 1114)
@@ -0,0 +1,26 @@
+﻿namespace Oni
+{
+    internal abstract class ImporterDescriptor
+    {
+        private readonly ImporterFile file;
+        private readonly TemplateTag tag;
+        private readonly int index;
+        private readonly string name;
+
+        protected ImporterDescriptor(ImporterFile file, TemplateTag tag, int index, string name)
+        {
+            this.file = file;
+            this.tag = tag;
+            this.index = index;
+            this.name = name;
+        }
+
+        public ImporterFile File => file;
+        public TemplateTag Tag => tag;
+        public int Index => index;
+        public string Name => name;
+
+        public abstract BinaryWriter OpenWrite();
+        public abstract BinaryWriter OpenWrite(int offset);
+    }
+}
Index: /OniSplit/ImporterDescriptorExtensions.cs
===================================================================
--- /OniSplit/ImporterDescriptorExtensions.cs	(revision 1114)
+++ /OniSplit/ImporterDescriptorExtensions.cs	(revision 1114)
@@ -0,0 +1,58 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni
+{
+    internal static class ImporterDescriptorExtensions
+    {
+        public static void WriteIndices(this ImporterDescriptor descriptor, int[] indices)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(indices.Length);
+                writer.Write(indices);
+            }
+        }
+
+        public static void WritePoints(this ImporterDescriptor descriptor, ICollection<Vector3> points)
+        {
+            var bbox = BoundingBox.CreateFromPoints(points);
+            var bsphere = BoundingSphere.CreateFromPoints(points);
+
+            using (var writer = descriptor.OpenWrite(12))
+            {
+                writer.Write(bbox);
+                writer.Write(bsphere);
+                writer.Write(points.Count);
+                writer.Write(points);
+            }
+        }
+
+        public static void WriteTexCoords(this ImporterDescriptor descriptor, ICollection<Vector2> texCoords)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(texCoords.Count);
+                writer.Write(texCoords);
+            }
+        }
+
+        public static void WriteVectors(this ImporterDescriptor descriptor, ICollection<Vector3> vectors)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(vectors.Count);
+                writer.Write(vectors);
+            }
+        }
+
+        public static void WritePlanes(this ImporterDescriptor descriptor, ICollection<Plane> planes)
+        {
+            using (var writer = descriptor.OpenWrite(20))
+            {
+                writer.Write(planes.Count);
+                writer.Write(planes);
+            }
+        }
+    }
+}
Index: /OniSplit/ImporterFile.cs
===================================================================
--- /OniSplit/ImporterFile.cs	(revision 1114)
+++ /OniSplit/ImporterFile.cs	(revision 1114)
@@ -0,0 +1,307 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Oni
+{
+    internal sealed class ImporterFile
+    {
+        private static readonly byte[] txcaPadding = new byte[480];
+        private readonly long templateChecksum = InstanceFileHeader.OniPCTemplateChecksum;
+        private MemoryStream rawStream;
+        private BinaryWriter rawWriter;
+        private List<ImporterFileDescriptor> descriptors;
+        private int nameOffset;
+
+        #region private class FileHeader
+
+        private class FileHeader
+        {
+            public const int Size = 64;
+
+            public long TemplateChecksum;
+            public int Version;
+            public int InstanceCount;
+            public int DataTableOffset;
+            public int DataTableSize;
+            public int NameTableOffset;
+            public int NameTableSize;
+            public int RawTableOffset;
+            public int RawTableSize;
+
+            public void Write(BinaryWriter writer)
+            {
+                writer.Write(TemplateChecksum);
+                writer.Write(Version);
+                writer.Write(InstanceFileHeader.Signature);
+                writer.Write(InstanceCount);
+                writer.Write(0ul);
+                writer.Write(DataTableOffset);
+                writer.Write(DataTableSize);
+                writer.Write(NameTableOffset);
+                writer.Write(NameTableSize);
+                writer.Write(RawTableOffset);
+                writer.Write(RawTableSize);
+                writer.Write(0ul);
+            }
+        }
+
+        #endregion
+
+        public ImporterFile()
+        {
+        }
+
+        public ImporterFile(long templateChecksum)
+        {
+            this.templateChecksum = templateChecksum;
+        }
+
+        public void BeginImport()
+        {
+            rawStream = null;
+            rawWriter = null;
+
+            descriptors = new List<ImporterFileDescriptor>();
+            nameOffset = 0;
+        }
+
+        public BinaryWriter RawWriter
+        {
+            get
+            {
+                if (rawWriter == null)
+                {
+                    rawStream = new MemoryStream();
+                    rawWriter = new BinaryWriter(rawStream);
+                    rawWriter.Write(new byte[32]);
+                }
+
+                return rawWriter;
+            }
+        }
+
+        public ImporterDescriptor CreateInstance(TemplateTag tag, string name = null)
+        {
+            var descriptor = new ImporterFileDescriptor(this, tag, descriptors.Count, MakeInstanceName(tag, name));
+
+            descriptors.Add(descriptor);
+
+            return descriptor;
+        }
+
+        public int WriteRawPart(byte[] data)
+        {
+            int offset = RawWriter.Align32();
+            RawWriter.Write(data);
+            return offset;
+        }
+
+        public int WriteRawPart(string text)
+        {
+            return WriteRawPart(Encoding.UTF8.GetBytes(text));
+        }
+
+        private sealed class ImporterFileDescriptor : ImporterDescriptor
+        {
+            public const int Size = 20;
+
+            private int nameOffset;
+            private int dataOffset;
+            private byte[] data;
+
+            public ImporterFileDescriptor(ImporterFile file, TemplateTag tag, int index, string name)
+                : base(file, tag, index, name)
+            {
+                if (!string.IsNullOrEmpty(name))
+                {
+                    nameOffset = file.nameOffset;
+                    file.nameOffset += name.Length + 1;
+                }
+            }
+
+            public int NameOffset
+            {
+                get { return nameOffset; }
+                set { nameOffset = value; }
+            }
+
+            public int DataOffset
+            {
+                get { return dataOffset; }
+                set { dataOffset = value; }
+            }
+
+            public int DataSize
+            {
+                get
+                {
+                    if (data == null)
+                        return 0;
+
+                    return data.Length + 8;
+                }
+            }
+
+            public byte[] Data
+            {
+                get { return data; }
+            }
+
+            public override BinaryWriter OpenWrite()
+            {
+                if (data != null)
+                    throw new InvalidOperationException("Descriptor has already been written to");
+
+                return new InstanceWriter(this);
+            }
+
+            public override BinaryWriter OpenWrite(int offset)
+            {
+                if (data != null)
+                    throw new InvalidOperationException("Descriptor has already been written to");
+
+                var writer = new InstanceWriter(this);
+                writer.Skip(offset);
+                return writer;
+            }
+
+            public void Close(byte[] data)
+            {
+                this.data = data;
+            }
+        }
+
+        private class InstanceWriter : BinaryWriter
+        {
+            private readonly ImporterFileDescriptor descriptor;
+
+            public InstanceWriter(ImporterFileDescriptor descriptor)
+                : base(new MemoryStream())
+            {
+                this.descriptor = descriptor;
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+                var stream = (MemoryStream)BaseStream;
+
+                if (descriptor.Tag == TemplateTag.TXCA)
+                    stream.Write(txcaPadding, 0, txcaPadding.Length);
+                else if (stream.Position > stream.Length)
+                    stream.SetLength(stream.Position);
+
+                descriptor.Close(stream.ToArray());
+
+                base.Dispose(disposing);
+            }
+        }
+
+        public void Write(string outputDirPath)
+        {
+            var filePath = Path.Combine(outputDirPath, Importer.EncodeFileName(descriptors[0].Name) + ".oni");
+
+            Directory.CreateDirectory(outputDirPath);
+
+            int nameTableOffset = Utils.Align32(FileHeader.Size + ImporterFileDescriptor.Size * descriptors.Count);
+            int nameTableSize = nameOffset;
+            int dataTableOffset = Utils.Align32(nameTableOffset + nameOffset);
+            int dataTableSize = 0;
+
+            foreach (var descriptor in descriptors.Where(d => d.Data != null))
+            {
+                descriptor.DataOffset = dataTableSize + 8;
+
+                dataTableSize += Utils.Align32(descriptor.DataSize);
+            }
+
+            var header = new FileHeader
+            {
+                TemplateChecksum = templateChecksum,
+                Version = InstanceFileHeader.Version32,
+                InstanceCount = descriptors.Count,
+                DataTableOffset = dataTableOffset,
+                DataTableSize = dataTableSize,
+                NameTableOffset = nameTableOffset,
+                NameTableSize = nameTableSize
+            };
+
+            using (var stream = File.Create(filePath))
+            using (var writer = new BinaryWriter(stream))
+            {
+                bool hasRawParts = (rawStream != null && rawStream.Length > 32);
+
+                if (hasRawParts)
+                {
+                    header.RawTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize);
+                    header.RawTableSize = (int)rawStream.Length;
+                }
+
+                header.Write(writer);
+
+                foreach (var descriptor in descriptors)
+                {
+                    WriteDescriptor(writer, descriptor);
+                }
+
+                writer.Position = header.NameTableOffset;
+
+                foreach (var entry in descriptors)
+                {
+                    if (entry.Name != null)
+                        writer.Write(entry.Name, entry.Name.Length + 1);
+                }
+
+                writer.Position = header.DataTableOffset;
+
+                foreach (var descriptor in descriptors.Where(d => d.Data != null))
+                {
+                    writer.Align32();
+                    writer.WriteInstanceId(descriptor.Index);
+                    writer.Write(0);
+                    writer.Write(descriptor.Data);
+                }
+
+                if (hasRawParts)
+                {
+                    writer.Position = header.RawTableOffset;
+                    rawStream.WriteTo(stream);
+                }
+            }
+        }
+
+        private void WriteDescriptor(BinaryWriter writer, ImporterFileDescriptor descriptor)
+        {
+            var flags = InstanceDescriptorFlags.None;
+
+            if (descriptor.Name == null)
+                flags |= InstanceDescriptorFlags.Private;
+
+            if (descriptor.Data == null)
+                flags |= InstanceDescriptorFlags.Placeholder;
+
+            if (descriptor.Name == null && descriptor.Data == null)
+                throw new InvalidOperationException("Link descriptors must have names");
+
+            writer.Write((int)descriptor.Tag);
+            writer.Write(descriptor.DataOffset);
+            writer.Write(descriptor.NameOffset);
+            writer.Write(descriptor.DataSize);
+            writer.Write((int)flags);
+        }
+
+        private static string MakeInstanceName(TemplateTag tag, string name)
+        {
+            if (string.IsNullOrEmpty(name))
+                return null;
+
+            var tagName = tag.ToString();
+
+            if (!name.StartsWith(tagName, StringComparison.Ordinal))
+                name = tagName + name;
+
+            return name;
+        }
+    }
+}
Index: /OniSplit/ImporterTask.cs
===================================================================
--- /OniSplit/ImporterTask.cs	(revision 1114)
+++ /OniSplit/ImporterTask.cs	(revision 1114)
@@ -0,0 +1,22 @@
+﻿namespace Oni
+{
+    internal struct ImporterTask
+    {
+        private readonly string filePath;
+        private readonly TemplateTag type;
+
+        public ImporterTask(string filePath)
+            : this(filePath, TemplateTag.NONE)
+        {
+        }
+
+        public ImporterTask(string filePath, TemplateTag type)
+        {
+            this.filePath = filePath;
+            this.type = type;
+        }
+
+        public string FilePath => filePath;
+        public TemplateTag Type => type;
+    }
+}
Index: /OniSplit/InstanceDescriptor.cs
===================================================================
--- /OniSplit/InstanceDescriptor.cs	(revision 1114)
+++ /OniSplit/InstanceDescriptor.cs	(revision 1114)
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Metadata;
+
+namespace Oni
+{
+    internal sealed class InstanceDescriptor
+    {
+        private InstanceFile file;
+        private string fullName;
+        private int index;
+        private Template template;
+        private int dataOffset;
+        private int nameOffset;
+        private int dataSize;
+        private InstanceDescriptorFlags flags;
+
+        internal static InstanceDescriptor Read(InstanceFile file, BinaryReader reader, int index)
+        {
+            var metadata = InstanceMetadata.GetMetadata(file);
+
+            var descriptor = new InstanceDescriptor
+            {
+                file = file,
+                index = index,
+                template = metadata.GetTemplate((TemplateTag)reader.ReadInt32()),
+                dataOffset = reader.ReadInt32(),
+                nameOffset = reader.ReadInt32(),
+                dataSize = reader.ReadInt32(),
+                flags = (InstanceDescriptorFlags)(reader.ReadInt32() & 0xff)
+            };
+
+            if (descriptor.IsPlaceholder && !descriptor.HasName)
+                throw new InvalidDataException("Empty descriptors must have names");
+
+            return descriptor;
+        }
+
+        public InstanceFile File => file;
+
+        public int Index => index;
+
+        public string FullName
+        {
+            get
+            {
+                if (fullName == null)
+                    fullName = index.ToString();
+
+                return fullName;
+            }
+        }
+
+        public string Name
+        {
+            get
+            {
+                string name = FullName;
+
+                if (name.StartsWith(Template.Tag.ToString(), StringComparison.Ordinal))
+                    name = name.Substring(4);
+
+                return name;
+            }
+        }
+
+        public Template Template => template;
+
+        public bool HasName => ((flags & InstanceDescriptorFlags.Private) == 0);
+
+        public bool IsPlaceholder => ((flags & InstanceDescriptorFlags.Placeholder) != 0 || dataSize == 0 || dataOffset == 0);
+
+        public int DataOffset => file.Header.DataTableOffset + dataOffset;
+
+        public int DataSize => dataSize;
+
+        internal void ReadName(Dictionary<int, string> names)
+        {
+            if (!HasName)
+                return;
+
+            if (IsPlaceholder || file.Header.Version == InstanceFileHeader.Version31)
+            {
+                names.TryGetValue(nameOffset, out fullName);
+            }
+            else
+            {
+                fullName = Importer.DecodeFileName(file.FilePath);
+
+                string tagName = template.Tag.ToString();
+
+                if (!fullName.StartsWith(tagName, StringComparison.Ordinal))
+                    fullName = tagName + fullName;
+            }
+        }
+
+        internal void SetName(string newName)
+        {
+            flags &= ~InstanceDescriptorFlags.Private;
+            fullName = newName;
+        }
+
+        public List<InstanceDescriptor> GetReferencedDescriptors() => file.GetReferencedDescriptors(this);
+
+        internal BinaryReader OpenRead()
+        {
+            if (IsPlaceholder)
+                throw new InvalidOperationException();
+
+            return new BinaryReader(file.FilePath, file)
+            {
+                Position = DataOffset
+            };
+        }
+
+        internal BinaryReader OpenRead(int offset)
+        {
+            if (IsPlaceholder)
+                throw new InvalidOperationException();
+
+            return new BinaryReader(file.FilePath, file)
+            {
+                Position = DataOffset + offset
+            };
+        }
+
+        internal BinaryReader GetRawReader(int offset) => file.GetRawReader(offset);
+
+        internal BinaryReader GetSepReader(int offset)
+        {
+            if (!IsMacFile)
+                return GetRawReader(offset);
+
+            return file.GetSepReader(offset);
+        }
+
+        internal bool IsMacFile => (file.Header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum);
+
+        public long TemplateChecksum => file.Header.TemplateChecksum;
+
+        public string FilePath => file.FilePath;
+
+        public bool HasRawParts()
+        {
+            if (IsPlaceholder)
+                return false;
+
+            switch (template.Tag)
+            {
+                case TemplateTag.AKVA:
+                case TemplateTag.AGDB:
+                case TemplateTag.BINA:
+                case TemplateTag.TXMP:
+                case TemplateTag.OSBD:
+                case TemplateTag.SNDD:
+                case TemplateTag.SUBT:
+                case TemplateTag.TRAM:
+                    return true;
+
+                default:
+                    return false;
+            }
+        }
+    }
+}
Index: /OniSplit/InstanceDescriptorFlags.cs
===================================================================
--- /OniSplit/InstanceDescriptorFlags.cs	(revision 1114)
+++ /OniSplit/InstanceDescriptorFlags.cs	(revision 1114)
@@ -0,0 +1,13 @@
+using System;
+
+namespace Oni
+{
+    [Flags]
+    internal enum InstanceDescriptorFlags
+    {
+        None = 0x0000,
+        Private = 0x0001,
+        Placeholder = 0x0002,
+        Shared = 0x0008
+    }
+}
Index: /OniSplit/InstanceFile.cs
===================================================================
--- /OniSplit/InstanceFile.cs	(revision 1114)
+++ /OniSplit/InstanceFile.cs	(revision 1114)
@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Metadata;
+
+namespace Oni
+{
+    internal sealed class InstanceFile
+    {
+        private readonly InstanceFileManager fileManager;
+        private readonly string filePath;
+        private InstanceFileHeader header;
+        private Dictionary<int, int> rawParts;
+        private Dictionary<int, int> sepParts;
+        private string rawFilePath;
+        private string sepFilePath;
+        private List<InstanceDescriptor> descriptors;
+        private IList<InstanceDescriptor> readOnlyDescriptors;
+
+        private InstanceFile(InstanceFileManager fileManager, string filePath)
+        {
+            this.fileManager = fileManager;
+            this.filePath = filePath;
+        }
+
+        public static InstanceFile Read(InstanceFileManager fileManager, string filePath)
+        {
+            var file = new InstanceFile(fileManager, filePath);
+
+            using (var reader = new BinaryReader(filePath))
+            {
+                var header = InstanceFileHeader.Read(reader);
+                var descriptors = new List<InstanceDescriptor>(header.InstanceCount);
+
+                file.header = header;
+                file.descriptors = descriptors;
+
+                for (int i = 0; i < file.header.InstanceCount; i++)
+                    descriptors.Add(InstanceDescriptor.Read(file, reader, i));
+
+                var names = ReadNames(header, reader);
+
+                for (int i = 0; i < file.header.InstanceCount; i++)
+                    descriptors[i].ReadName(names);
+            }
+
+            //
+            // Force AGDB instances to have a name so they get exported into separate .oni files
+            // instead of being exported inside an AKEV.oni file. This code assumes that there
+            // is only 1 AGDB/AKEV file per .dat file (or none at all).
+            //
+
+            foreach (var descriptor in file.descriptors)
+            {
+                if (descriptor.Template.Tag != TemplateTag.AGDB)
+                    continue;
+
+                var akevDescriptors = file.GetNamedDescriptors(TemplateTag.AKEV);
+
+                if (akevDescriptors.Count == 1)
+                {
+                    string agdbName = "AGDB" + akevDescriptors[0].Name;
+                    descriptor.SetName(agdbName);
+                    break;
+                }
+            }
+
+            return file;
+        }
+
+        private static Dictionary<int, string> ReadNames(InstanceFileHeader header, BinaryReader reader)
+        {
+            reader.Position = header.NameTableOffset;
+
+            var nameTable = reader.ReadBytes(header.NameTableSize);
+            int nameOffset = 0;
+
+            var names = new Dictionary<int, string>(header.NameCount);
+            var buffer = new char[64];
+
+            while (nameOffset < nameTable.Length)
+            {
+                int i = 0;
+
+                while (true)
+                {
+                    byte c = nameTable[nameOffset + i];
+
+                    if (c == 0)
+                        break;
+
+                    buffer[i++] = (char)c;
+                }
+
+                names.Add(nameOffset, new string(buffer, 0, i));
+                nameOffset += i + 1;
+            }
+
+            return names;
+        }
+
+        public InstanceFileManager FileManager
+        {
+            get { return fileManager; }
+        }
+
+        public string FilePath
+        {
+            get { return filePath; }
+        }
+
+        public InstanceFileHeader Header
+        {
+            get { return header; }
+        }
+
+        public List<InstanceDescriptor> GetReferencedDescriptors(InstanceDescriptor descriptor)
+        {
+            var result = new List<InstanceDescriptor>();
+
+            result.Add(descriptor);
+
+            var stack = new Stack<InstanceDescriptor>();
+            var seen = new bool[descriptors.Count];
+
+            stack.Push(descriptor);
+            seen[descriptor.Index] = true;
+
+            using (var reader = new BinaryReader(filePath))
+            {
+                var linkVisitor = new LinkVisitor(reader);
+
+                while (stack.Count > 0)
+                {
+                    descriptor = stack.Pop();
+                    reader.Position = descriptor.DataOffset;
+                    linkVisitor.Links.Clear();
+
+                    descriptor.Template.Type.Accept(linkVisitor);
+
+                    foreach (int id in linkVisitor.Links)
+                    {
+                        if (!seen[id >> 8])
+                        {
+                            var referencedDescriptor = GetDescriptor(id);
+
+                            if (!referencedDescriptor.IsPlaceholder && !referencedDescriptor.HasName)
+                                stack.Push(referencedDescriptor);
+
+                            result.Add(referencedDescriptor);
+                            seen[referencedDescriptor.Index] = true;
+                        }
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        public int GetRawPartSize(int offset)
+        {
+            EnsureRawAndSepParts();
+            return rawParts[offset];
+        }
+
+        public int GetSepPartSize(int offset)
+        {
+            EnsureRawAndSepParts();
+            return sepParts[offset];
+        }
+
+        public string RawFilePath
+        {
+            get
+            {
+                if (rawFilePath == null)
+                {
+                    if (header.Version == InstanceFileHeader.Version31)
+                        rawFilePath = Path.ChangeExtension(filePath, ".raw");
+                    else
+                        rawFilePath = filePath;
+                }
+
+                return rawFilePath;
+            }
+        }
+
+        public string SepFilePath
+        {
+            get
+            {
+                if (sepFilePath == null)
+                    sepFilePath = Path.ChangeExtension(filePath, ".sep");
+
+                return sepFilePath;
+            }
+        }
+
+        public BinaryReader GetRawReader(int offset)
+        {
+            return GetBinaryReader(offset, RawFilePath);
+        }
+
+        public BinaryReader GetSepReader(int offset)
+        {
+            return GetBinaryReader(offset, SepFilePath);
+        }
+
+        private void EnsureRawAndSepParts()
+        {
+            if (rawParts != null)
+                return;
+
+            rawParts = new Dictionary<int, int>();
+            sepParts = new Dictionary<int, int>();
+
+            InstanceMetadata.GetRawAndSepParts(this, rawParts, sepParts);
+        }
+
+        private BinaryReader GetBinaryReader(int offset, string binaryFilePath)
+        {
+            var reader = new BinaryReader(binaryFilePath);
+            reader.Position = offset + header.RawTableOffset;
+            return reader;
+        }
+
+        public InstanceDescriptor ResolveLink(int id)
+        {
+            var descriptor = GetDescriptor(id);
+
+            if (descriptor == null || !descriptor.IsPlaceholder)
+                return descriptor;
+
+            if (!descriptor.HasName)
+                return null;
+
+            var file = fileManager.FindInstance(descriptor.FullName, this);
+
+            if (file == null || file == this)
+            {
+                Console.Error.WriteLine("Cannot find instance '{0}'", descriptor.FullName);
+                return null;
+            }
+
+            if (file.header.Version == InstanceFileHeader.Version32)
+                return file.GetDescriptor(1);
+
+            foreach (var target in file.descriptors)
+            {
+                if (target.HasName && target.FullName == descriptor.FullName)
+                    return target;
+            }
+
+            return null;
+        }
+
+        public InstanceDescriptor GetDescriptor(int id)
+        {
+            if (id == 0)
+                return null;
+
+            return descriptors[id >> 8];
+        }
+
+        public IList<InstanceDescriptor> Descriptors
+        {
+            get
+            {
+                if (readOnlyDescriptors == null)
+                    readOnlyDescriptors = descriptors.AsReadOnly();
+
+                return readOnlyDescriptors;
+            }
+        }
+
+        public List<InstanceDescriptor> GetNamedDescriptors()
+        {
+            return descriptors.FindAll(x => x.HasName && !x.IsPlaceholder);
+        }
+
+        public List<InstanceDescriptor> GetNamedDescriptors(TemplateTag tag)
+        {
+            return descriptors.FindAll(x => x.Template.Tag == tag && x.HasName && !x.IsPlaceholder);
+        }
+
+        public List<InstanceDescriptor> GetPlaceholders()
+        {
+            return descriptors.FindAll(x => x.HasName && x.IsPlaceholder);
+        }
+    }
+}
Index: /OniSplit/InstanceFileHeader.cs
===================================================================
--- /OniSplit/InstanceFileHeader.cs	(revision 1114)
+++ /OniSplit/InstanceFileHeader.cs	(revision 1114)
@@ -0,0 +1,90 @@
+using System;
+using System.IO;
+
+namespace Oni
+{
+    internal sealed class InstanceFileHeader
+    {
+        public const long OniPCTemplateChecksum = 0x0003bcdf33dc271f;
+        public const long OniMacTemplateChecksum = 0x0003bcdf23c13061;
+        public const int Version31 = 0x56523331;
+        public const int Version32 = 0x56523332;
+        public const long Signature = 0x0008001000140040;
+
+        #region Private data
+        private long templateChecksum;
+        private int version;
+        private long signature;
+        private int instanceCount;
+        private int nameCount;
+        private int templateCount;
+        private int dataTableOffset;
+        private int dataTableSize;
+        private int nameTableOffset;
+        private int nameTableSize;
+        private int rawTableOffset;
+        private int rawTableSize;
+        #endregion
+
+        internal static InstanceFileHeader Read(BinaryReader reader)
+        {
+            var header = new InstanceFileHeader
+            {
+                templateChecksum = reader.ReadInt64(),
+                version = reader.ReadInt32(),
+                signature = reader.ReadInt64()
+            };
+
+            ValidateHeader(header);
+
+            header.instanceCount = reader.ReadInt32();
+            header.nameCount = reader.ReadInt32();
+            header.templateCount = reader.ReadInt32();
+            header.dataTableOffset = reader.ReadInt32();
+            header.dataTableSize = reader.ReadInt32();
+            header.nameTableOffset = reader.ReadInt32();
+            header.nameTableSize = reader.ReadInt32();
+
+            if (header.version == Version32)
+            {
+                header.rawTableOffset = reader.ReadInt32();
+                header.rawTableSize = reader.ReadInt32();
+
+                reader.Skip(8);
+            }
+            else
+            {
+                reader.Skip(16);
+            }
+
+            return header;
+        }
+
+        private static void ValidateHeader(InstanceFileHeader header)
+        {
+            if (header.templateChecksum != OniPCTemplateChecksum && header.templateChecksum != OniMacTemplateChecksum)
+            {
+                header.templateChecksum = OniMacTemplateChecksum;
+                //throw new InvalidDataException("Invalid template checksum");
+            }
+
+            if (header.version != Version31 && header.version != Version32)
+                throw new InvalidDataException("Unknown file version");
+
+            if (header.version == Version31 && header.signature != Signature)
+                throw new InvalidDataException("Invalid file signature");
+        }
+
+        public long TemplateChecksum => templateChecksum;
+        public int Version => version;
+        public int InstanceCount => instanceCount;
+        public int NameCount => nameCount;
+        public int TemplateCoun => templateCount;
+        public int DataTableOffset => dataTableOffset;
+        public int DataTableSize => dataTableSize;
+        public int NameTableOffset => nameTableOffset;
+        public int NameTableSize => nameTableSize;
+        public int RawTableOffset => rawTableOffset;
+        public int RawTableSize => rawTableSize;
+    }
+}
Index: /OniSplit/InstanceFileManager.cs
===================================================================
--- /OniSplit/InstanceFileManager.cs	(revision 1114)
+++ /OniSplit/InstanceFileManager.cs	(revision 1114)
@@ -0,0 +1,196 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Collections;
+
+namespace Oni
+{
+    internal sealed class InstanceFileManager
+    {
+        private readonly List<string> searchPaths = new List<string>();
+        private readonly Dictionary<string, InstanceFile> loadedFiles = new Dictionary<string, InstanceFile>(StringComparer.OrdinalIgnoreCase);
+        private Dictionary<string, string> files;
+
+        public InstanceFile OpenFile(string filePath)
+        {
+            InstanceFile file;
+
+            if (!loadedFiles.TryGetValue(filePath, out file))
+            {
+                try
+                {
+                    file = InstanceFile.Read(this, filePath);
+                }
+                catch (Exception ex)
+                {
+                    Console.Error.WriteLine("Error opening file {0}: {1}", filePath, ex.Message);
+                    throw;
+                }
+
+                loadedFiles.Add(filePath, file);
+            }
+
+            return file;
+        }
+
+        public List<InstanceFile> OpenDirectories(string[] dirPaths)
+        {
+            var files = new List<InstanceFile>();
+            var seenFileNames = new Set<string>(StringComparer.Ordinal);
+
+            Array.Reverse(dirPaths);
+
+            foreach (string dirPath in dirPaths)
+            {
+                var filePaths = FindFiles(dirPath);
+
+                foreach (string filePath in filePaths)
+                {
+                    if (!seenFileNames.Contains(Path.GetFileName(filePath)))
+                        files.Add(OpenFile(filePath));
+                }
+
+                foreach (string filePath in filePaths)
+                    seenFileNames.Add(Path.GetFileName(filePath));
+            }
+
+            return files;
+        }
+
+        public List<InstanceFile> OpenDirectory(string dirPath)
+        {
+            var files = new List<InstanceFile>();
+
+            foreach (string filePath in FindFiles(dirPath))
+                files.Add(OpenFile(filePath));
+
+            return files;
+        }
+
+        public void AddSearchPath(string path)
+        {
+            if (Directory.Exists(path))
+                searchPaths.Add(path);
+        }
+
+        public InstanceFile FindInstance(string instanceName)
+        {
+            if (files == null)
+            {
+                files = new Dictionary<string, string>(StringComparer.Ordinal);
+
+                foreach (string searchPath in searchPaths)
+                {
+                    foreach (string filePath in FindFiles(searchPath))
+                    {
+                        string name = Importer.DecodeFileName(filePath);
+
+                        if (!files.ContainsKey(name))
+                            files[name] = filePath;
+                    }
+                }
+            }
+
+            string instanceFilePath;
+
+            if (!files.TryGetValue(instanceName, out instanceFilePath))
+                return null;
+
+            var file = OpenFile(instanceFilePath);
+
+            if (file == null)
+                return null;
+
+            var descriptor = file.Descriptors[0];
+
+            if (file.Header.Version != InstanceFileHeader.Version32)
+                return null;
+
+            if (descriptor == null || !descriptor.HasName || descriptor.FullName != instanceName)
+                return null;
+
+            return file;
+        }
+
+        public InstanceFile FindInstance(string instanceName, InstanceFile baseFile)
+        {
+            if (files == null)
+            {
+                files = new Dictionary<string, string>(StringComparer.Ordinal);
+
+                foreach (string filePath in FindFiles(Path.GetDirectoryName(baseFile.FilePath)))
+                    files[Importer.DecodeFileName(filePath)] = filePath;
+
+                foreach (string searchPath in searchPaths)
+                {
+                    foreach (string filePath in FindFiles(searchPath))
+                    {
+                        string name = Importer.DecodeFileName(filePath);
+
+                        if (!files.ContainsKey(name))
+                            files[name] = filePath;
+                    }
+                }
+            }
+
+            string instanceFilePath;
+
+            if (!files.TryGetValue(instanceName, out instanceFilePath))
+            {
+                if (instanceName.Length > 4)
+                    instanceName = instanceName.Substring(4);
+
+                if (!files.TryGetValue(instanceName, out instanceFilePath))
+                {
+                    string level0FilePath = Path.Combine(Path.GetDirectoryName(baseFile.FilePath), "level0_Final.dat");
+
+                    if (!File.Exists(level0FilePath))
+                        return null;
+
+                    return OpenFile(level0FilePath);
+                }
+            }
+
+            var file = OpenFile(instanceFilePath);
+
+            if (file == null || file == baseFile)
+                return null;
+
+            var descriptor = file.Descriptors[0];
+
+            if (file.Header.Version == InstanceFileHeader.Version32)
+                return file;
+
+            if (descriptor == null || !descriptor.HasName || descriptor.FullName != instanceName)
+                return null;
+
+            return file;
+        }
+
+        private static List<string> FindFiles(string dirPath)
+        {
+            var files = new List<string>();
+
+            if (Directory.Exists(dirPath))
+                FindFilesRecursive(dirPath, files);
+
+            return files;
+        }
+
+        private static void FindFilesRecursive(string dirPath, List<string> files)
+        {
+            files.AddRange(Directory.GetFiles(dirPath, "*.oni"));
+
+            foreach (string childDirPath in Directory.GetDirectories(dirPath))
+            {
+                string name = Path.GetFileName(childDirPath);
+
+                if (!string.Equals(name, "_noimport", StringComparison.OrdinalIgnoreCase)
+                    && !string.Equals(name, "noimport", StringComparison.OrdinalIgnoreCase))
+                {
+                    FindFilesRecursive(childDirPath, files);
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/InstanceFileOperations.cs
===================================================================
--- /OniSplit/InstanceFileOperations.cs	(revision 1114)
+++ /OniSplit/InstanceFileOperations.cs	(revision 1114)
@@ -0,0 +1,174 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni
+{
+    internal sealed class InstanceFileOperations
+    {
+        private InstanceFileManager fileManager;
+        private string destinationDir;
+        private readonly Dictionary<string, string> fileNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        private Dictionary<string, string> referencedFiles;
+        private readonly Dictionary<string, string> instances = new Dictionary<string, string>(StringComparer.Ordinal);
+
+        public void Copy(InstanceFileManager fileManager, List<string> sourceFiles, string destinationDir)
+        {
+            Initialize(fileManager, sourceFiles, destinationDir);
+
+            foreach (KeyValuePair<string, string> pair in referencedFiles)
+            {
+                if (File.Exists(pair.Value))
+                {
+                    if (!Utils.AreFilesEqual(pair.Key, pair.Value))
+                        Console.WriteLine("File {0} already exists at destination and it is different. File not copied.", pair.Value);
+                }
+                else
+                {
+                    File.Copy(pair.Key, pair.Value);
+                }
+            }
+        }
+
+        public void Move(InstanceFileManager fileManager, List<string> sourceFilePaths, string outputDirPath)
+        {
+            Initialize(fileManager, sourceFilePaths, outputDirPath);
+
+            foreach (KeyValuePair<string, string> pair in referencedFiles)
+            {
+                if (File.Exists(pair.Value))
+                {
+                    if (Utils.AreFilesEqual(pair.Key, pair.Value))
+                        File.Delete(pair.Key);
+                    else
+                        Console.WriteLine("File {0} already exists at destination and it is different. Source file not moved.", pair.Value);
+                }
+                else
+                {
+                    File.Move(pair.Key, pair.Value);
+                }
+            }
+        }
+
+        public void MoveOverwrite(InstanceFileManager fileManager, List<string> sourceFilePaths, string outputDirPath)
+        {
+            Initialize(fileManager, sourceFilePaths, outputDirPath);
+
+            foreach (KeyValuePair<string, string> pair in referencedFiles)
+            {
+                if (File.Exists(pair.Value))
+                    File.Delete(pair.Value);
+
+                File.Move(pair.Key, pair.Value);
+            }
+        }
+
+        public void MoveDelete(InstanceFileManager fileManager, List<string> sourceFilePaths, string outputDirPath)
+        {
+            Initialize(fileManager, sourceFilePaths, outputDirPath);
+
+            foreach (KeyValuePair<string, string> pair in referencedFiles)
+            {
+                if (File.Exists(pair.Value))
+                    File.Delete(pair.Key);
+                else
+                    File.Move(pair.Key, pair.Value);
+            }
+        }
+
+        public void GetDependencies(InstanceFileManager fileManager, List<string> sourceFilePaths)
+        {
+            Initialize(fileManager, sourceFilePaths, null);
+
+            foreach (string filePath in referencedFiles.Keys)
+            {
+                Console.WriteLine(filePath);
+            }
+        }
+
+        private void Initialize(InstanceFileManager fileManager, List<string> inputFiles, string destinationDir)
+        {
+            this.fileManager = fileManager;
+            this.destinationDir = destinationDir;
+            this.referencedFiles = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+            if (destinationDir != null)
+            {
+                if (Directory.Exists(destinationDir))
+                {
+                    //
+                    // Get a list of existing files to avoid name conflicts due to instance names
+                    // that differ only in case.
+                    //
+
+                    foreach (string existingFilePath in Directory.GetFiles(destinationDir, "*.oni"))
+                    {
+                        string fileName = Path.GetFileNameWithoutExtension(existingFilePath);
+                        string instanceName = Importer.DecodeFileName(existingFilePath);
+
+                        fileNames[fileName] = fileName;
+                        instances[instanceName] = existingFilePath;
+                    }
+                }
+                else
+                {
+                    Directory.CreateDirectory(destinationDir);
+                }
+            }
+
+            var sourceFiles = new Dictionary<string, string>(StringComparer.Ordinal);
+            string lastSourceDir = null;
+
+            foreach (string inputFile in inputFiles)
+            {
+                string sourceDir = Path.GetDirectoryName(inputFile);
+
+                if (sourceDir != lastSourceDir)
+                {
+                    lastSourceDir = sourceDir;
+                    sourceFiles.Clear();
+
+                    foreach (string file in Directory.GetFiles(sourceDir, "*.oni"))
+                        sourceFiles[Importer.DecodeFileName(file)] = file;
+                }
+
+                GetReferencedFiles(inputFile, sourceFiles);
+            }
+        }
+
+        private void GetReferencedFiles(string sourceFile, Dictionary<string, string> sourceFiles)
+        {
+            AddReferencedFile(sourceFile);
+
+            var instanceFile = fileManager.OpenFile(sourceFile);
+
+            foreach (var descriptor in instanceFile.GetPlaceholders())
+            {
+                string referencedSourceFile;
+
+                if (!sourceFiles.TryGetValue(descriptor.FullName, out referencedSourceFile)
+                    || referencedFiles.ContainsKey(referencedSourceFile))
+                    continue;
+
+                GetReferencedFiles(referencedSourceFile, sourceFiles);
+            }
+        }
+
+        private void AddReferencedFile(string filePath)
+        {
+            if (referencedFiles.ContainsKey(filePath))
+                return;
+
+            string instanceName = Importer.DecodeFileName(filePath);
+            string destinationFile;
+
+            if (!instances.TryGetValue(instanceName, out destinationFile))
+            {
+                if (destinationDir != null)
+                    destinationFile = Path.Combine(destinationDir, Importer.EncodeFileName(instanceName, fileNames) + ".oni");
+            }
+
+            referencedFiles.Add(filePath, destinationFile);
+        }
+    }
+}
Index: /OniSplit/InstanceFileWriter.cs
===================================================================
--- /OniSplit/InstanceFileWriter.cs	(revision 1114)
+++ /OniSplit/InstanceFileWriter.cs	(revision 1114)
@@ -0,0 +1,1315 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Oni.Collections;
+using Oni.Metadata;
+
+namespace Oni
+{
+    internal sealed class InstanceFileWriter
+    {
+        #region Private data
+        private static readonly byte[] padding = new byte[512];
+        private static byte[] copyBuffer1 = new byte[32768];
+        private static byte[] copyBuffer2 = new byte[32768];
+        private StreamCache streamCache;
+        private readonly bool bigEndian;
+        private readonly Dictionary<string, int> namedInstancedIdMap;
+        private readonly FileHeader header;
+        private readonly List<DescriptorTableEntry> descriptorTable;
+        private NameDescriptorTable nameIndex;
+        private TemplateDescriptorTable templateTable;
+        private NameTable nameTable;
+        private readonly Dictionary<InstanceDescriptor, InstanceDescriptor> sharedMap;
+        private readonly Dictionary<InstanceFile, int[]> linkMaps;
+        private readonly Dictionary<InstanceFile, Dictionary<int, int>> rawOffsetMaps;
+        private readonly Dictionary<InstanceFile, Dictionary<int, int>> sepOffsetMaps;
+        private int rawOffset;
+        private int sepOffset;
+        private List<BinaryPartEntry> rawParts;
+        private List<BinaryPartEntry> sepParts;
+        #endregion
+
+        #region private class FileHeader
+
+        private class FileHeader
+        {
+            public const int Size = 64;
+
+            public long TemplateChecksum;
+            public int Version;
+            public int InstanceCount;
+            public int NameCount;
+            public int TemplateCount;
+            public int DataTableOffset;
+            public int DataTableSize;
+            public int NameTableOffset;
+            public int NameTableSize;
+            public int RawTableOffset;
+            public int RawTableSize;
+
+            public void Write(BinaryWriter writer)
+            {
+                writer.Write(TemplateChecksum);
+                writer.Write(Version);
+                writer.Write(InstanceFileHeader.Signature);
+                writer.Write(InstanceCount);
+                writer.Write(NameCount);
+                writer.Write(TemplateCount);
+                writer.Write(DataTableOffset);
+                writer.Write(DataTableSize);
+                writer.Write(NameTableOffset);
+                writer.Write(NameTableSize);
+                writer.Write(RawTableOffset);
+                writer.Write(RawTableSize);
+                writer.Write(0);
+                writer.Write(0);
+            }
+        }
+
+        #endregion
+        #region private class DescriptorTableEntry
+
+        private class DescriptorTableEntry
+        {
+            public const int Size = 20;
+
+            public readonly InstanceDescriptor SourceDescriptor;
+            public readonly int Id;
+            public int DataOffset;
+            public int NameOffset;
+            public int DataSize;
+            public bool AnimationPositionPointHack;
+
+            public DescriptorTableEntry(int id, InstanceDescriptor descriptor)
+            {
+                Id = id;
+                SourceDescriptor = descriptor;
+            }
+
+            public bool HasName => SourceDescriptor.HasName;
+            public string Name => SourceDescriptor.FullName;
+            public TemplateTag Code => SourceDescriptor.Template.Tag;
+            public InstanceFile SourceFile => SourceDescriptor.File;
+
+            public void Write(BinaryWriter writer, bool shared)
+            {
+                writer.Write((int)Code);
+                writer.Write(DataOffset);
+                writer.Write(NameOffset);
+                writer.Write(DataSize);
+
+                var flags = InstanceDescriptorFlags.None;
+
+                if (!SourceDescriptor.HasName)
+                    flags |= InstanceDescriptorFlags.Private;
+
+                if (DataOffset == 0)
+                    flags |= InstanceDescriptorFlags.Placeholder;
+
+                if (shared)
+                    flags |= InstanceDescriptorFlags.Shared;
+
+                writer.Write((int)flags);
+            }
+        }
+
+        #endregion
+        #region private class NameDescriptorTable
+
+        private class NameDescriptorTable
+        {
+            private List<Entry> entries;
+
+            #region private class Entry
+
+            private class Entry : IComparable<Entry>
+            {
+                public const int Size = 8;
+
+                public int InstanceNumber;
+                public string Name;
+
+                public void Write(BinaryWriter writer)
+                {
+                    writer.Write(InstanceNumber);
+                    writer.Write(0);
+                }
+
+                #region IComparable<NameIndexEntry> Members
+
+                int IComparable<Entry>.CompareTo(Entry other)
+                {
+                    //
+                    // Note: Oni is case sensitive so we need to sort names accordingly.
+                    //
+
+                    return string.CompareOrdinal(Name, other.Name);
+                }
+
+                #endregion
+            }
+
+            #endregion
+
+            public static NameDescriptorTable CreateFromDescriptors(List<DescriptorTableEntry> descriptorTable)
+            {
+                NameDescriptorTable nameIndex = new NameDescriptorTable();
+
+                nameIndex.entries = new List<Entry>();
+
+                for (int i = 0; i < descriptorTable.Count; i++)
+                {
+                    DescriptorTableEntry descriptor = descriptorTable[i];
+
+                    if (descriptor.HasName)
+                    {
+                        Entry entry = new Entry();
+                        entry.Name = descriptor.Name;
+                        entry.InstanceNumber = i;
+                        nameIndex.entries.Add(entry);
+                    }
+                }
+
+                nameIndex.entries.Sort();
+
+                return nameIndex;
+            }
+
+            public int Count => entries.Count;
+            public int Size => entries.Count * Entry.Size;
+
+            public void Write(BinaryWriter writer)
+            {
+                foreach (Entry entry in entries)
+                    entry.Write(writer);
+            }
+        }
+
+        #endregion
+        #region private class TemplateDescriptorTable
+
+        private class TemplateDescriptorTable
+        {
+            private List<Entry> entries;
+
+            #region private class Entry
+
+            private class Entry : IComparable<Entry>
+            {
+                public const int Size = 16;
+
+                public long Checksum;
+                public TemplateTag Code;
+                public int Count;
+
+                public void Write(BinaryWriter writer)
+                {
+                    writer.Write(Checksum);
+                    writer.Write((int)Code);
+                    writer.Write(Count);
+                }
+
+                int IComparable<Entry>.CompareTo(Entry other) => Code.CompareTo(other.Code);
+            }
+
+            #endregion
+
+            public static TemplateDescriptorTable CreateFromDescriptors(InstanceMetadata metadata, List<DescriptorTableEntry> descriptorTable)
+            {
+                Dictionary<TemplateTag, int> templateCount = new Dictionary<TemplateTag, int>();
+
+                foreach (DescriptorTableEntry entry in descriptorTable)
+                {
+                    int count;
+                    templateCount.TryGetValue(entry.Code, out count);
+                    templateCount[entry.Code] = count + 1;
+                }
+
+                TemplateDescriptorTable templateTable = new TemplateDescriptorTable();
+                templateTable.entries = new List<Entry>(templateCount.Count);
+
+                foreach (KeyValuePair<TemplateTag, int> pair in templateCount)
+                {
+                    Entry entry = new Entry();
+                    entry.Checksum = metadata.GetTemplate(pair.Key).Checksum;
+                    entry.Code = pair.Key;
+                    entry.Count = pair.Value;
+                    templateTable.entries.Add(entry);
+                }
+
+                templateTable.entries.Sort();
+
+                return templateTable;
+            }
+
+            public int Count => entries.Count;
+
+            public int Size => entries.Count * Entry.Size;
+
+            public void Write(BinaryWriter writer)
+            {
+                foreach (Entry entry in entries)
+                    entry.Write(writer);
+            }
+        }
+
+        #endregion
+        #region private class NameTable
+
+        private class NameTable
+        {
+            private List<string> names;
+            private int size;
+
+            public static NameTable CreateFromDescriptors(List<DescriptorTableEntry> descriptors)
+            {
+                NameTable nameTable = new NameTable();
+
+                nameTable.names = new List<string>();
+
+                int nameTableSize = 0;
+
+                foreach (DescriptorTableEntry descriptor in descriptors)
+                {
+                    if (!descriptor.HasName)
+                        continue;
+
+                    string name = descriptor.Name;
+
+                    nameTable.names.Add(name);
+                    descriptor.NameOffset = nameTableSize;
+                    nameTableSize += name.Length + 1;
+
+                    if (name.Length > 63)
+                        Console.WriteLine("Warning: name '{0}' too long.", name);
+                }
+
+                nameTable.size = nameTableSize;
+
+                return nameTable;
+            }
+
+            public int Size => size;
+
+            public void Write(BinaryWriter writer)
+            {
+                byte[] copyBuffer = new byte[256];
+
+                foreach (string name in names)
+                {
+                    int length = Encoding.UTF8.GetBytes(name, 0, name.Length, copyBuffer, 0);
+                    copyBuffer[length] = 0;
+                    writer.Write(copyBuffer, 0, length + 1);
+                }
+            }
+        }
+
+        #endregion
+        #region private class BinaryPartEntry
+
+        private class BinaryPartEntry : IComparable<BinaryPartEntry>
+        {
+            public readonly int SourceOffset;
+            public readonly string SourceFile;
+            public readonly int DestinationOffset;
+            public readonly int Size;
+            public readonly BinaryPartField Field;
+
+            public BinaryPartEntry(string sourceFile, int sourceOffset, int size, int destinationOffset, Field field)
+            {
+                SourceFile = sourceFile;
+                SourceOffset = sourceOffset;
+                Size = size;
+                DestinationOffset = destinationOffset;
+                Field = (BinaryPartField)field;
+            }
+
+            #region IComparable<BinaryPartEntry> Members
+
+            int IComparable<BinaryPartEntry>.CompareTo(BinaryPartEntry other)
+            {
+                //
+                // Sort the binary parts by destination offset in an attempt to streamline the write IO
+                //
+
+                return DestinationOffset.CompareTo(other.DestinationOffset);
+            }
+
+            #endregion
+        }
+
+        #endregion
+        #region private class ChecksumStream
+
+        private class ChecksumStream : Stream
+        {
+            private int checksum;
+            private int position;
+
+            public int Checksum => checksum;
+
+            public override bool CanRead => false;
+            public override bool CanSeek => false;
+            public override bool CanWrite => true;
+
+            public override void Flush()
+            {
+            }
+
+            public override long Length => position;
+
+            public override long Position
+            {
+                get
+                {
+                    return position;
+                }
+                set
+                {
+                    throw new NotSupportedException();
+                }
+            }
+
+            public override int Read(byte[] buffer, int offset, int count)
+            {
+                throw new NotSupportedException();
+            }
+
+            public override long Seek(long offset, SeekOrigin origin)
+            {
+                throw new NotSupportedException();
+            }
+
+            public override void SetLength(long value)
+            {
+                throw new NotSupportedException();
+            }
+
+            public override void Write(byte[] buffer, int offset, int count)
+            {
+                for (int i = offset; i < offset + count; i++)
+                    checksum += buffer[i] ^ (i + position);
+
+                position += count;
+            }
+        }
+
+        #endregion
+        #region private class StreamCache
+
+        private class StreamCache : IDisposable
+        {
+            private const int maxCacheSize = 32;
+            private Dictionary<string, CacheEntry> cacheEntries = new Dictionary<string, CacheEntry>();
+
+            private class CacheEntry
+            {
+                public BinaryReader Stream;
+                public long LastTimeUsed;
+            }
+
+            public BinaryReader GetReader(InstanceDescriptor descriptor)
+            {
+                CacheEntry entry;
+
+                if (!cacheEntries.TryGetValue(descriptor.FilePath, out entry))
+                    entry = OpenStream(descriptor);
+
+                entry.LastTimeUsed = DateTime.Now.Ticks;
+                entry.Stream.Position = descriptor.DataOffset;
+
+                return entry.Stream;
+            }
+
+            private CacheEntry OpenStream(InstanceDescriptor descriptor)
+            {
+                CacheEntry oldestEntry = null;
+                string oldestDescriptor = null;
+
+                if (cacheEntries.Count >= maxCacheSize)
+                {
+                    foreach (KeyValuePair<string, CacheEntry> pair in cacheEntries)
+                    {
+                        if (oldestEntry == null || pair.Value.LastTimeUsed < oldestEntry.LastTimeUsed)
+                        {
+                            oldestDescriptor = pair.Key;
+                            oldestEntry = pair.Value;
+                        }
+                    }
+                }
+
+                if (oldestEntry == null)
+                {
+                    oldestEntry = new CacheEntry();
+                }
+                else
+                {
+                    oldestEntry.Stream.Dispose();
+                    cacheEntries.Remove(oldestDescriptor);
+                }
+
+                oldestEntry.Stream = new BinaryReader(descriptor.FilePath);
+                cacheEntries.Add(descriptor.FilePath, oldestEntry);
+
+                return oldestEntry;
+            }
+
+            public void Dispose()
+            {
+                foreach (CacheEntry entry in cacheEntries.Values)
+                    entry.Stream.Dispose();
+            }
+        }
+
+        #endregion
+
+        public static InstanceFileWriter CreateV31(long templateChecksum, bool bigEndian)
+        {
+            return new InstanceFileWriter(templateChecksum, InstanceFileHeader.Version31, bigEndian);
+        }
+
+        public static InstanceFileWriter CreateV32(List<InstanceDescriptor> descriptors)
+        {
+            long templateChecksum;
+
+            if (descriptors.Exists(x => x.Template.Tag == TemplateTag.SNDD && x.IsMacFile))
+                templateChecksum = InstanceFileHeader.OniMacTemplateChecksum;
+            else
+                templateChecksum = InstanceFileHeader.OniPCTemplateChecksum;
+
+            var writer = new InstanceFileWriter(templateChecksum, InstanceFileHeader.Version32, false);
+            writer.AddDescriptors(descriptors, false);
+            return writer;
+        }
+
+        private InstanceFileWriter(long templateChecksum, int version, bool bigEndian)
+        {
+            if (templateChecksum != InstanceFileHeader.OniPCTemplateChecksum
+                && templateChecksum != InstanceFileHeader.OniMacTemplateChecksum
+                && templateChecksum != 0)
+            {
+                throw new ArgumentException("Unknown template checksum", "templateChecksum");
+            }
+
+            this.bigEndian = bigEndian;
+
+            header = new FileHeader
+            {
+                TemplateChecksum = templateChecksum,
+                Version = version
+            };
+
+            descriptorTable = new List<DescriptorTableEntry>();
+            namedInstancedIdMap = new Dictionary<string, int>();
+
+            linkMaps = new Dictionary<InstanceFile, int[]>();
+            rawOffsetMaps = new Dictionary<InstanceFile, Dictionary<int, int>>();
+            sepOffsetMaps = new Dictionary<InstanceFile, Dictionary<int, int>>();
+
+            sharedMap = new Dictionary<InstanceDescriptor, InstanceDescriptor>();
+        }
+
+        public void AddDescriptors(List<InstanceDescriptor> descriptors, bool removeDuplicates)
+        {
+            if (removeDuplicates)
+            {
+                Console.WriteLine("Removing duplicates");
+
+                using (streamCache = new StreamCache())
+                    descriptors = RemoveDuplicates(descriptors);
+            }
+
+            //
+            // Initialize LinkMap table of each source file.
+            //
+
+            var inputFiles = new Set<InstanceFile>();
+
+            foreach (var descriptor in descriptors)
+                inputFiles.Add(descriptor.File);
+
+            foreach (var inputFile in inputFiles)
+            {
+                linkMaps[inputFile] = new int[inputFile.Descriptors.Count];
+                rawOffsetMaps[inputFile] = new Dictionary<int, int>();
+                sepOffsetMaps[inputFile] = new Dictionary<int, int>();
+            }
+
+            foreach (var descriptor in descriptors)
+            {
+                AddDescriptor(descriptor);
+            }
+
+            CreateHeader();
+        }
+
+        private void AddDescriptor(InstanceDescriptor descriptor)
+        {
+            //
+            // SNDD instances are special because they are different between PC 
+            // and Mac/PC Demo versions so we need to check if the instance type matches the 
+            // output file type. If the file type wasn't specified then we will set it when
+            // the first SNDD instance is seen.
+            //
+
+            if (descriptor.Template.Tag == TemplateTag.SNDD)
+            {
+                if (header.TemplateChecksum == 0)
+                {
+                    header.TemplateChecksum = descriptor.TemplateChecksum;
+                }
+                else if (header.TemplateChecksum != descriptor.TemplateChecksum)
+                {
+                    if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
+                        throw new NotSupportedException(string.Format("File {0} cannot be imported due to conflicting template checksums", descriptor.FilePath));
+                }
+            }
+
+            //
+            // Create a new id for this descriptor and remember it and the old one 
+            // in the LinkMap table of the source file.
+            // 
+
+            int id = MakeInstanceId(descriptorTable.Count);
+
+            linkMaps[descriptor.File][descriptor.Index] = id;
+
+            //
+            // If the descriptor has a name we will need to know later what is the new id
+            // for its name.
+            //
+
+            if (descriptor.HasName)
+                namedInstancedIdMap[descriptor.FullName] = id;
+
+            //
+            // Create and add new table entry for this descriptor.
+            //
+
+            var entry = new DescriptorTableEntry(id, descriptor);
+
+            if (!descriptor.IsPlaceholder)
+            {
+                //
+                // .oni files have only one non empty named descriptor. The rest are
+                // forced to be empty and their contents stored in separate .oni files.
+                // 
+
+                if (!IsV32
+                    || !descriptor.HasName
+                    || descriptorTable.Count == 0
+                    || descriptorTable[0].SourceDescriptor == descriptor)
+                {
+                    int dataSize = descriptor.DataSize;
+
+                    if (descriptor.Template.Tag == TemplateTag.SNDD
+                        && header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum
+                        && descriptor.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
+                    {
+                        //
+                        // HACK: when converting SNDD instances from PC Demo to PC Retail the resulting
+                        // data size differs from the original.
+                        //
+
+                        dataSize = 0x60;
+                    }
+                    else if (descriptor.Template.Tag == TemplateTag.AKDA)
+                    {
+                        dataSize = 0x20;
+                    }
+
+                    entry.DataSize = dataSize;
+                    entry.DataOffset = header.DataTableSize + 8;
+
+                    header.DataTableSize += entry.DataSize;
+                }
+            }
+
+            descriptorTable.Add(entry);
+        }
+
+        private void CreateHeader()
+        {
+            if (header.TemplateChecksum == 0)
+                throw new InvalidOperationException("Target file format was not specified and cannot be autodetected.");
+
+            header.InstanceCount = descriptorTable.Count;
+
+            int offset = FileHeader.Size + descriptorTable.Count * DescriptorTableEntry.Size;
+
+            if (IsV31)
+            {
+                nameIndex = NameDescriptorTable.CreateFromDescriptors(descriptorTable);
+
+                header.NameCount = nameIndex.Count;
+                offset += nameIndex.Size;
+
+                templateTable = TemplateDescriptorTable.CreateFromDescriptors(
+                    InstanceMetadata.GetMetadata(header.TemplateChecksum),
+                    descriptorTable);
+
+                header.TemplateCount = templateTable.Count;
+                offset += templateTable.Size;
+
+                header.DataTableOffset = Utils.Align32(offset);
+
+                nameTable = NameTable.CreateFromDescriptors(descriptorTable);
+
+                header.NameTableSize = nameTable.Size;
+                header.NameTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize);
+            }
+            else
+            {
+                //
+                // .oni files do not need the name index and the template table.
+                // They consume space, complicate things and the information 
+                // contained in them can be recreated anyway.
+                //
+
+                nameTable = NameTable.CreateFromDescriptors(descriptorTable);
+
+                header.NameTableSize = nameTable.Size;
+                header.NameTableOffset = Utils.Align32(offset);
+                header.DataTableOffset = Utils.Align32(header.NameTableOffset + nameTable.Size);
+                header.RawTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize);
+            }
+        }
+
+        public void Write(string filePath)
+        {
+            string outputDirPath = Path.GetDirectoryName(filePath);
+
+            Directory.CreateDirectory(outputDirPath);
+
+            int fileId = IsV31 ? MakeFileId(filePath) : 0;
+
+            using (streamCache = new StreamCache())
+            using (var outputStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 65536))
+            using (var writer = new BinaryWriter(outputStream))
+            {
+                outputStream.Position = FileHeader.Size;
+
+                foreach (DescriptorTableEntry entry in descriptorTable)
+                {
+                    entry.Write(writer, sharedMap.ContainsKey(entry.SourceDescriptor));
+                }
+
+                if (IsV31)
+                {
+                    nameIndex.Write(writer);
+                    templateTable.Write(writer);
+                }
+                else
+                {
+                    // 
+                    // For .oni files write the name table before the data table
+                    // for better reading performance at import time.
+                    //
+
+                    writer.Position = header.NameTableOffset;
+                    nameTable.Write(writer);
+                }
+
+                WriteDataTable(writer, fileId);
+
+                if (IsV31)
+                {
+                    writer.Position = header.NameTableOffset;
+                    nameTable.Write(writer);
+                }
+
+                WriteBinaryParts(writer, filePath);
+
+                if (IsV32 && outputStream.Length > header.RawTableOffset)
+                {
+                    //
+                    // The header was created with a RawTable size of 0 because
+                    // we don't know the size in advance. Fix that now.
+                    //
+
+                    header.RawTableSize = (int)outputStream.Length - header.RawTableOffset;
+                }
+
+                outputStream.Position = 0;
+                header.Write(writer);
+            }
+        }
+
+        private void WriteDataTable(BinaryWriter writer, int fileId)
+        {
+            writer.Position = header.DataTableOffset;
+
+            //
+            // Raw and sep parts will be added as they are found. The initial offset
+            // is 32 because a 0 offset means "NULL".
+            //
+
+            rawOffset = 32;
+            rawParts = new List<BinaryPartEntry>();
+
+            sepOffset = 32;
+            sepParts = new List<BinaryPartEntry>();
+
+            var entries = descriptorTable.ToArray();
+            Array.Sort(entries, (x, y) => x.DataOffset.CompareTo(y.DataOffset));
+
+            foreach (var entry in entries)
+            {
+                if (entry.DataSize == 0)
+                    continue;
+
+                int padSize = header.DataTableOffset + entry.DataOffset - 8 - writer.Position;
+
+                if (padSize <= 512)
+                    writer.Write(padding, 0, padSize);
+                else
+                    writer.Position = header.DataTableOffset + entry.DataOffset - 8;
+
+                writer.Write(entry.Id);
+                writer.Write(fileId);
+
+                var template = entry.SourceDescriptor.Template;
+
+                if (template.Tag == TemplateTag.SNDD
+                    && entry.SourceDescriptor.File.Header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum
+                    && header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum)
+                {
+                    //
+                    // Special case to convert PC Demo SNDD files to PC Retail SNDD files.
+                    //
+
+                    ConvertSNDDHack(entry, writer);
+                }
+                else
+                {
+                    template.Type.Copy(streamCache.GetReader(entry.SourceDescriptor), writer, state =>
+                    {
+                        if (state.Type == MetaType.RawOffset)
+                            RemapRawOffset(entry, state);
+                        else if (state.Type == MetaType.SepOffset)
+                            RemapSepOffset(entry, state);
+                        else if (state.Type is MetaPointer)
+                            RemapLinkId(entry, state);
+                    });
+
+                    if (entry.Code == TemplateTag.TXMP)
+                    {
+                        //
+                        // HACK: All .oni files use the PC format except the SNDD ones. Most
+                        // differences between PC and Mac formats are handled by the metadata
+                        // but the TXMP is special because the raw/sep offset field is at a
+                        // different offset.
+                        //
+
+                        ConvertTXMPHack(entry, writer.BaseStream);
+                    }
+                }
+            }
+        }
+
+        private void ConvertSNDDHack(DescriptorTableEntry entry, BinaryWriter writer)
+        {
+            var reader = streamCache.GetReader(entry.SourceDescriptor);
+
+            int flags = reader.ReadInt32();
+            int duration = reader.ReadInt32();
+            int dataSize = reader.ReadInt32();
+            int dataOffset = reader.ReadInt32();
+
+            int channelCount = (flags == 3) ? 2 : 1;
+
+            writer.Write(8);
+            writer.WriteInt16(2);
+            writer.WriteInt16(channelCount);
+            writer.Write(22050);
+            writer.Write(11155);
+            writer.WriteInt16(512);
+            writer.WriteInt16(4);
+            writer.WriteInt16(32);
+            writer.Write(new byte[] {
+                0xf4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00,
+                0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
+                0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, 0x00,
+                0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff
+            });
+
+            writer.Write((short)duration);
+            writer.Write(dataSize);
+            writer.Write(RemapRawOffsetCore(entry, dataOffset, null));
+        }
+
+        private void ConvertTXMPHack(DescriptorTableEntry entry, Stream stream)
+        {
+            stream.Position = header.DataTableOffset + entry.DataOffset + 0x80;
+            stream.Read(copyBuffer1, 0, 28);
+
+            if (header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum)
+            {
+                //
+                // Swap Bytes is always set for PC files.
+                //
+
+                copyBuffer1[1] |= 0x10;
+            }
+            else if (IsV31 && header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
+            {
+                //
+                // Swap Bytes if always set for MacPPC files except if the format is RGBA 
+                // which requires no conversion.
+                //
+
+                if (bigEndian && copyBuffer1[8] == (byte)Motoko.TextureFormat.RGBA)
+                    copyBuffer1[1] &= 0xef;
+                else
+                    copyBuffer1[1] |= 0x10;
+            }
+
+            if (entry.SourceDescriptor.TemplateChecksum != header.TemplateChecksum)
+            {
+                //
+                // Swap the 0x94 and 0x98 fields to convert between Mac and PC TXMPs
+                //
+
+                for (int i = 20; i < 24; i++)
+                {
+                    byte b = copyBuffer1[i];
+                    copyBuffer1[i] = copyBuffer1[i + 4];
+                    copyBuffer1[i + 4] = b;
+                }
+            }
+
+            stream.Position = header.DataTableOffset + entry.DataOffset + 0x80;
+            stream.Write(copyBuffer1, 0, 28);
+        }
+
+        private bool ZeroTRAMPositionPointsHack(DescriptorTableEntry entry, CopyVisitor state)
+        {
+            if (entry.Code != TemplateTag.TRAM)
+                return false;
+
+            int offset = state.GetInt32();
+
+            if (state.Position == 0x04)
+            {
+                entry.AnimationPositionPointHack = (offset == 0);
+            }
+            else if (state.Position == 0x28 && entry.AnimationPositionPointHack)
+            {
+                if (offset != 0)
+                {
+                    InstanceFile input = entry.SourceFile;
+                    int size = input.GetRawPartSize(offset);
+                    offset = AllocateRawPart(null, 0, size, null);
+                    state.SetInt32(offset);
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+
+        private void RemapRawOffset(DescriptorTableEntry entry, CopyVisitor state)
+        {
+            if (ZeroTRAMPositionPointsHack(entry, state))
+                return;
+
+            state.SetInt32(RemapRawOffsetCore(entry, state.GetInt32(), state.Field));
+        }
+
+        private int RemapRawOffsetCore(DescriptorTableEntry entry, int oldOffset, Field field)
+        {
+            if (oldOffset == 0)
+                return 0;
+
+            InstanceFile input = entry.SourceFile;
+            Dictionary<int, int> rawOffsetMap = rawOffsetMaps[input];
+            int newOffset;
+
+            if (!rawOffsetMap.TryGetValue(oldOffset, out newOffset))
+            {
+                int size = input.GetRawPartSize(oldOffset);
+
+                //
+                // .oni files are always in PC format (except SNDD files) so when importing
+                // to a Mac file we need to allocate some binary parts in the sep file
+                // instead of the raw file.
+                //
+
+                if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum
+                    && (entry.Code == TemplateTag.TXMP
+                       || entry.Code == TemplateTag.OSBD
+                       || entry.Code == TemplateTag.BINA))
+                {
+                    newOffset = AllocateSepPart(input.RawFilePath, oldOffset + input.Header.RawTableOffset, size, null);
+                }
+                else
+                {
+                    newOffset = AllocateRawPart(input.RawFilePath, oldOffset + input.Header.RawTableOffset, size, field);
+                }
+
+                rawOffsetMap[oldOffset] = newOffset;
+            }
+
+            return newOffset;
+        }
+
+        private void RemapSepOffset(DescriptorTableEntry entry, CopyVisitor state)
+        {
+            int oldOffset = state.GetInt32();
+
+            if (oldOffset == 0)
+                return;
+
+            InstanceFile input = entry.SourceFile;
+            Dictionary<int, int> sepOffsetMap = sepOffsetMaps[input];
+            int newOffset;
+
+            if (!sepOffsetMap.TryGetValue(oldOffset, out newOffset))
+            {
+                int size = input.GetSepPartSize(oldOffset);
+
+                //
+                // If we're writing a PC file then there is no sep file, everything gets allocated
+                // in the raw file
+                //
+
+                if (header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum)
+                    newOffset = AllocateRawPart(input.SepFilePath, oldOffset, size, null);
+                else
+                    newOffset = AllocateSepPart(input.SepFilePath, oldOffset, size, null);
+
+                sepOffsetMap[oldOffset] = newOffset;
+            }
+
+            state.SetInt32(newOffset);
+        }
+
+        private int AllocateRawPart(string sourceFile, int sourceOffset, int size, Field field)
+        {
+            var entry = new BinaryPartEntry(sourceFile, sourceOffset, size, rawOffset, field);
+            rawOffset = Utils.Align32(rawOffset + size);
+            rawParts.Add(entry);
+            return entry.DestinationOffset;
+        }
+
+        private int AllocateSepPart(string sourceFile, int sourceOffset, int size, Field field)
+        {
+            var entry = new BinaryPartEntry(sourceFile, sourceOffset, size, sepOffset, field);
+            sepOffset = Utils.Align32(sepOffset + size);
+            sepParts.Add(entry);
+            return entry.DestinationOffset;
+        }
+
+        private void RemapLinkId(DescriptorTableEntry entry, CopyVisitor state)
+        {
+            int oldId = state.GetInt32();
+
+            if (oldId != 0)
+            {
+                int newId = RemapLinkIdCore(entry.SourceDescriptor, oldId);
+                state.SetInt32(newId);
+            }
+        }
+
+        private int RemapLinkIdCore(InstanceDescriptor descriptor, int id)
+        {
+            var file = descriptor.File;
+
+            if (IsV31)
+            {
+                InstanceDescriptor oldDescriptor = file.GetDescriptor(id);
+                InstanceDescriptor newDescriptor;
+                int newDescriptorId;
+
+                if (oldDescriptor.HasName)
+                {
+                    //
+                    // Always lookup named instances, this deals with cases where an instance from one source file
+                    // is replaced by one from another source file.
+                    //
+
+                    if (namedInstancedIdMap.TryGetValue(oldDescriptor.FullName, out newDescriptorId))
+                        return newDescriptorId;
+                }
+
+                if (sharedMap.TryGetValue(oldDescriptor, out newDescriptor))
+                    return linkMaps[newDescriptor.File][newDescriptor.Index];
+            }
+
+            return linkMaps[file][id >> 8];
+        }
+
+        private void WriteBinaryParts(BinaryWriter writer, string filePath)
+        {
+            if (IsV32)
+            {
+                //
+                // For .oni files the raw/sep parts are written to the .oni file.
+                // Separate .raw/.sep files are not used.
+                //
+
+                WriteParts(writer, rawParts);
+                return;
+            }
+
+            string rawFilePath = Path.ChangeExtension(filePath, ".raw");
+
+            Console.WriteLine("Writing {0}", rawFilePath);
+
+            using (var rawOutputStream = new FileStream(rawFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536))
+            using (var rawWriter = new BinaryWriter(rawOutputStream))
+            {
+                WriteParts(rawWriter, rawParts);
+            }
+
+            if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
+            {
+                //
+                // Only Mac/PC Demo files have a .sep file.
+                //
+
+                string sepFilePath = Path.ChangeExtension(filePath, ".sep");
+
+                Console.WriteLine("Writing {0}", sepFilePath);
+
+                using (var sepOutputStream = new FileStream(sepFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536))
+                using (var sepWriter = new BinaryWriter(sepOutputStream))
+                {
+                    WriteParts(sepWriter, sepParts);
+                }
+            }
+        }
+
+        private void WriteParts(BinaryWriter writer, List<BinaryPartEntry> binaryParts)
+        {
+            if (binaryParts.Count == 0)
+            {
+                writer.Write(padding, 0, 32);
+                return;
+            }
+
+            binaryParts.Sort();
+
+            int fileLength = 0;
+
+            foreach (BinaryPartEntry entry in binaryParts)
+            {
+                if (entry.DestinationOffset + entry.Size > fileLength)
+                    fileLength = entry.DestinationOffset + entry.Size;
+            }
+
+            if (IsV31)
+                writer.BaseStream.SetLength(fileLength);
+            else
+                writer.BaseStream.SetLength(fileLength + header.RawTableOffset);
+
+            BinaryReader reader = null;
+
+            foreach (BinaryPartEntry entry in binaryParts)
+            {
+                if (entry.SourceFile == null)
+                    continue;
+
+                if (reader == null)
+                {
+                    reader = new BinaryReader(entry.SourceFile);
+                }
+                else if (reader.Name != entry.SourceFile)
+                {
+                    reader.Dispose();
+                    reader = new BinaryReader(entry.SourceFile);
+                }
+
+                reader.Position = entry.SourceOffset;
+
+                //
+                // Smart change of output's stream position. This assumes that
+                // the binary parts are sorted by destination offset.
+                //
+
+                int padSize = entry.DestinationOffset + header.RawTableOffset - writer.Position;
+
+                if (padSize <= 32)
+                    writer.Write(padding, 0, padSize);
+                else
+                    writer.Position = entry.DestinationOffset + header.RawTableOffset;
+
+                if (entry.Field == null || entry.Field.RawType == null)
+                {
+                    //
+                    // If we don't know the field or the fieldtype for this binary part
+                    // we just copy it over without cleaning up garbage.
+                    //
+
+                    if (copyBuffer1.Length < entry.Size)
+                        copyBuffer1 = new byte[entry.Size * 2];
+
+                    reader.Read(copyBuffer1, 0, entry.Size);
+                    writer.Write(copyBuffer1, 0, entry.Size);
+                }
+                else
+                {
+                    int size = entry.Size;
+
+                    while (size > 0)
+                    {
+                        int copiedSize = entry.Field.RawType.Copy(reader, writer, null);
+
+                        if (copiedSize > size)
+                            throw new InvalidOperationException(string.Format("Bad metadata copying field {0}", entry.Field.Name));
+
+                        size -= copiedSize;
+                    }
+                }
+            }
+
+            if (reader != null)
+                reader.Dispose();
+        }
+
+        private List<InstanceDescriptor> RemoveDuplicates(List<InstanceDescriptor> descriptors)
+        {
+            var checksums = new Dictionary<int, List<InstanceDescriptor>>();
+            var newDescriptorList = new List<InstanceDescriptor>(descriptors.Count);
+
+            foreach (var descriptor in descriptors)
+            {
+                //
+                // We only handle duplicates for these types of instances.
+                // These are the most common cases and they are simple to handle
+                // because they do not contain links to other instances.
+                //
+
+                if (!(descriptor.Template.Tag == TemplateTag.IDXA
+                    || descriptor.Template.Tag == TemplateTag.PNTA
+                    || descriptor.Template.Tag == TemplateTag.VCRA
+                    || descriptor.Template.Tag == TemplateTag.TXCA
+                    || descriptor.Template.Tag == TemplateTag.TRTA
+                    || descriptor.Template.Tag == TemplateTag.TRIA
+                    || descriptor.Template.Tag == TemplateTag.ONCP
+                    || descriptor.Template.Tag == TemplateTag.ONIA))
+                {
+                    newDescriptorList.Add(descriptor);
+                    continue;
+                }
+
+                int checksum = GetInstanceChecksum(descriptor);
+
+                List<InstanceDescriptor> existingDescriptors;
+
+                if (!checksums.TryGetValue(checksum, out existingDescriptors))
+                {
+                    existingDescriptors = new List<InstanceDescriptor>();
+                    checksums.Add(checksum, existingDescriptors);
+                }
+                else
+                {
+                    InstanceDescriptor existing = existingDescriptors.Find(x => AreInstancesEqual(descriptor, x));
+
+                    if (existing != null)
+                    {
+                        sharedMap.Add(descriptor, existing);
+                        continue;
+                    }
+                }
+
+                existingDescriptors.Add(descriptor);
+                newDescriptorList.Add(descriptor);
+            }
+
+            return newDescriptorList;
+        }
+
+        private int GetInstanceChecksum(InstanceDescriptor descriptor)
+        {
+            using (var checksumStream = new ChecksumStream())
+            using (var writer = new BinaryWriter(checksumStream))
+            {
+                descriptor.Template.Type.Copy(streamCache.GetReader(descriptor), writer, null);
+                return checksumStream.Checksum;
+            }
+        }
+
+        private bool AreInstancesEqual(InstanceDescriptor d1, InstanceDescriptor d2)
+        {
+            if (d1.File == d2.File && d1.Index == d2.Index)
+                return true;
+
+            if (d1.Template.Tag != d2.Template.Tag
+                || d1.DataSize != d2.DataSize)
+                return false;
+
+            if (copyBuffer1.Length < d1.DataSize)
+                copyBuffer1 = new byte[d1.DataSize * 2];
+
+            if (copyBuffer2.Length < d2.DataSize)
+                copyBuffer2 = new byte[d2.DataSize * 2];
+
+            MetaType type = d1.Template.Type;
+
+            //return type.Compare(streamCache.GetStream(d1), streamCache.GetStream(d2));
+
+            using (var writer1 = new BinaryWriter(new MemoryStream(copyBuffer1)))
+            using (var writer2 = new BinaryWriter(new MemoryStream(copyBuffer2)))
+            {
+                int s1 = type.Copy(streamCache.GetReader(d1), writer1, null);
+                int s2 = type.Copy(streamCache.GetReader(d2), writer2, null);
+
+                if (s1 != s2)
+                    return false;
+
+                for (int i = 0; i < s1; i++)
+                {
+                    if (copyBuffer1[i] != copyBuffer2[i])
+                        return false;
+                }
+            }
+
+            return true;
+        }
+
+        private bool IsV31 => header.Version == InstanceFileHeader.Version31;
+        private bool IsV32 => header.Version == InstanceFileHeader.Version32;
+
+        private static int MakeFileId(string filePath)
+        {
+            //
+            // File id is generated from the filename. The filename is expected to be in
+            // XXXXXN_YYYYY.dat format where the XXXXX (5 characters) part is ignored, N is treated
+            // as a number (level number) and the YYYYY (this part can have any length) is hashed.
+            // The file extension is ignored.
+            //
+
+            string fileName = Path.GetFileNameWithoutExtension(filePath);
+
+            if (fileName.Length < 6)
+                return 0;
+
+            fileName = fileName.Substring(5);
+
+            int levelNumber = 0;
+            int buildTypeHash = 0;
+
+            int i = fileName.IndexOf('_');
+
+            if (i != -1)
+            {
+                int.TryParse(fileName.Substring(0, i), out levelNumber);
+
+                if (!string.Equals(fileName.Substring(i + 1), "Final", StringComparison.Ordinal))
+                {
+                    for (int j = 1; i + j < fileName.Length; j++)
+                        buildTypeHash += (char.ToUpperInvariant(fileName[i + j]) - 0x40) * j;
+                }
+            }
+
+            return (((levelNumber << 24) | (buildTypeHash & 0xffffff)) << 1) | 1;
+        }
+
+        public static int MakeInstanceId(int index) => (index << 8) | 1;
+    }
+}
Index: /OniSplit/Level/CameraImporter.cs
===================================================================
--- /OniSplit/Level/CameraImporter.cs	(revision 1114)
+++ /OniSplit/Level/CameraImporter.cs	(revision 1114)
@@ -0,0 +1,90 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace Oni.Level
+{
+    using Physics;
+
+    partial class LevelImporter
+    {
+        private void ReadCameras(XmlReader xml, string basePath)
+        {
+            if (!xml.IsStartElement("Cameras"))
+                return;
+
+            if (xml.SkipEmpty())
+                return;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+                ReadCamera(xml, basePath);
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadCamera(XmlReader xml, string basePath)
+        {
+            var filePath = Path.Combine(basePath, xml.GetAttribute("Path"));
+            var scene = LoadScene(filePath);
+            var clips = new List<ObjectAnimationClip>();
+
+            if (filePath.Contains("Camout"))
+            {
+                Console.WriteLine(filePath);
+            }
+
+            ReadSequence(xml, "Camera", name =>
+            {
+                switch (name)
+                {
+                    case "Animation":
+                        clips.Add(ReadAnimationClip(xml));
+                        return true;
+
+                    default:
+                        return false;
+                }
+            });
+
+            var props = new ObjectDaeNodeProperties();
+            props.HasPhysics = true;
+            props.Animations.AddRange(clips);
+
+            var importer = new ObjectDaeImporter(null, new Dictionary<string, Akira.AkiraDaeNodeProperties> { { scene.Id, props } });
+
+            importer.Import(scene);
+
+            foreach (var node in importer.Nodes)
+            {
+                foreach (var animation in node.Animations)
+                {
+                    var writer = new DatWriter();
+                    ObjectDatWriter.WriteAnimation(animation, writer);
+                    writer.Write(outputDirPath);
+                }
+            }
+        }
+
+        private void ReadSequence(XmlReader xml, string name, Func<string, bool> action)
+        {
+            if (!xml.SkipEmpty())
+            {
+                xml.ReadStartElement(name);
+
+                while (xml.IsStartElement())
+                {
+                    if (!action(xml.LocalName))
+                    {
+                        error.WriteLine("Unknown element {0}", xml.LocalName);
+                        xml.Skip();
+                    }
+                }
+
+                xml.ReadEndElement();
+            }
+        }
+    }
+}
Index: /OniSplit/Level/CharacterImporter.cs
===================================================================
--- /OniSplit/Level/CharacterImporter.cs	(revision 1114)
+++ /OniSplit/Level/CharacterImporter.cs	(revision 1114)
@@ -0,0 +1,22 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+
+namespace Oni.Level
+{
+    partial class LevelImporter
+    {
+        private static IEnumerable<ScriptCharacter> ReadCharacters(XmlReader xml, string basePath)
+        {
+            if (xml.SkipEmpty())
+                yield break;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+                yield return ScriptCharacter.Read(xml);
+
+            xml.ReadEndElement();
+        }
+    }
+}
Index: /OniSplit/Level/Corpse.cs
===================================================================
--- /OniSplit/Level/Corpse.cs	(revision 1114)
+++ /OniSplit/Level/Corpse.cs	(revision 1114)
@@ -0,0 +1,25 @@
+﻿namespace Oni.Level
+{
+    internal class Corpse
+    {
+        public bool IsFixed;
+        public bool IsUsed;
+        public string FileName;
+        public string CharacterClass;
+        public readonly Matrix[] Transforms = new Matrix[19];
+        public BoundingBox BoundingBox;
+
+        public int Order
+        {
+            get
+            {
+                if (IsFixed)
+                    return 1;
+                else if (IsUsed)
+                    return 2;
+                else
+                    return 3;
+            }
+        }
+    }
+}
Index: /OniSplit/Level/FilmImporter.cs
===================================================================
--- /OniSplit/Level/FilmImporter.cs	(revision 1114)
+++ /OniSplit/Level/FilmImporter.cs	(revision 1114)
@@ -0,0 +1,247 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace Oni.Level
+{
+    using Metadata;
+    using Xml;
+
+    partial class LevelImporter
+    {
+        private class Film
+        {
+            public string Name;
+            public Vector3 Position;
+            public float Facing;
+            public float DesiredFacing;
+            public float HeadFacing;
+            public float HeadPitch;
+            public readonly string[] Animations = new string[2];
+            public int Length;
+            public readonly List<FilmFrame> Frames = new List<FilmFrame>();
+        }
+
+        private class FilmFrame
+        {
+            public Vector2 MouseDelta;
+            public InstanceMetadata.FILMKeys Keys;
+            public uint Time;
+        }
+
+        private void ReadFilms(XmlReader xml, string basePath)
+        {
+            if (!xml.IsStartElement("Films") || xml.SkipEmpty())
+                return;
+
+            xml.ReadStartElement("Films");
+
+            while (xml.IsStartElement())
+            {
+                xml.ReadStartElement("Import");
+                string filePath = Path.Combine(basePath, xml.ReadElementContentAsString());
+                xml.ReadEndElement();
+
+                if (!File.Exists(filePath))
+                {
+                    error.WriteLine("Could not find file '{0}'", filePath);
+                    continue;
+                }
+
+                string extension = Path.GetExtension(filePath);
+                string name = Path.GetFileNameWithoutExtension(filePath);
+
+                if (string.Equals(extension, ".oni", StringComparison.OrdinalIgnoreCase))
+                {
+                    var outputFilePath = Path.Combine(outputDirPath, name + ".oni");
+
+                    File.Copy(filePath, outputFilePath, true);
+                }
+                else if (string.Equals(extension, ".dat", StringComparison.OrdinalIgnoreCase))
+                {
+                    var film = ReadBinFilm(filePath);
+
+                    var datWriter = new DatWriter();
+                    WriteDatFilm(datWriter, film);
+                    datWriter.Write(outputDirPath);
+                }
+                else if (string.Equals(extension, ".xml", StringComparison.OrdinalIgnoreCase))
+                {
+                    var film = ReadXmlFilm(filePath);
+
+                    var datWriter = new DatWriter();
+                    WriteDatFilm(datWriter, film);
+                    datWriter.Write(outputDirPath);
+                }
+                else
+                {
+                    error.WriteLine("Unsupported film file type {0}", extension);
+                }
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private static Film ReadBinFilm(string filePath)
+        {
+            string name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (name.StartsWith("FILM", StringComparison.Ordinal))
+                name = name.Substring(4);
+
+            var film = new Film();
+            film.Name = name;
+
+            using (var reader = new BinaryReader(filePath, true))
+            {
+                film.Animations[0] = reader.ReadString(128);
+                film.Animations[1] = reader.ReadString(128);
+                film.Position = reader.ReadVector3();
+                film.Facing = reader.ReadSingle();
+                film.DesiredFacing = reader.ReadSingle();
+                film.HeadFacing = reader.ReadSingle();
+                film.HeadPitch = reader.ReadSingle();
+                film.Length = reader.ReadInt32();
+                reader.Skip(28);
+                int numFrames = reader.ReadInt32();
+                film.Frames.Capacity = numFrames;
+
+                for (int i = 0; i < numFrames; i++)
+                {
+                    var frame = new FilmFrame();
+                    frame.MouseDelta = reader.ReadVector2();
+                    frame.Keys = (InstanceMetadata.FILMKeys)reader.ReadUInt64();
+                    frame.Time = reader.ReadUInt32();
+                    reader.Skip(4);
+                    film.Frames.Add(frame);
+                }
+            }
+
+            return film;
+        }
+
+        private static Film ReadXmlFilm(string filePath)
+        {
+            string name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (name.StartsWith("FILM", StringComparison.Ordinal))
+                name = name.Substring(4);
+
+            var film = new Film();
+            film.Name = name;
+
+            var settings = new XmlReaderSettings {
+                IgnoreWhitespace = true,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
+
+            using (var xml = XmlReader.Create(filePath, settings))
+            {
+                xml.ReadStartElement("Oni");
+
+                name = xml.GetAttribute("Name");
+
+                if (!string.IsNullOrEmpty(name))
+                    film.Name = name;
+
+                xml.ReadStartElement("FILM");
+
+                film.Position = xml.ReadElementContentAsVector3("Position");
+                film.Facing = xml.ReadElementContentAsFloat("Facing", "");
+                film.DesiredFacing = xml.ReadElementContentAsFloat("DesiredFacing", "");
+                film.HeadFacing = xml.ReadElementContentAsFloat("HeadFacing", "");
+                film.HeadPitch = xml.ReadElementContentAsFloat("HeadPitch", "");
+                film.Length = xml.ReadElementContentAsInt("FrameCount", "");
+                xml.ReadStartElement("Animations");
+                film.Animations[0] = xml.ReadElementContentAsString("Link", "");
+                film.Animations[1] = xml.ReadElementContentAsString("Link", "");
+                xml.ReadEndElement();
+                xml.ReadStartElement("Frames");
+
+                while (xml.IsStartElement())
+                {
+                    var frame = new FilmFrame();
+
+                    switch (xml.LocalName)
+                    {
+                        case "FILMFrame":
+                            xml.ReadStartElement();
+                            frame.MouseDelta.X = xml.ReadElementContentAsFloat("MouseDeltaX", "");
+                            frame.MouseDelta.Y = xml.ReadElementContentAsFloat("MouseDeltaY", "");
+                            frame.Keys = xml.ReadElementContentAsEnum<InstanceMetadata.FILMKeys>("Keys");
+                            frame.Time = (uint)xml.ReadElementContentAsInt("Frame", "");
+                            xml.ReadEndElement();
+                            break;
+
+                        case "Frame":
+                            xml.ReadStartElement();
+                            while (xml.IsStartElement())
+                            {
+                                switch (xml.LocalName)
+                                {
+                                    case "Time":
+                                        frame.Time = (uint)xml.ReadElementContentAsInt();
+                                        break;
+                                    case "MouseDelta":
+                                        frame.MouseDelta = xml.ReadElementContentAsVector2();
+                                        break;
+                                    case "Keys":
+                                        frame.Keys = xml.ReadElementContentAsEnum<InstanceMetadata.FILMKeys>();
+                                        break;
+                                }
+                            }
+                            xml.ReadEndElement();
+                            break;
+                        default:
+                            xml.Skip();
+                            continue;
+                    }
+
+                    film.Frames.Add(frame);
+
+                }
+
+                xml.ReadEndElement();
+                xml.ReadEndElement();
+            }
+
+            return film;
+        }
+
+        private static void WriteDatFilm(DatWriter filmWriter, Film film)
+        {
+            var descriptor = filmWriter.CreateInstance(TemplateTag.FILM, film.Name);
+
+            var animations = new ImporterDescriptor[2];
+
+            for (int i = 0; i < animations.Length; i++)
+            {
+                if (!string.IsNullOrEmpty(film.Animations[i]))
+                    animations[i] = filmWriter.CreateInstance(TemplateTag.TRAM, film.Animations[i]);
+            }
+
+            using (var writer = descriptor.OpenWrite())
+            {
+                writer.Write(film.Position);
+                writer.Write(film.Facing);
+                writer.Write(film.DesiredFacing);
+                writer.Write(film.HeadFacing);
+                writer.Write(film.HeadPitch);
+                writer.Write(film.Length);
+                writer.Write(animations);
+                writer.Skip(12);
+                writer.Write(film.Frames.Count);
+
+                foreach (var frame in film.Frames)
+                {
+                    writer.Write(frame.MouseDelta);
+                    writer.Write((ulong)frame.Keys);
+                    writer.Write(frame.Time);
+                    writer.Skip(4);
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Level/Later/EnvParticle.cs
===================================================================
--- /OniSplit/Level/Later/EnvParticle.cs	(revision 1114)
+++ /OniSplit/Level/Later/EnvParticle.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Level
+{
+    internal class EnvParticle
+    {
+        public string ClassName;
+        public string Name;
+        public Matrix Matrix;
+        public Vector2 DecalScale;
+    }
+}
Index: /OniSplit/Level/Later/Level.cs
===================================================================
--- /OniSplit/Level/Later/Level.cs	(revision 1114)
+++ /OniSplit/Level/Later/Level.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Level
+{
+    internal class Level
+    {
+        public string Name;
+    }
+}
Index: /OniSplit/Level/Later/LevelDatReader.cs
===================================================================
--- /OniSplit/Level/Later/LevelDatReader.cs	(revision 1114)
+++ /OniSplit/Level/Later/LevelDatReader.cs	(revision 1114)
@@ -0,0 +1,171 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Level
+{
+    internal class LevelDatReader
+    {
+        private InstanceDescriptor onlv;
+        private BinaryReader dat;
+        private Level level;
+
+        public static Level Read(InstanceDescriptor onlv, BinaryReader dat)
+        {
+            LevelDatReader reader = new LevelDatReader();
+            reader.onlv = onlv;
+            reader.dat = dat;
+            reader.level = new Level();
+            reader.Read();
+            return reader.level;
+        }
+
+        private void Read()
+        {
+            InstanceDescriptor akev;
+            InstanceDescriptor oboa;
+            InstanceDescriptor onsk;
+            InstanceDescriptor aisa;
+            InstanceDescriptor onoa;
+            InstanceDescriptor envp;
+            InstanceDescriptor crsa;
+
+            level.Name = dat.ReadString(64);
+            akev = dat.ReadInstance();
+            oboa = dat.ReadInstance();
+            dat.Skip(12);
+            onsk = dat.ReadInstance();
+            dat.Skip(4);
+            aisa = dat.ReadInstance();
+            dat.Skip(12);
+            onoa = dat.ReadInstance();
+            envp = dat.ReadInstance();
+            dat.Skip(644);
+            crsa = dat.ReadInstance();
+        }
+
+        private void ReadOBOA(InstanceDescriptor oboa)
+        {
+            List<PhyObject> objects = new List<PhyObject>();
+
+            using (BinaryReader reader = oboa.OpenRead())
+            {
+                reader.Skip(22);
+                int count = reader.ReadUInt16();
+
+                for (int i = 0; i < count; i++)
+                {
+                    InstanceDescriptor m3ga = reader.ReadInstance();
+                    InstanceDescriptor oban = reader.ReadInstance();
+                    InstanceDescriptor envp = reader.ReadInstance();
+                    PhyObjectFlags flags = (PhyObjectFlags)reader.ReadInt32();
+                    reader.Skip(4);
+                    int doorId = reader.ReadInt32();
+                    PhyType phyType = (PhyType)reader.ReadInt32();
+                    int scriptId = reader.ReadInt32();
+                    Vector3 position = reader.ReadVector3();
+                    Quaternion rotation = reader.ReadQuaternion();
+                    float scale = reader.ReadSingle();
+                    Matrix matrix = reader.ReadMatrix4x3();
+                    string name = reader.ReadString(64);
+                    string fileName = reader.ReadString(64);
+
+                    if ((flags & PhyObjectFlags.Used) == 0)
+                        continue;
+                }
+            }
+        }
+
+        private void ReadENVP(InstanceDescriptor envp)
+        {
+            List<EnvParticle> objects = new List<EnvParticle>();
+
+            using (BinaryReader reader = envp.OpenRead())
+            {
+                reader.Skip(22);
+                int count = reader.ReadUInt16();
+
+                for (int i = 0; i < count; i++)
+                {
+                    string className = reader.ReadString(64);
+                    string name = reader.ReadString(48);
+                    Matrix matrix = reader.ReadMatrix4x3();
+                    Vector2 decalScale = reader.ReadVector2();
+                    reader.Skip(40);
+                }
+            }
+        }
+
+        private class DatObjectQuads
+        {
+            public uint ObjectId;
+            public int[] QuadIndices;
+        }
+
+        private void ReadONOA(InstanceDescriptor onoa)
+        {
+            List<DatObjectQuads> objects = new List<DatObjectQuads>();
+            List<InstanceDescriptor> indices = new List<InstanceDescriptor>();
+
+            using (BinaryReader reader = onoa.OpenRead())
+            {
+                reader.Skip(20);
+                int count = reader.ReadInt32();
+
+                for (int i = 0; i < count; i++)
+                {
+                    uint id = reader.ReadUInt32();
+                    InstanceDescriptor idxa = reader.ReadInstance();
+
+                    DatObjectQuads obj = new DatObjectQuads();
+                    obj.ObjectId = id;
+                    objects.Add(obj);
+                    indices.Add(idxa);
+                }
+            }
+
+            for (int i = 0; i < objects.Count; i++)
+            {
+                using (BinaryReader reader = indices[i].OpenRead())
+                {
+                    reader.Skip(20);
+                    objects[i].QuadIndices = reader.ReadInt32VarArray();
+                }
+            }
+        }
+
+        private void ReadAISA(InstanceDescriptor aisa)
+        {
+            List<ScriptCharacter> chars = new List<ScriptCharacter>();
+
+            using (BinaryReader reader = aisa.OpenRead())
+            {
+                reader.Skip(22);
+                int count = reader.ReadUInt16();
+
+                for (int i = 0; i < count; i++)
+                {
+                    string name = reader.ReadString(32);
+                    int id = reader.ReadUInt16();
+                    int flagId = reader.ReadUInt16();
+                    reader.Skip(2);
+                    int teamId = reader.ReadUInt16();
+                    InstanceDescriptor oncc = reader.ReadInstance();
+                    reader.Skip(32);
+                    reader.Skip(4);
+                    string createScript = reader.ReadString(32);
+                    string dieScript = reader.ReadString(32);
+                    string enemyScript = reader.ReadString(32);
+                    string alarmScript = reader.ReadString(32);
+                    string hurtScript = reader.ReadString(32);
+                    string lowHealthScript = reader.ReadString(32);
+                    string lowAmmoScript = reader.ReadString(32);
+                    string noPathScript = reader.ReadString(32);
+                    InstanceDescriptor onwc = reader.ReadInstance();
+                    int ammo = reader.ReadUInt16();
+                    reader.Skip(10);
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Level/Later/PhyObject.cs
===================================================================
--- /OniSplit/Level/Later/PhyObject.cs	(revision 1114)
+++ /OniSplit/Level/Later/PhyObject.cs	(revision 1114)
@@ -0,0 +1,25 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Level
+{
+    internal class PhyObject
+    {
+        public PhyObjectFlags Flags;
+        public PhyType PhysicsType;
+        public int ScriptId;
+        public List<EnvParticle> Particles;
+
+        public Vector3 Position;
+        public Quaternion Rotation;
+        public float Scale;
+        public Matrix Matrix;
+
+        public int DoorId;
+        public int DoorQuad;
+
+        public string Name;
+        public string FileName;
+    }
+}
Index: /OniSplit/Level/Later/PhyObjectFlags.cs
===================================================================
--- /OniSplit/Level/Later/PhyObjectFlags.cs	(revision 1114)
+++ /OniSplit/Level/Later/PhyObjectFlags.cs	(revision 1114)
@@ -0,0 +1,15 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Level
+{
+    [Flags]
+    internal enum PhyObjectFlags
+    {
+        Used = 0x20000,
+        NoCollision = 0x40000,
+        NoGravity = 0x80000,
+        BoxCollision = 0x100000,
+    }
+}
Index: /OniSplit/Level/Later/PhyType.cs
===================================================================
--- /OniSplit/Level/Later/PhyType.cs	(revision 1114)
+++ /OniSplit/Level/Later/PhyType.cs	(revision 1114)
@@ -0,0 +1,15 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Level
+{
+    internal enum PhyType
+    {
+        None0,
+        None1,
+        HasPhysics2,
+        IsAnimated3,
+        HasPhysics4
+    }
+}
Index: /OniSplit/Level/Later/ScriptCharacter.cs
===================================================================
--- /OniSplit/Level/Later/ScriptCharacter.cs	(revision 1114)
+++ /OniSplit/Level/Later/ScriptCharacter.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Level
+{
+    internal class ScriptCharacter
+    {
+    }
+}
Index: /OniSplit/Level/LevelDatWriter.cs
===================================================================
--- /OniSplit/Level/LevelDatWriter.cs	(revision 1114)
+++ /OniSplit/Level/LevelDatWriter.cs	(revision 1114)
@@ -0,0 +1,331 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Level
+{
+    internal class LevelDatWriter
+    {
+        private readonly Importer importer;
+        private readonly DatLevel level;
+
+        public class DatLevel
+        {
+            public string name;
+            public string skyName;
+            public readonly List<ObjectSetup> physics = new List<ObjectSetup>();
+            public readonly List<Physics.ObjectParticle> particles = new List<Physics.ObjectParticle>();
+            public readonly List<ScriptCharacter> characters = new List<ScriptCharacter>();
+            public readonly List<Corpse> corpses = new List<Corpse>();
+            public Akira.PolygonMesh model;
+        }
+
+        private LevelDatWriter(Importer importer, DatLevel level)
+        {
+            this.importer = importer;
+            this.level = level;
+        }
+
+        public static void Write(Importer importer, DatLevel level)
+        {
+            var writer = new LevelDatWriter(importer, level);
+            writer.WriteONLV();
+        }
+
+        private void WriteONLV()
+        {
+            var onlv = importer.CreateInstance(TemplateTag.ONLV, level.name);
+            var oboa = importer.CreateInstance(TemplateTag.OBOA);
+            var aisa = importer.CreateInstance(TemplateTag.AISA);
+            var onoa = importer.CreateInstance(TemplateTag.ONOA);
+            var envp = importer.CreateInstance(TemplateTag.ENVP);
+            var crsa = importer.CreateInstance(TemplateTag.CRSA);
+            var onsk = importer.CreateInstance(TemplateTag.ONSK, level.skyName);
+            var akev = importer.CreateInstance(TemplateTag.AKEV, level.name);
+
+            using (var writer = onlv.OpenWrite())
+            {
+                writer.Write(level.name, 64);
+                writer.Write(akev);
+                writer.Write(oboa);
+                writer.Write(0);
+                writer.Write(0);
+                writer.Write(0);
+                writer.Write(onsk);
+                writer.Write(0.0f);
+                writer.Write(aisa);
+                writer.Write(0);
+                writer.Write(0);
+                writer.Write(0);
+                writer.Write(onoa);
+                writer.Write(envp);
+                writer.Skip(644);
+                writer.Write(crsa);
+            }
+
+            WriteOBOA(oboa);
+            WriteAISA(aisa);
+            WriteONOA(onoa);
+            WriteENVP(envp, level.particles);
+            WriteCRSA(crsa, level.corpses);
+        }
+
+        private void WriteOBOA(ImporterDescriptor oboa)
+        {
+            var objects = level.physics;
+            var m3ga = new ImporterDescriptor[objects.Count];
+            var oban = new ImporterDescriptor[objects.Count];
+            var envp = new ImporterDescriptor[objects.Count];
+
+            for (int i = 0; i < objects.Count; i++)
+            {
+                var obj = objects[i];
+
+                var m3gms = new List<ImporterDescriptor>();
+
+                foreach (var geom in obj.Geometries)
+                {
+                    if (geom is string)
+                        m3gms.Add(importer.CreateInstance(TemplateTag.M3GM, (string)geom));
+                    else
+                        m3gms.Add(Motoko.GeometryDatWriter.Write((Motoko.Geometry)geom, importer.ImporterFile));
+                }
+
+                m3ga[i] = importer.CreateInstance(TemplateTag.M3GA);
+
+                WriteM3GA(m3ga[i], m3gms);
+
+                if (obj.Animation != null)
+                    oban[i] = importer.CreateInstance(TemplateTag.OBAN, obj.Animation.Name);
+
+                if (obj.Particles.Count > 0)
+                {
+                    envp[i] = importer.CreateInstance(TemplateTag.ENVP);
+                    WriteENVP(envp[i], obj.Particles);
+                }
+            }
+
+            int unused = 32;
+
+            using (var writer = oboa.OpenWrite(22))
+            {
+                writer.WriteUInt16(objects.Count + unused);
+
+                for (int i = 0; i != objects.Count; i++)
+                {
+                    var obj = objects[i];
+
+                    writer.Write(m3ga[i]);
+                    writer.Write(oban[i]);
+                    writer.Write(envp[i]);
+                    writer.Write((uint)(obj.Flags | Physics.ObjectSetupFlags.InUse));
+                    //writer.Write(obj.DoorGunkIndex);
+                    writer.Write(0);
+                    writer.Write(obj.DoorScriptId);
+                    writer.Write((uint)obj.PhysicsType);
+                    writer.Write(obj.ScriptId);
+                    writer.Write(obj.Position);
+                    writer.Write(obj.Orientation);
+                    writer.Write(obj.Scale);
+                    writer.WriteMatrix4x3(obj.Origin);
+                    writer.Write(obj.Name, 64);
+                    writer.Write(obj.FileName, 64);
+                }
+
+                writer.Skip(unused * 240);
+            }
+        }
+
+        private void WriteM3GA(ImporterDescriptor m3ga, ICollection<ImporterDescriptor> geometries)
+        {
+            using (var writer = m3ga.OpenWrite(20))
+            {
+                writer.Write(geometries.Count);
+                writer.Write(geometries);
+            }
+        }
+
+        private void WriteAISA(ImporterDescriptor aisa)
+        {
+            var characterClasses = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);
+            var weaponClasses = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);
+
+            foreach (var chr in level.characters)
+            {
+                if (!characterClasses.ContainsKey(chr.className))
+                    characterClasses.Add(chr.className, importer.CreateInstance(TemplateTag.ONCC, chr.className));
+
+                if (!string.IsNullOrEmpty(chr.weaponClassName) && !weaponClasses.ContainsKey(chr.weaponClassName))
+                    weaponClasses.Add(chr.weaponClassName, importer.CreateInstance(TemplateTag.ONWC, chr.weaponClassName));
+            }
+
+            using (var writer = aisa.OpenWrite(22))
+            {
+                writer.WriteUInt16(level.characters.Count);
+
+                foreach (var chr in level.characters)
+                {
+                    ImporterDescriptor characterClass, weaponClass;
+
+                    characterClasses.TryGetValue(chr.className, out characterClass);
+
+                    if (!string.IsNullOrEmpty(chr.weaponClassName))
+                        weaponClasses.TryGetValue(chr.weaponClassName, out weaponClass);
+                    else
+                        weaponClass = null;
+
+                    writer.Write(chr.name, 32);
+                    writer.WriteInt16(chr.scriptId);
+                    writer.WriteInt16(chr.flagId);
+                    writer.WriteUInt16((ushort)chr.flags);
+                    writer.WriteUInt16((ushort)chr.team);
+                    writer.Write(characterClass);
+                    writer.Skip(36);
+                    writer.Write(chr.onSpawn, 32);
+                    writer.Write(chr.onDeath, 32);
+                    writer.Write(chr.onSeenEnemy, 32);
+                    writer.Write(chr.onAlarmed, 32);
+                    writer.Write(chr.onHurt, 32);
+                    writer.Write(chr.onDefeated, 32);
+                    writer.Write(chr.onOutOfAmmo, 32);
+                    writer.Write(chr.onNoPath, 32);
+                    writer.Write(weaponClass);
+                    writer.WriteInt16(chr.ammo);
+                    writer.Skip(10);
+                }
+            }
+        }
+
+        private void WriteONOA(ImporterDescriptor onoa)
+        {
+            var map = new Dictionary<int, List<int>>();
+            int pi = 0;
+
+            foreach (var poly in level.model.Polygons)
+            {
+                if (poly.ObjectId > 0)
+                {
+                    List<int> indices;
+
+                    int objectId = poly.ObjectType << 24 | poly.ObjectId;
+
+                    if (!map.TryGetValue(objectId, out indices))
+                    {
+                        indices = new List<int>();
+                        map[objectId] = indices;
+                    }
+
+                    indices.Add(pi);
+                }
+
+                pi++;
+            }
+
+            var elt = new List<KeyValuePair<int, ImporterDescriptor>>();
+
+            foreach (var pair in map)
+            {
+                var idxa = importer.CreateInstance(TemplateTag.IDXA);
+
+                elt.Add(new KeyValuePair<int, ImporterDescriptor>(pair.Key, idxa));
+
+                using (var idxaWriter = idxa.OpenWrite(20))
+                {
+                    idxaWriter.Write(pair.Value.Count);
+                    idxaWriter.Write(pair.Value.ToArray());
+                }
+            }
+
+            using (var writer = onoa.OpenWrite(20))
+            {
+                writer.Write(elt.Count);
+
+                foreach (var e in elt)
+                {
+                    writer.Write(e.Key);
+                    writer.Write(e.Value);
+                }
+            }
+        }
+
+        private void WriteENVP(ImporterDescriptor envp, List<Physics.ObjectParticle> particles)
+        {
+            using (var writer = envp.OpenWrite(22))
+            {
+                writer.WriteUInt16(particles.Count);
+
+                foreach (var particle in particles)
+                {
+                    writer.Write(particle.ParticleClass, 64);
+                    writer.Write(particle.Tag, 48);
+                    writer.WriteMatrix4x3(particle.Matrix);
+                    writer.Write(particle.DecalScale);
+                    writer.Write((ushort)particle.Flags);
+                    writer.Skip(38);
+                }
+            }
+        }
+
+        private void WriteCRSA(ImporterDescriptor crsa, List<Corpse> corpses)
+        {
+            //
+            // Ensure that there are at least 20 corpses
+            //
+
+            while (corpses.Count < 20)
+                corpses.Add(new Corpse());
+
+            int fixedCount = corpses.Count(c => c.IsFixed);
+            int usedCount = corpses.Count(c => c.IsUsed);
+
+            //
+            // Ensure that there are at least 5 unused corpses 
+            // so new corpses can be created at runtime
+            //
+
+            while (corpses.Count - usedCount < 5)
+                corpses.Add(new Corpse());
+
+            corpses.Sort((x, y) => x.Order.CompareTo(y.Order));
+
+            var onccDesriptors = new Dictionary<string, ImporterDescriptor>();
+
+            using (var writer = crsa.OpenWrite(12))
+            {
+                writer.Write(fixedCount);
+                writer.Write(usedCount);
+                writer.Write(corpses.Count);
+
+                foreach (var corpse in corpses)
+                {
+                    writer.Write(corpse.FileName ?? "", 32);
+                    writer.Skip(128);
+
+                    if (corpse.IsUsed)
+                    {
+                        ImporterDescriptor oncc = null;
+
+                        if (!string.IsNullOrEmpty(corpse.CharacterClass))
+                        {
+                            if (!onccDesriptors.TryGetValue(corpse.CharacterClass, out oncc))
+                            {
+                                oncc = importer.CreateInstance(TemplateTag.ONCC, corpse.CharacterClass);
+                                onccDesriptors.Add(corpse.CharacterClass, oncc);
+                            }
+                        }
+
+                        writer.Write(oncc);
+
+                        foreach (var transform in corpse.Transforms)
+                            writer.WriteMatrix4x3(transform);
+
+                        writer.Write(corpse.BoundingBox);
+                    }
+                    else
+                    {
+                        writer.Skip(940);
+                    }
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Level/LevelImporter.cs
===================================================================
--- /OniSplit/Level/LevelImporter.cs	(revision 1114)
+++ /OniSplit/Level/LevelImporter.cs	(revision 1114)
@@ -0,0 +1,146 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace Oni.Level
+{
+    internal partial class LevelImporter : Importer
+    {
+        private readonly TextWriter info;
+        private readonly TextWriter error;
+        private bool debug;
+        private string outputDirPath;
+        private LevelDatWriter.DatLevel level;
+        private string sharedPath;
+        private InstanceFileManager sharedManager;
+        private Dictionary<string, InstanceDescriptor> sharedCache;
+        private Dictionary<string, Dae.Scene> sceneCache;
+
+        public LevelImporter()
+        {
+            info = Console.Out;
+            error = Console.Error;
+        }
+
+        public bool Debug
+        {
+            get { return debug; }
+            set { debug = value; }
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            this.outputDirPath = outputDirPath;
+            this.textureImporter = new Motoko.TextureImporter3(outputDirPath);
+
+            Read(filePath);
+
+            WriteLevel();
+            WriteObjects();
+        }
+
+        private void Read(string filePath)
+        {
+            level = new LevelDatWriter.DatLevel();
+            level.name = Path.GetFileNameWithoutExtension(filePath);
+
+            string basePath = Path.GetDirectoryName(filePath);
+
+            var settings = new XmlReaderSettings {
+                IgnoreWhitespace = true,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
+
+            using (var xml = XmlReader.Create(filePath, settings))
+            {
+                xml.ReadStartElement("Oni");
+                ReadLevel(xml, basePath);
+                xml.ReadEndElement();
+            }
+
+            ImportModel(basePath);
+        }
+
+        private void ReadLevel(XmlReader xml, string basePath)
+        {
+            string path = xml.GetAttribute("SharedPath");
+
+            if (!string.IsNullOrEmpty(path))
+                sharedPath = Path.GetFullPath(Path.Combine(basePath, path));
+            else
+                sharedPath = Path.GetFullPath(Path.Combine(basePath, "classes"));
+
+            sharedManager = new InstanceFileManager();
+            sharedManager.AddSearchPath(sharedPath);
+
+            string name = xml.GetAttribute("Name");
+
+            if (!string.IsNullOrEmpty(name))
+                level.name = name;
+
+            xml.ReadStartElement("Level");
+
+            ReadModel(xml, basePath);
+            ReadSky(xml, basePath);
+            ReadObjects(xml, basePath);
+            ReadFilms(xml, basePath);
+            ReadCameras(xml, basePath);
+
+            xml.ReadEndElement();
+        }
+
+        private void WriteLevel()
+        {
+            BeginImport();
+            LevelDatWriter.Write(this, level);
+            Write(outputDirPath);
+            textureImporter.Write();
+        }
+
+        private Dae.Scene LoadScene(string filePath)
+        {
+            if (sceneCache == null)
+            {
+                sceneCache = new Dictionary<string, Dae.Scene>(StringComparer.OrdinalIgnoreCase);
+            }
+
+            filePath = Path.GetFullPath(filePath);
+            Dae.Scene scene;
+
+            if (!sceneCache.TryGetValue(filePath, out scene))
+            {
+                scene = Dae.Reader.ReadFile(filePath);
+                sceneCache.Add(filePath, scene);
+            }
+
+            return scene;
+        }
+
+        private InstanceDescriptor FindSharedInstance(TemplateTag tag, string name)
+        {
+            if (sharedCache == null)
+                sharedCache = new Dictionary<string, InstanceDescriptor>(StringComparer.Ordinal);
+
+            string fullName = tag.ToString() + name;
+            InstanceDescriptor descriptor;
+
+            if (!sharedCache.TryGetValue(fullName, out descriptor))
+            {
+                var file = sharedManager.FindInstance(fullName);
+
+                if (file == null)
+                    error.WriteLine("Could not find {0} instance {1}", tag, name);
+                else if (file.Descriptors[0].Template.Tag != tag)
+                    error.WriteLine("Found '{0}' but its type {1} doesn't match the expected type {2}", name, file.Descriptors[0].Template.Tag, tag);
+                else
+                    descriptor = file.Descriptors[0];
+
+                sharedCache.Add(fullName, descriptor);
+            }
+
+            return descriptor;
+        }
+    }
+}
Index: /OniSplit/Level/ModelImporter.cs
===================================================================
--- /OniSplit/Level/ModelImporter.cs	(revision 1114)
+++ /OniSplit/Level/ModelImporter.cs	(revision 1114)
@@ -0,0 +1,527 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace Oni.Level
+{
+    using Akira;
+    using Metadata;
+    using Motoko;
+    using Physics;
+
+    partial class LevelImporter
+    {
+        private List<Dae.Scene> roomScenes;
+        private PolygonMesh model;
+        private AkiraDaeReader daeReader;
+
+        private void ReadModel(XmlReader xml, string basePath)
+        {
+            xml.ReadStartElement("Environment");
+
+            xml.ReadStartElement("Model");
+
+            daeReader = new AkiraDaeReader();
+            model = daeReader.Mesh;
+            level.model = model;
+
+            info.WriteLine("Reading environment...");
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Import":
+                    case "Scene":
+                        ImportModelScene(xml, basePath);
+                        break;
+
+                    case "Object":
+                        xml.Skip();
+                        break;
+
+                    case "Camera":
+                        ReadCamera(xml, basePath);
+                        break;
+
+                    case "Texture":
+                        textureImporter.ReadOptions(xml, basePath);
+                        break;
+
+                    default:
+                        error.WriteLine("Unknown element {0}", xml.LocalName);
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            info.WriteLine("Reading rooms...");
+
+            roomScenes = new List<Dae.Scene>();
+
+            xml.ReadEndElement();
+            xml.ReadStartElement("Rooms");
+
+            while (xml.IsStartElement("Import"))
+            {
+                string filePath = xml.GetAttribute("Path");
+
+                if (filePath == null)
+                    filePath = xml.ReadElementContentAsString();
+                else
+                    xml.Skip();
+
+                filePath = Path.Combine(basePath, filePath);
+
+                roomScenes.Add(LoadScene(filePath));
+            }
+
+            xml.ReadEndElement();
+
+            if (xml.IsStartElement("Textures"))
+                ReadTextures(xml, basePath);
+
+            xml.ReadEndElement();
+        }
+
+        private class NodePropertiesReader
+        {
+            private readonly string basePath;
+            private readonly TextWriter error;
+            public readonly Dictionary<string, AkiraDaeNodeProperties> properties = new Dictionary<string, AkiraDaeNodeProperties>(StringComparer.Ordinal);
+
+            public NodePropertiesReader(string basePath, TextWriter error)
+            {
+                this.basePath = basePath;
+                this.error = error;
+            }
+
+            public Dictionary<string, AkiraDaeNodeProperties> Properties
+            {
+                get { return properties; }
+            }
+
+            public void ReadScene(XmlReader xml, Dae.Node scene)
+            {
+                var nodeProperties = new ObjectDaeNodeProperties();
+                properties.Add(scene.Id, nodeProperties);
+
+                while (xml.IsStartElement())
+                {
+                    switch (xml.LocalName)
+                    {
+                        case "GunkFlags":
+                            nodeProperties.GunkFlags = xml.ReadElementContentAsEnum<GunkFlags>();
+                            break;
+                        case "ScriptId":
+                            nodeProperties.ScriptId = xml.ReadElementContentAsInt();
+                            break;
+                        case "Node":
+                            ReadNode(xml, scene, nodeProperties);
+                            break;
+                        default:
+                            xml.Skip();
+                            break;
+                    }
+                }
+            }
+
+            private void ReadNode(XmlReader xml, Dae.Node parentNode, ObjectDaeNodeProperties parentNodeProperties)
+            {
+                string id = xml.GetAttribute("Id");
+
+                if (string.IsNullOrEmpty(id))
+                {
+                    error.Write("Each import node must have an Id attribute");
+                    xml.Skip();
+                    return;
+                }
+
+                var nodeProperties = new ObjectDaeNodeProperties {
+                    GunkFlags = parentNodeProperties.GunkFlags,
+                    ScriptId = parentNodeProperties.ScriptId,
+                    HasPhysics = parentNodeProperties.HasPhysics
+                };
+
+                properties.Add(id, nodeProperties);
+
+                xml.ReadStartElement("Node");
+
+                while (xml.IsStartElement())
+                {
+                    switch (xml.LocalName)
+                    {
+                        case "GunkFlags":
+                            nodeProperties.GunkFlags |= xml.ReadElementContentAsEnum<GunkFlags>();
+                            break;
+                        case "ScriptId":
+                            nodeProperties.ScriptId = xml.ReadElementContentAsInt();
+                            break;
+                        case "Physics":
+                            nodeProperties.PhysicsType = xml.ReadElementContentAsEnum<ObjectPhysicsType>();
+                            nodeProperties.HasPhysics = true;
+                            break;
+                        case "ObjectFlags":
+                            nodeProperties.ObjectFlags = xml.ReadElementContentAsEnum<ObjectSetupFlags>();
+                            nodeProperties.HasPhysics = true;
+                            break;
+                        case "Animation":
+                            nodeProperties.Animations.Add(ReadAnimationClip(xml));
+                            nodeProperties.HasPhysics = true;
+                            break;
+                        case "Particles":
+                            nodeProperties.Particles.AddRange(ReadParticles(xml, basePath));
+                            nodeProperties.HasPhysics = true;
+                            break;
+                        default:
+                            error.WriteLine("Unknown physics object element {0}", xml.LocalName);
+                            xml.Skip();
+                            break;
+                    }
+                }
+
+                xml.ReadEndElement();
+            }
+
+            private ObjectAnimationClip ReadAnimationClip(XmlReader xml)
+            {
+                var animClip = new ObjectAnimationClip(xml.GetAttribute("Name"));
+
+                if (!xml.SkipEmpty())
+                {
+                    xml.ReadStartElement();
+
+                    while (xml.IsStartElement())
+                    {
+                        switch (xml.LocalName)
+                        {
+                            case "Start":
+                                animClip.Start = xml.ReadElementContentAsInt();
+                                break;
+                            case "Stop":
+                                animClip.Stop = xml.ReadElementContentAsInt();
+                                break;
+                            case "End":
+                                animClip.End = xml.ReadElementContentAsInt();
+                                break;
+                            case "Flags":
+                                animClip.Flags = xml.ReadElementContentAsEnum<ObjectAnimationFlags>();
+                                break;
+                            default:
+                                error.WriteLine("Unknown object animation property {0}", xml.LocalName);
+                                xml.Skip();
+                                break;
+                        }
+                    }
+
+                    xml.ReadEndElement();
+                }
+
+                return animClip;
+            }
+        }
+
+        private void ImportModelScene(XmlReader xml, string basePath)
+        {
+            var filePath = Path.Combine(basePath, xml.GetAttribute("Path"));
+            var scene = LoadScene(filePath);
+
+            var propertiesReader = new NodePropertiesReader(basePath, error);
+
+            if (!xml.SkipEmpty())
+            {
+                xml.ReadStartElement();
+                propertiesReader.ReadScene(xml, scene);
+                xml.ReadEndElement();
+            }
+
+            daeReader.ReadScene(scene, propertiesReader.Properties);
+
+            if (propertiesReader.Properties.Values.Any(p => p.HasPhysics))
+            {
+                var imp = new ObjectDaeImporter(textureImporter, propertiesReader.Properties);
+
+                imp.Import(scene);
+
+                foreach (var node in imp.Nodes.Where(n => n.Geometries.Length > 0))
+                {
+                    var setup = new ObjectSetup {
+                        Name = node.Name,
+                        FileName = node.FileName,
+                        ScriptId = node.ScriptId,
+                        Flags = node.Flags,
+                        PhysicsType = ObjectPhysicsType.Animated
+                    };
+
+                    setup.Geometries = node.Geometries
+                        .Where(n => (n.Flags & GunkFlags.Invisible) == 0)
+                        .Select(n => n.Geometry.Name).ToArray();
+
+                    foreach (var nodeGeometry in node.Geometries.Where(g => (g.Flags & GunkFlags.Invisible) == 0))
+                    {
+                        var writer = new DatWriter();
+                        GeometryDatWriter.Write(nodeGeometry.Geometry, writer.ImporterFile);
+                        writer.Write(outputDirPath);
+                    }
+
+                    setup.Position = Vector3.Zero;
+                    setup.Orientation = Quaternion.Identity;
+                    setup.Scale = 1.0f;
+                    setup.Origin = Matrix.CreateFromQuaternion(setup.Orientation)
+                        * Matrix.CreateScale(setup.Scale)
+                        * Matrix.CreateTranslation(setup.Position);
+
+                    //int i = 0;
+
+                    foreach (var animation in node.Animations)
+                    {
+                        //if (nodes.Count > 1)
+                        //    animation.Name += i.ToString("d2", CultureInfo.InvariantCulture);
+
+                        if ((animation.Flags & ObjectAnimationFlags.Local) == 0)
+                        {
+                            //animation.Scale = Matrix.CreateScale(setup.Scale);
+
+                            foreach (var key in animation.Keys)
+                            {
+                                key.Rotation = setup.Orientation * key.Rotation;
+                                key.Translation += setup.Position;
+                            }
+                        }
+
+                        if ((animation.Flags & ObjectAnimationFlags.AutoStart) != 0)
+                        {
+                            setup.Animation = animation;
+                            setup.PhysicsType = ObjectPhysicsType.Animated;
+                        }
+
+                        var writer = new DatWriter();
+                        writer.BeginImport();
+                        ObjectDatWriter.WriteAnimation(animation, writer);
+                        writer.Write(outputDirPath);
+                    }
+
+                    if (setup.Animation == null && node.Animations.Length > 0)
+                    {
+                        setup.Animation = node.Animations[0];
+                    }
+
+                    if (setup.Animation != null)
+                    {
+                        var frame0 = setup.Animation.Keys[0];
+
+                        setup.Scale = frame0.Scale.X;
+                        setup.Orientation = frame0.Rotation;
+                        setup.Position = frame0.Translation;
+                    }
+
+                    level.physics.Add(setup);
+                }
+            }
+        }
+
+        private void ImportModel(string basePath)
+        {
+            info.WriteLine("Importing objects...");
+            ImportGunkObjects();
+
+            info.WriteLine("Importing textures...");
+            ImportModelTextures();
+
+            info.WriteLine("Generating grids...");
+
+            string gridFilePath = Path.Combine(basePath, string.Format("temp/grids/{0}_grids.dae", level.name));
+
+            var gridBuilder = new RoomGridBuilder(roomScenes[0], model);
+            gridBuilder.Build();
+            AkiraDaeWriter.WriteRooms(gridBuilder.Mesh, gridFilePath);
+
+            daeReader.ReadScene(Dae.Reader.ReadFile(gridFilePath), new Dictionary<string, AkiraDaeNodeProperties>());
+
+            info.WriteLine("Writing environment...");
+
+            var writer = new DatWriter();
+            AkiraDatWriter.Write(model, writer, level.name, debug);
+            writer.Write(outputDirPath);
+        }
+
+        private void ImportGunkNode(int gunkId, Matrix transform, GunkFlags flags, Geometry geometry)
+        {
+            ImportGunk(gunkId, transform, flags, geometry, null);
+        }
+
+        private void ImportGunk(int gunkId, Matrix transform, GunkFlags flags, Geometry geometry, string textureName)
+        {
+            TextureFormat? textureFormat = null;
+
+            if (geometry.Texture != null)
+            {
+                Texture texture = null;
+
+                if (!geometry.Texture.IsPlaceholder)
+                {
+                    texture = TextureDatReader.ReadInfo(geometry.Texture);
+                }
+                else
+                {
+                    var txmp = FindSharedInstance(TemplateTag.TXMP, geometry.Texture.Name);
+
+                    if (txmp != null)
+                        texture = TextureDatReader.ReadInfo(txmp);
+                }
+
+                if (texture != null)
+                    textureFormat = texture.Format;
+            }
+            else
+            {
+                if (geometry.TextureName != null)
+                {
+                    var options = textureImporter.GetOptions(geometry.TextureName, false);
+
+                    if (options != null)
+                        textureFormat = options.Format;
+                }
+            }
+
+            switch (textureFormat)
+            {
+                case TextureFormat.BGRA4444:
+                case TextureFormat.BGRA5551:
+                case TextureFormat.RGBA:
+                    flags |= GunkFlags.Transparent | GunkFlags.TwoSided | GunkFlags.NoOcclusion;
+                    break;
+            }
+
+            Material material;
+
+            if (!string.IsNullOrEmpty(textureName))
+                material = model.Materials.GetMaterial(textureName);
+            else if (!string.IsNullOrEmpty(geometry.TextureName))
+                material = model.Materials.GetMaterial(geometry.TextureName);
+            else if (geometry.Texture != null)
+                material = model.Materials.GetMaterial(geometry.Texture.Name);
+            else
+                material = model.Materials.GetMaterial("NONE");
+
+            int pointIndexBase = model.Points.Count;
+            int texCoordIndexBase = model.TexCoords.Count;
+
+            model.Points.AddRange(Vector3.Transform(geometry.Points, ref transform));
+            model.TexCoords.AddRange(geometry.TexCoords);
+
+            foreach (var quad in Quadify.Do(geometry))
+            {
+                var pointIndices = new int[quad.Length];
+                var texCoordIndices = new int[quad.Length];
+                var colors = new Imaging.Color[quad.Length];
+
+                for (int j = 0; j < quad.Length; j++)
+                {
+                    pointIndices[j] = pointIndexBase + quad[j];
+                    texCoordIndices[j] = texCoordIndexBase + quad[j];
+                    colors[j] = new Imaging.Color(207, 207, 207);
+                }
+
+                var poly = new Polygon(model, pointIndices) {
+                    TexCoordIndices = texCoordIndices,
+                    Colors = colors,
+                    Material = material,
+                    ObjectId = gunkId & 0xffffff,
+                    ObjectType = gunkId >> 24
+                };
+
+                poly.Flags |= flags;
+                model.Polygons.Add(poly);
+            }
+        }
+
+        private void ImportModelTextures()
+        {
+            int imported = 0;
+            int copied = 0;
+
+            var copy = new List<InstanceDescriptor>();
+
+            foreach (var material in model.Polygons.Select(p => p.Material).Distinct())
+            {
+                //if (material.IsMarker)
+                //    continue;
+
+                if (File.Exists(material.ImageFilePath))
+                {
+                    var options = textureImporter.AddMaterial(material);
+
+                    if (options != null)
+                        material.Flags |= options.GunkFlags;
+
+                    imported++;
+                }
+                else
+                {
+                    var txmp = FindSharedInstance(TemplateTag.TXMP, material.Name);
+
+                    if (txmp != null)
+                        copy.Add(txmp);
+                }
+            }
+
+            Parallel.ForEach(copy, txmp => {
+                var texture = TextureDatReader.Read(txmp);
+
+                if ((texture.Flags & TextureFlags.HasMipMaps) == 0)
+                {
+                    texture.GenerateMipMaps();
+                    TextureDatWriter.Write(texture, outputDirPath);
+                    System.Threading.Interlocked.Increment(ref imported);
+                }
+                else
+                {
+                    string sourceFilePath = txmp.File.FilePath;
+                    File.Copy(sourceFilePath, Path.Combine(outputDirPath, Path.GetFileName(sourceFilePath)), true);
+                    System.Threading.Interlocked.Increment(ref copied);
+                }
+            });
+
+            error.WriteLine("Imported {0} textures, copied {1} textures", imported, copied);
+        }
+
+        private ObjectAnimationClip ReadAnimationClip(XmlReader xml)
+        {
+            var anim = new ObjectAnimationClip(xml.GetAttribute("Name"));
+
+            if (!xml.SkipEmpty())
+            {
+                xml.ReadStartElement();
+
+                while (xml.IsStartElement())
+                {
+                    switch (xml.LocalName)
+                    {
+                        case "Start":
+                            anim.Start = xml.ReadElementContentAsInt();
+                            break;
+                        case "Stop":
+                            anim.Stop = xml.ReadElementContentAsInt();
+                            break;
+                        case "End":
+                            anim.End = xml.ReadElementContentAsInt();
+                            break;
+                        case "Flags":
+                            anim.Flags = xml.ReadElementContentAsEnum<ObjectAnimationFlags>();
+                            break;
+                        default:
+                            error.WriteLine("Unknown object animation parameter {0}", xml.LocalName);
+                            xml.Skip();
+                            break;
+                    }
+                }
+
+                xml.ReadEndElement();
+            }
+
+            return anim;
+        }
+    }
+}
Index: /OniSplit/Level/ObjectImporter.cs
===================================================================
--- /OniSplit/Level/ObjectImporter.cs	(revision 1114)
+++ /OniSplit/Level/ObjectImporter.cs	(revision 1114)
@@ -0,0 +1,473 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using Oni.Xml;
+
+namespace Oni.Level
+{
+    using Akira;
+    using Motoko;
+    using Objects;
+    using Physics;
+
+    partial class LevelImporter
+    {
+        private List<ObjectBase> objects;
+        private ObjectLoadContext objectLoadContext;
+
+        private void ReadObjects(XmlReader xml, string basePath)
+        {
+            info.WriteLine("Reading objects...");
+
+            objects = new List<ObjectBase>();
+            objectLoadContext = new ObjectLoadContext(FindSharedInstance, info);
+
+            xml.ReadStartElement("Objects");
+
+            while (xml.IsStartElement())
+                ReadObjectFile(Path.Combine(basePath, xml.ReadElementContentAsString("Import", "")));
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadObjectFile(string filePath)
+        {
+            var basePath = Path.GetDirectoryName(filePath);
+
+            objectLoadContext.BasePath = basePath;
+            objectLoadContext.FilePath = filePath;
+
+            var settings = new XmlReaderSettings {
+                IgnoreWhitespace = true,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
+
+            using (var xml = XmlReader.Create(filePath, settings))
+            {
+                xml.ReadStartElement("Oni");
+
+                switch (xml.LocalName)
+                {
+                    case "Objects":
+                        objects.AddRange(ReadObjects(xml));
+                        break;
+
+                    case "Particles":
+                        level.particles.AddRange(ReadParticles(xml, basePath));
+                        break;
+
+                    case "Characters":
+                        level.characters.AddRange(ReadCharacters(xml, basePath));
+                        break;
+
+                    case "Physics":
+                        ReadPhysics(xml, basePath);
+                        break;
+
+                    case "Corpses":
+                    case "CRSA":
+                        level.corpses.AddRange(ReadCorpses(xml, basePath));
+                        break;
+
+                    default:
+                        error.WriteLine("Unknown object file type {0}", xml.LocalName);
+                        xml.Skip();
+                        break;
+                }
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private IEnumerable<ObjectBase> ReadObjects(XmlReader xml)
+        {
+            if (xml.SkipEmpty())
+                yield break;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                ObjectBase obj;
+
+                switch (xml.LocalName)
+                {
+                    case "CHAR":
+                    case "Character":
+                        obj = new Character();
+                        break;
+                    case "WEAP":
+                    case "Weapon":
+                        obj = new Weapon();
+                        break;
+                    case "PART":
+                    case "Particle":
+                        obj = new Particle();
+                        break;
+                    case "PWRU":
+                    case "PowerUp":
+                        obj = new PowerUp();
+                        break;
+                    case "FLAG":
+                    case "Flag":
+                        obj = new Flag();
+                        break;
+                    case "DOOR":
+                    case "Door":
+                        obj = new Door();
+                        break;
+                    case "CONS":
+                    case "Console":
+                        obj = new Console();
+                        break;
+                    case "FURN":
+                    case "Furniture":
+                        obj = new Furniture();
+                        break;
+                    case "TURR":
+                    case "Turret":
+                        obj = new Turret();
+                        break;
+                    case "SNDG":
+                    case "Sound":
+                        obj = new Sound();
+                        break;
+                    case "TRIG":
+                    case "Trigger":
+                        obj = new Trigger();
+                        break;
+                    case "TRGV":
+                    case "TriggerVolume":
+                        obj = new TriggerVolume();
+                        break;
+                    case "NEUT":
+                    case "Neutral":
+                        obj = new Neutral();
+                        break;
+                    case "PATR":
+                    case "Patrol":
+                        obj = new PatrolPath();
+                        break;
+                    default:
+                        error.WriteLine("Unknonw object type {0}", xml.LocalName);
+                        xml.Skip();
+                        continue;
+                }
+
+                obj.Read(xml, objectLoadContext);
+
+                var gunkObject = obj as GunkObject;
+
+                if (gunkObject != null && gunkObject.GunkClass == null)
+                    continue;
+
+                yield return obj;
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ImportGunkObjects()
+        {
+            int nextObjectId = 1;
+
+            foreach (var obj in objects)
+            {
+                obj.ObjectId = nextObjectId++;
+
+                if (obj is Door)
+                    ImportDoor((Door)obj);
+                else if (obj is Furniture)
+                    ImportFurniture((Furniture)obj);
+                else if (obj is GunkObject)
+                    ImportGunkObject((GunkObject)obj, GunkFlags.NoOcclusion);
+            }
+        }
+
+        private void ImportFurniture(Furniture furniture)
+        {
+            ImportGunkObject(furniture, GunkFlags.NoOcclusion | GunkFlags.Furniture);
+
+            foreach (var particle in furniture.Class.Geometry.Particles)
+                ImportParticle(furniture.ParticleTag, furniture.Transform, particle);
+        }
+
+        private void ImportGunkObject(GunkObject gunkObject, GunkFlags flags)
+        {
+            foreach (var node in gunkObject.GunkClass.GunkNodes)
+                ImportGunkNode(gunkObject.GunkId, gunkObject.Transform, node.Flags | flags, node.Geometry);
+        }
+
+        private void ImportDoor(Door door)
+        {
+            var placement = door.Transform;
+
+            float minY = 0.0f;
+            float minX = 0.0f;
+            float maxX = 0.0f;
+
+            var geometryTransform = Matrix.CreateScale(door.Class.Animation.Keys[0].Scale) * Matrix.CreateRotationX(-MathHelper.HalfPi);
+
+            foreach (var node in door.Class.GunkNodes)
+            {
+                var bbox = BoundingBox.CreateFromPoints(Vector3.Transform(node.Geometry.Points, ref geometryTransform));
+
+                minY = Math.Min(minY, bbox.Min.Y);
+                minX = Math.Min(minX, bbox.Min.X);
+                maxX = Math.Max(maxX, bbox.Max.X);
+            }
+
+            placement.Translation -= Vector3.UnitY * minY;
+
+            float xOffset;
+            int sides;
+
+            if ((door.Flags & DoorFlags.DoubleDoor) == 0)
+            {
+                xOffset = 0.0f;
+                sides = 1;
+            }
+            else
+            {
+                xOffset = (maxX - minX) / 2.0f;
+                sides = 2;
+            }
+
+            for (int side = 0; side < sides; side++)
+            {
+                Matrix origin, gunkTransform;
+
+                if (side == 0)
+                {
+                    var m1 = Matrix.CreateTranslation(xOffset, 0.0f, 0.0f) * placement;
+                    origin = geometryTransform * m1;
+                    gunkTransform = origin;
+                }
+                else
+                {
+                    var m2 = Matrix.CreateTranslation(-xOffset, 0.0f, 0.0f) * placement;
+                    origin = Matrix.CreateRotationY(MathHelper.Pi) * geometryTransform * m2;
+                    gunkTransform = geometryTransform * Matrix.CreateRotationY(MathHelper.Pi) * m2;
+                }
+
+                var scriptId = door.ScriptId | (side << 12);
+                var geometries = ImportDoorGeometry(door, side);
+
+                level.physics.Add(new ObjectSetup {
+                    Name = string.Format("door_{0}", scriptId),
+                    Flags = ObjectSetupFlags.FaceCollision,
+                    DoorScriptId = scriptId,
+                    Origin = origin,
+                    Geometries = geometries
+                });
+
+                foreach (var geometry in geometries)
+                    ImportGunkNode(door.GunkId, gunkTransform, GunkFlags.NoDecals | GunkFlags.NoCollision, geometry);
+            }
+        }
+
+        private Geometry[] ImportDoorGeometry(Door door, int side)
+        {
+            InstanceDescriptor overrideTexture = null;
+
+            if (!string.IsNullOrEmpty(door.Textures[side]))
+                overrideTexture = FindSharedInstance(TemplateTag.TXMP, door.Textures[side]);
+
+            var nodes = door.Class.Geometry.Geometries;
+            var geometries = new Geometry[nodes.Length];
+
+            for (int i = 0; i < nodes.Length; i++)
+            {
+                var node = nodes[i];
+
+                var geometry = new Geometry {
+                    Points = node.Geometry.Points,
+                    TexCoords = node.Geometry.TexCoords,
+                    Normals = node.Geometry.Normals,
+                    Triangles = node.Geometry.Triangles
+                };
+
+                if (overrideTexture != null)
+                {
+                    geometry.Texture = overrideTexture;
+                    geometry.TextureName = overrideTexture.Name;
+                }
+                else if (node.Geometry.Texture != null)
+                {
+                    geometry.TextureName = node.Geometry.Texture.Name;
+                }
+
+                geometries[i] = geometry;
+            }
+
+            return geometries;
+        }
+
+        private void WriteObjects()
+        {
+            ObjcDatWriter.Write(objects, outputDirPath);
+        }
+
+        private IEnumerable<Corpse> ReadCorpses(XmlReader xml, string basePath)
+        {
+            var fileName = Path.GetFileName(objectLoadContext.FilePath);
+
+            var isOldFormat = xml.IsStartElement("CRSA");
+            int readCount = 0, fixedCount = 0, usedCount = 0;
+
+            if (isOldFormat)
+            {
+                xml.ReadStartElement("CRSA");
+
+                if (xml.IsStartElement("FixedCount"))
+                    fixedCount = xml.ReadElementContentAsInt("FixedCount", "");
+
+                if (xml.IsStartElement("UsedCount"))
+                    usedCount = xml.ReadElementContentAsInt("UsedCount", "");
+
+                if (usedCount < fixedCount)
+                {
+                    error.WriteLine("There are more fixed corpses ({0}) than used corpses ({1}) - assuming fixed = used", fixedCount, usedCount);
+                    fixedCount = usedCount;
+                }
+            }
+
+            xml.ReadStartElement("Corpses");
+
+            while (xml.IsStartElement())
+            {
+                var corpse = new Corpse();
+                corpse.IsFixed = isOldFormat && readCount < fixedCount;
+                corpse.IsUsed = !isOldFormat || readCount < usedCount;
+                corpse.FileName = fileName;
+
+                if (xml.IsEmptyElement)
+                {
+                    corpse.IsUsed = false;
+                }
+
+                if (!corpse.IsUsed)
+                {
+                    xml.Skip();
+                }
+                else if (xml.LocalName == "Corpse" || xml.LocalName == "CRSACorpse")
+                {
+                    xml.ReadStartElement();
+
+                    if (!isOldFormat)
+                    {
+                        if (xml.IsStartElement("CanDelete"))
+                        {
+                            corpse.IsFixed = false;
+                            xml.Skip();
+                        }
+                        else
+                        {
+                            corpse.IsFixed = true;
+                        }
+                    }
+
+                    if (xml.IsStartElement("Class") || xml.IsStartElement("CharacterClass"))
+                        corpse.CharacterClass = xml.ReadElementContentAsString();
+
+                    if (string.IsNullOrEmpty(corpse.CharacterClass))
+                    {
+                        corpse.IsUsed = false;
+                        corpse.IsFixed = false;
+                    }
+
+                    xml.ReadStartElement("Transforms");
+
+                    for (int j = 0; j < corpse.Transforms.Length; j++)
+                    {
+                        if (xml.IsStartElement("Matrix4x3"))
+                            corpse.Transforms[j] = xml.ReadElementContentAsMatrix43("Matrix4x3");
+                        else if (xml.IsStartElement("Matrix"))
+                            corpse.Transforms[j] = xml.ReadElementContentAsMatrix43("Matrix");
+                    }
+
+                    xml.ReadEndElement();
+
+                    if (xml.IsStartElement("BoundingBox"))
+                    {
+                        xml.ReadStartElement("BoundingBox");
+                        corpse.BoundingBox.Min = xml.ReadElementContentAsVector3("Min");
+                        corpse.BoundingBox.Max = xml.ReadElementContentAsVector3("Max");
+                        xml.ReadEndElement();
+                    }
+                    else
+                    {
+                        corpse.BoundingBox.Min = corpse.Transforms[0].Translation;
+                        corpse.BoundingBox.Max = corpse.Transforms[0].Translation;
+                        corpse.BoundingBox.Inflate(new Vector3(10.0f, 5.0f, 10.0f));
+                    }
+
+                    xml.ReadEndElement();
+                }
+                else
+                {
+                    var filePath = xml.ReadElementContentAsString("Import", "");
+                    filePath = Path.Combine(basePath, filePath);
+
+                    using (var reader = new BinaryReader(filePath))
+                    {
+                        corpse.FileName = Path.GetFileName(filePath);
+                        corpse.IsUsed = true;
+                        corpse.IsFixed = true;
+
+                        corpse.CharacterClass = reader.ReadString(128);
+                        reader.Skip(4);
+
+                        for (int i = 0; i < corpse.Transforms.Length; i++)
+                            corpse.Transforms[i] = reader.ReadMatrix4x3();
+
+                        corpse.BoundingBox = reader.ReadBoundingBox();
+                    }
+                }
+
+                readCount++;
+                yield return corpse;
+            }
+
+            if (readCount < usedCount)
+            {
+                error.WriteLine("{0} corpses were expected but only {1} have been read", usedCount, readCount);
+            }
+
+            info.WriteLine("Read {0} corpses", readCount);
+        }
+
+        private InstanceFileManager fileManager;
+
+        private InstanceDescriptor FindSharedInstance(TemplateTag tag, string name, ObjectLoadContext loadContext)
+        {
+            if (!name.EndsWith(".oni", StringComparison.OrdinalIgnoreCase))
+                return FindSharedInstance(tag, name);
+
+            string filePath = Path.GetFullPath(Path.Combine(loadContext.BasePath, name));
+
+            if (File.Exists(filePath))
+            {
+                if (fileManager == null)
+                    fileManager = new InstanceFileManager();
+
+                return fileManager.OpenFile(filePath).Descriptors[0];
+            }
+
+            filePath = Path.GetFullPath(Path.Combine(sharedPath, name));
+
+            if (File.Exists(filePath))
+            {
+                var file = sharedManager.OpenFile(filePath);
+
+                return file.Descriptors[0];
+            }
+
+            error.WriteLine("Could not find {0}", name);
+
+            return null;
+        }
+    }
+}
Index: /OniSplit/Level/ParticleImporter.cs
===================================================================
--- /OniSplit/Level/ParticleImporter.cs	(revision 1114)
+++ /OniSplit/Level/ParticleImporter.cs	(revision 1114)
@@ -0,0 +1,35 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+
+namespace Oni.Level
+{
+    using Oni.Physics;
+
+    partial class LevelImporter
+    {
+        private static IEnumerable<ObjectParticle> ReadParticles(XmlReader xml, string basePath)
+        {
+            if (xml.SkipEmpty())
+                yield break;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+                yield return ObjectXmlReader.ReadParticle(xml);
+
+            xml.ReadEndElement();
+        }
+
+        private void ImportParticle(string tag, Matrix matrix, ObjectParticle particle)
+        {
+            level.particles.Add(new ObjectParticle {
+                ParticleClass = particle.ParticleClass,
+                Tag = tag + "_" + particle.Tag,
+                Matrix = particle.Matrix * matrix,
+                DecalScale = particle.DecalScale,
+                Flags = particle.Flags
+            });
+        }
+    }
+}
Index: /OniSplit/Level/PhysicsImporter.cs
===================================================================
--- /OniSplit/Level/PhysicsImporter.cs	(revision 1114)
+++ /OniSplit/Level/PhysicsImporter.cs	(revision 1114)
@@ -0,0 +1,245 @@
+﻿using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Xml;
+
+namespace Oni.Level
+{
+    using Akira;
+    using Metadata;
+    using Motoko;
+    using Physics;
+    using Xml;
+
+    partial class LevelImporter
+    {
+        private void ReadPhysics(XmlReader xml, string basePath)
+        {
+            if (xml.SkipEmpty())
+                return;
+
+            xml.ReadStartElement("Physics");
+
+            while (xml.IsStartElement())
+                ReadObjectSetup(xml, basePath);
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadObjectSetup(XmlReader xml, string basePath)
+        {
+            var scriptId = -1;
+            var name = xml.GetAttribute("Name");
+            var position = Vector3.Zero;
+            var rotation = Quaternion.Identity;
+            var scale = 1.0f;
+            var flags = ObjectSetupFlags.None;
+            var physicsType = ObjectPhysicsType.None;
+            var particles = new List<ObjectParticle>();
+            var nodes = new List<ObjectNode>();
+            string geometryName = null;
+            string animationName = null;
+
+            xml.ReadStartElement("Object");
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Name":
+                        name = xml.ReadElementContentAsString();
+                        break;
+                    case "ScriptId":
+                        scriptId = xml.ReadElementContentAsInt();
+                        break;
+                    case "Flags":
+                        flags = xml.ReadElementContentAsEnum<ObjectSetupFlags>() & ~ObjectSetupFlags.InUse;
+                        break;
+                    case "Position":
+                        position = xml.ReadElementContentAsVector3();
+                        break;
+                    case "Rotation":
+                        rotation = xml.ReadElementContentAsEulerXYZ();
+                        break;
+                    case "Scale":
+                        scale = xml.ReadElementContentAsFloat();
+                        break;
+                    case "Physics":
+                        physicsType = xml.ReadElementContentAsEnum<ObjectPhysicsType>();
+                        break;
+                    case "Particles":
+                        particles.AddRange(ReadParticles(xml, basePath));
+                        break;
+
+                    case "Geometry":
+                        geometryName = xml.ReadElementContentAsString();
+                        if (nodes.Count > 0)
+                        {
+                            error.WriteLine("Geometry cannot be used together with Import, ignoring");
+                            break;
+                        }
+                        break;
+
+                    case "Animation":
+                        animationName = xml.ReadElementContentAsString();
+                        if (nodes.Count > 0)
+                        {
+                            error.WriteLine("Animation cannot be used together with Import, ignoring");
+                            break;
+                        }
+                        break;
+
+                    case "Import":
+                        if (geometryName != null || animationName != null)
+                        {
+                            error.WriteLine("Import cannot be used together with Geometry and Animation, ignoring");
+                            break;
+                        }
+                        nodes.AddRange(ImportObjectGeometry(xml, basePath));
+                        break;
+
+                    default:
+                        error.WriteLine("Unknown physics object element {0}", xml.LocalName);
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+
+            if (geometryName != null)
+            {
+                var m3gm = FindSharedInstance(TemplateTag.M3GM, geometryName, objectLoadContext);
+                var geometry = GeometryDatReader.Read(m3gm);
+                var animation = new ObjectAnimation[0];
+
+                if (animationName != null)
+                {
+                    var oban = FindSharedInstance(TemplateTag.OBAN, animationName, objectLoadContext);
+                    animation = new[] { ObjectDatReader.ReadAnimation(oban) };
+                }
+
+                nodes.Add(new ObjectNode(new[] { new ObjectGeometry(geometry) }) {
+                    FileName = Path.GetFileName(geometryName),
+                    Name = m3gm.Name,
+                    ScriptId = scriptId,
+                    Flags = flags,
+                    Animations = animation
+                });
+            }
+
+            for (int i = 0; i < nodes.Count; i++)
+            {
+                var node = nodes[i];
+
+                var setup = new ObjectSetup {
+                    Name = node.Name,
+                    FileName = node.FileName,
+                    ScriptId = scriptId++,
+                    Flags = flags,
+                    PhysicsType = physicsType,
+                };
+
+                setup.Particles.AddRange(particles);
+
+                setup.Geometries = node.Geometries
+                    .Where(n => (n.Flags & GunkFlags.Invisible) == 0)
+                    .Select(n => n.Geometry.Name).ToArray();
+
+                foreach (var nodeGeometry in node.Geometries.Where(g => (g.Flags & GunkFlags.Invisible) == 0))
+                {
+                    var writer = new DatWriter();
+                    GeometryDatWriter.Write(nodeGeometry.Geometry, writer.ImporterFile);
+                    writer.Write(outputDirPath);
+                }
+
+                setup.Position = position;
+                setup.Orientation = rotation;
+                setup.Scale = scale;
+                setup.Origin = Matrix.CreateFromQuaternion(setup.Orientation)
+                    * Matrix.CreateScale(setup.Scale)
+                    * Matrix.CreateTranslation(setup.Position);
+
+                foreach (var animation in node.Animations)
+                {
+                    if (nodes.Count > 1)
+                        animation.Name += i.ToString("d2", CultureInfo.InvariantCulture);
+
+                    if ((animation.Flags & ObjectAnimationFlags.Local) != 0)
+                    {
+                        //animation.Scale = Matrix.CreateScale(setup.Scale);
+
+                        foreach (var key in animation.Keys)
+                        {
+                            key.Rotation = setup.Orientation * key.Rotation;
+                            key.Translation += setup.Position;
+                        }
+                    }
+
+                    if ((animation.Flags & ObjectAnimationFlags.AutoStart) != 0)
+                    {
+                        setup.Animation = animation;
+                        setup.PhysicsType = ObjectPhysicsType.Animated;
+                    }
+
+                    var writer = new DatWriter();
+                    ObjectDatWriter.WriteAnimation(animation, writer);
+                    writer.Write(outputDirPath);
+                }
+
+                if (setup.Animation == null && node.Animations.Length > 0)
+                {
+                    setup.Animation = node.Animations[0];
+                }
+
+                if (setup.Animation != null)
+                {
+                    var frame0 = setup.Animation.Keys[0];
+
+                    setup.Scale = frame0.Scale.X;
+                    setup.Orientation = frame0.Rotation;
+                    setup.Position = frame0.Translation;
+                }
+
+                level.physics.Add(setup);
+            }
+        }
+
+        private IEnumerable<ObjectNode> ImportObjectGeometry(XmlReader xml, string basePath)
+        {
+            var filePath = xml.GetAttribute("Path");
+
+            if (filePath == null)
+                filePath = xml.GetAttribute("Url");
+
+            var scene = LoadScene(Path.Combine(basePath, filePath));
+            var animClips = new List<ObjectAnimationClip>();
+
+            if (!xml.SkipEmpty())
+            {
+                xml.ReadStartElement();
+
+                while (xml.IsStartElement())
+                {
+                    switch (xml.LocalName)
+                    {
+                        case "Animation":
+                            animClips.Add(ReadAnimationClip(xml));
+                            break;
+
+                        default:
+                            error.WriteLine("Unknown element {0}", xml.LocalName);
+                            xml.Skip();
+                            break;
+                    }
+                }
+
+                xml.ReadEndElement();
+            }
+
+            var importer = new ObjectDaeImporter(textureImporter, null);
+            importer.Import(scene);
+            return importer.Nodes;
+        }
+    }
+}
Index: /OniSplit/Level/ScriptCharacter.cs
===================================================================
--- /OniSplit/Level/ScriptCharacter.cs	(revision 1114)
+++ /OniSplit/Level/ScriptCharacter.cs	(revision 1114)
@@ -0,0 +1,58 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Level
+{
+    internal class ScriptCharacter
+    {
+        public string className;
+        public string name;
+        public string weaponClassName;
+        public int flagId;
+        public int scriptId;
+        public InstanceMetadata.AISACharacterFlags flags;
+        public InstanceMetadata.AISACharacterTeam team;
+        public string onSpawn;
+        public string onDeath;
+        public string onSeenEnemy;
+        public string onAlarmed;
+        public string onHurt;
+        public string onDefeated;
+        public string onOutOfAmmo;
+        public string onNoPath;
+        public int ammo;
+
+        public static ScriptCharacter Read(XmlReader xml)
+        {
+            xml.ReadStartElement("Character");
+
+            var chr = new ScriptCharacter {
+                name = xml.ReadElementContentAsString("Name", ""),
+                scriptId = xml.ReadElementContentAsInt("ScriptId", ""),
+                flagId = xml.ReadElementContentAsInt("FlagId", ""),
+                flags = xml.ReadElementContentAsEnum<InstanceMetadata.AISACharacterFlags>("Flags"),
+                team = xml.ReadElementContentAsEnum<InstanceMetadata.AISACharacterTeam>("Team"),
+                className = xml.ReadElementContentAsString("Class", "")
+            };
+
+            xml.ReadStartElement("Scripts");
+            chr.onSpawn = xml.ReadElementContentAsString("Spawn", "");
+            chr.onDeath = xml.ReadElementContentAsString("Die", "");
+            chr.onSeenEnemy = xml.ReadElementContentAsString("Combat", "");
+            chr.onAlarmed = xml.ReadElementContentAsString("Alarm", "");
+            chr.onHurt = xml.ReadElementContentAsString("Hurt", "");
+            chr.onDefeated = xml.ReadElementContentAsString("Defeated", "");
+            chr.onOutOfAmmo = xml.ReadElementContentAsString("OutOfAmmo", "");
+            chr.onNoPath = xml.ReadElementContentAsString("NoPath", "");
+            xml.ReadEndElement();
+
+            chr.weaponClassName = xml.ReadElementContentAsString("Weapon", "");
+            chr.ammo = xml.ReadElementContentAsInt("Ammo", "");
+
+            xml.ReadEndElement();
+
+            return chr;
+        }
+    }
+}
Index: /OniSplit/Level/SkyImporter.cs
===================================================================
--- /OniSplit/Level/SkyImporter.cs	(revision 1114)
+++ /OniSplit/Level/SkyImporter.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+
+namespace Oni.Level
+{
+    partial class LevelImporter
+    {
+        private void ReadSky(XmlReader xml, string basePath)
+        {
+            level.skyName = xml.ReadElementContentAsString("Sky", "");
+        }
+    }
+}
Index: /OniSplit/Level/TextureImporter.cs
===================================================================
--- /OniSplit/Level/TextureImporter.cs	(revision 1114)
+++ /OniSplit/Level/TextureImporter.cs	(revision 1114)
@@ -0,0 +1,68 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace Oni.Level
+{
+    using Oni.Akira;
+    using Oni.Metadata;
+    using Oni.Motoko;
+    using Oni.Xml;
+
+    partial class LevelImporter
+    {
+        private Motoko.TextureImporter3 textureImporter;
+        private TextureFormat defaultTextureFormat = TextureFormat.BGR;
+        private TextureFormat defaultAlphaTextureFormat = TextureFormat.RGBA;
+        private int maxTextureSize = 512;
+
+        private void ReadTextures(XmlReader xml, string basePath)
+        {
+            if (xml.SkipEmpty())
+                return;
+
+            string format = xml.GetAttribute("Format");
+            string alphaFormat = xml.GetAttribute("AlphaFormat");
+            string size = xml.GetAttribute("MaxSize");
+
+            if (format != null)
+                defaultTextureFormat = TextureImporter.ParseTextureFormat(format);
+
+            if (alphaFormat != null)
+                defaultAlphaTextureFormat = TextureImporter.ParseTextureFormat(alphaFormat);
+
+            if (size != null)
+                maxTextureSize = int.Parse(size);
+
+            xml.ReadStartElement("Textures");
+
+            while (xml.IsStartElement())
+            {
+                if (xml.LocalName == "Import")
+                {
+                    var importPath = Path.Combine(basePath, xml.ReadElementContentAsString());
+
+                    var settings = new XmlReaderSettings {
+                        IgnoreWhitespace = true,
+                        IgnoreProcessingInstructions = true,
+                        IgnoreComments = true
+                    };
+
+                    using (var importXml = XmlReader.Create(importPath, settings))
+                    {
+                        importXml.ReadStartElement("Oni");
+                        ReadTextures(importXml, Path.GetDirectoryName(importPath));
+                        importXml.ReadEndElement();
+                    }
+                }
+                else
+                {
+                    textureImporter.ReadOptions(xml, basePath);
+                }
+            }
+
+            xml.ReadEndElement();
+        }
+    }
+}
Index: /OniSplit/Math/BoundingBox.cs
===================================================================
--- /OniSplit/Math/BoundingBox.cs	(revision 1114)
+++ /OniSplit/Math/BoundingBox.cs	(revision 1114)
@@ -0,0 +1,112 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni
+{
+    internal struct BoundingBox : IEquatable<BoundingBox>
+    {
+        public Vector3 Min;
+        public Vector3 Max;
+
+        public BoundingBox(Vector3 min, Vector3 max)
+        {
+            Min = min;
+            Max = max;
+        }
+
+        public static BoundingBox CreateFromSphere(BoundingSphere sphere)
+        {
+            var radius = new Vector3(sphere.Radius);
+
+            return new BoundingBox(sphere.Center - radius, sphere.Center + radius);
+        }
+
+        public static BoundingBox CreateFromPoints(IEnumerable<Vector3> points)
+        {
+            var min = new Vector3(float.MaxValue);
+            var max = new Vector3(float.MinValue);
+
+            foreach (var point in points)
+            {
+                var p = point;
+
+                Vector3.Min(ref min, ref p, out min);
+                Vector3.Max(ref max, ref p, out max);
+            }
+
+            return new BoundingBox(min, max);
+        }
+
+        public bool Contains(Vector3 point) =>
+            point.X >= Min.X && point.X <= Max.X
+            && point.Y >= Min.Y && point.Y <= Max.Y
+            && point.Z >= Min.Z && point.Z <= Max.Z;
+
+        public bool Contains(BoundingBox box) =>
+            Min.X <= box.Min.X && box.Max.X <= Max.X
+            && Min.Y <= box.Min.Y && box.Max.Y <= Max.Y
+            && Min.Z <= box.Min.Z && box.Max.Z <= Max.Z;
+
+        public bool Intersects(BoundingBox box) =>
+            Max.X >= box.Min.X && Min.X <= box.Max.X
+            && Max.Y >= box.Min.Y && Min.Y <= box.Max.Y
+            && Max.Z >= box.Min.Z && Min.Z <= box.Max.Z;
+
+        public bool Intersects(Plane plane)
+        {
+            Vector3 v0, v1;
+
+            v0.X = (plane.Normal.X >= 0.0f) ? Max.X : Min.X;
+            v1.X = (plane.Normal.X >= 0.0f) ? Min.X : Max.X;
+
+            v0.Y = (plane.Normal.Y >= 0.0f) ? Max.Y : Min.Y;
+            v1.Y = (plane.Normal.Y >= 0.0f) ? Min.Y : Max.Y;
+
+            v0.Z = (plane.Normal.Z >= 0.0f) ? Max.Z : Min.Z;
+            v1.Z = (plane.Normal.Z >= 0.0f) ? Min.Z : Max.Z;
+
+            return plane.Normal.Dot(ref v1) <= -plane.D
+                && plane.Normal.Dot(ref v0) >= -plane.D;
+        }
+
+        public Vector3[] GetCorners() => new[]
+        {
+            new Vector3(Min.X, Max.Y, Max.Z),
+            new Vector3(Max.X, Max.Y, Max.Z),
+            new Vector3(Max.X, Min.Y, Max.Z),
+            new Vector3(Min.X, Min.Y, Max.Z),
+            new Vector3(Min.X, Max.Y, Min.Z),
+            new Vector3(Max.X, Max.Y, Min.Z),
+            new Vector3(Max.X, Min.Y, Min.Z),
+            new Vector3(Min.X, Min.Y, Min.Z)
+        };
+
+        public static bool operator ==(BoundingBox b1, BoundingBox b2) => b1.Min == b2.Min && b1.Max == b2.Max;
+        public static bool operator !=(BoundingBox b1, BoundingBox b2) => b1.Min != b2.Min || b1.Max != b2.Max;
+
+        public bool Equals(BoundingBox other) => Min == other.Min && Max == other.Max;
+
+        public override bool Equals(object obj) => obj is BoundingBox && Equals((BoundingBox)obj);
+        public override int GetHashCode() => Min.GetHashCode() ^ Max.GetHashCode();
+
+        public override string ToString() => $"{{{Min} {Max}}}";
+
+        public float Volume()
+        {
+            Vector3 size = Max - Min;
+            return size.X * size.Y * size.Z;
+        }
+
+        public void Inflate(Vector3 v)
+        {
+            Min -= v;
+            Max += v;
+        }
+
+        public float Height => Max.Y - Min.Y;
+        public float Width => Max.X - Min.X;
+        public float Depth => Max.Z - Min.Z;
+
+        public Vector3 Size => Max - Min;
+    }
+}
Index: /OniSplit/Math/BoundingSphere.cs
===================================================================
--- /OniSplit/Math/BoundingSphere.cs	(revision 1114)
+++ /OniSplit/Math/BoundingSphere.cs	(revision 1114)
@@ -0,0 +1,63 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni
+{
+    internal struct BoundingSphere : IEquatable<BoundingSphere>
+    {
+        public Vector3 Center;
+        public float Radius;
+
+        public BoundingSphere(Vector3 center, float radius)
+        {
+            Center = center;
+            Radius = radius;
+        }
+
+        public static BoundingSphere CreateFromBoundingBox(BoundingBox bbox)
+        {
+            BoundingSphere r;
+            r.Center = (bbox.Min + bbox.Max) * 0.5f;
+            r.Radius = Vector3.Distance(r.Center, bbox.Min);
+            return r;
+        }
+
+        public static BoundingSphere CreateFromPoints(IEnumerable<Vector3> points)
+        {
+            var center = Vector3.Zero;
+            int count = 0;
+
+            foreach (var point in points)
+            {
+                center += point;
+                count++;
+            }
+
+            center /= count;
+
+            float radius = 0.0f;
+
+            foreach (var point in points)
+            {
+                float distance = Vector3.DistanceSquared(center, point);
+
+                if (distance > radius)
+                    radius = distance;
+            }
+
+            radius = FMath.Sqrt(radius);
+
+            return new BoundingSphere(center, radius);
+        }
+
+        public static bool operator ==(BoundingSphere s1, BoundingSphere s2) => s1.Radius == s2.Radius && s1.Center == s2.Center;
+        public static bool operator !=(BoundingSphere s1, BoundingSphere s2) => s1.Radius != s2.Radius || s1.Center != s2.Center;
+
+        public bool Equals(BoundingSphere other) => other.Radius == Radius && other.Center == Center;
+
+        public override bool Equals(object obj) => obj is BoundingSphere && Equals((BoundingSphere)obj);
+        public override int GetHashCode() => Radius.GetHashCode() ^ Center.GetHashCode();
+
+        public override string ToString() => $"{{{Center} {Radius}}}";
+    }
+}
Index: /OniSplit/Math/FMath.cs
===================================================================
--- /OniSplit/Math/FMath.cs	(revision 1114)
+++ /OniSplit/Math/FMath.cs	(revision 1114)
@@ -0,0 +1,27 @@
+﻿using System;
+
+namespace Oni
+{
+    internal static class FMath
+    {
+        public static float Sign(float x)
+        {
+            if (x > 0.0f)
+                return 1.0f;
+            else if (x < 0.0f)
+                return -1.0f;
+            else
+                return 0.0f;
+        }
+
+        public static float Sqrt(float x) => (float)Math.Sqrt(x);
+        public static float Sqr(float x) => x * x;
+        public static float Atan2(float y, float x) => (float)Math.Atan2(y, x);
+        public static float Cos(float x) => (float)Math.Cos(x);
+        public static float Sin(float x) => (float)Math.Sin(x);
+        public static float Acos(float x) => (float)Math.Acos(x);
+        public static float Round(float x, int digits) => (float)Math.Round(x, digits);
+        public static int RoundToInt32(float f) => (int)Math.Round(f);
+        public static int TruncateToInt32(float f) => (int)Math.Truncate(f);
+    }
+}
Index: /OniSplit/Math/MathHelper.cs
===================================================================
--- /OniSplit/Math/MathHelper.cs	(revision 1114)
+++ /OniSplit/Math/MathHelper.cs	(revision 1114)
@@ -0,0 +1,59 @@
+﻿using System;
+
+namespace Oni
+{
+    internal static class MathHelper
+    {
+        public const float Eps = 1e-5f;
+        public const float Pi = 3.141593f;
+        public const float HalfPi = Pi / 2.0f;
+        public const float PiOver4 = Pi / 4.0f;
+        public const float TwoPi = 2.0f * Pi;
+
+        public static float ToDegrees(float radians) => radians * (180.0f / Pi);
+        public static float ToRadians(float degrees) => degrees * (Pi / 180.0f);
+        public static float Distance(float v1, float v2) => Math.Abs(v2 - v1);
+        public static float Lerp(float v1, float v2, float amount) => v1 + (v2 - v1) * amount;
+
+        public static int Lerp(int v1, int v2, float amount)
+        {
+            if (amount == 0.0f)
+                return v1;
+
+            if (amount == 1.0f)
+                return v2;
+
+            return (int)(v1 + (v2 - v1) * amount);
+        }
+
+        public static float Clamp(float v, float min, float max)
+        {
+            v = (v > max) ? max : v;
+            v = (v < min) ? min : v;
+
+            return v;
+        }
+
+        public static int Clamp(int v, int min, int max)
+        {
+            v = (v > max) ? max : v;
+            v = (v < min) ? min : v;
+
+            return v;
+        }
+
+        public static float Area(Vector2[] points)
+        {
+            float area = 0.0f;
+
+            for (int i = 0; i < points.Length; i++)
+            {
+                int j = (i + 1) % points.Length;
+                area += points[i].X * points[j].Y;
+                area -= points[i].Y * points[j].X;
+            }
+
+            return Math.Abs(area * 0.5f);
+        }
+    }
+}
Index: /OniSplit/Math/Matrix.cs
===================================================================
--- /OniSplit/Math/Matrix.cs	(revision 1114)
+++ /OniSplit/Math/Matrix.cs	(revision 1114)
@@ -0,0 +1,683 @@
+﻿using System;
+
+namespace Oni
+{
+    internal struct Matrix : IEquatable<Matrix>
+    {
+        public float M11, M12, M13, M14;
+        public float M21, M22, M23, M24;
+        public float M31, M32, M33, M34;
+        public float M41, M42, M43, M44;
+
+        public Matrix(float m11, float m12, float m13, float m14,
+            float m21, float m22, float m23, float m24,
+            float m31, float m32, float m33, float m34,
+            float m41, float m42, float m43, float m44)
+        {
+            M11 = m11; M12 = m12; M13 = m13; M14 = m14;
+            M21 = m21; M22 = m22; M23 = m23; M24 = m24;
+            M31 = m31; M32 = m32; M33 = m33; M34 = m34;
+            M41 = m41; M42 = m42; M43 = m43; M44 = m44;
+        }
+
+        public Matrix(float[] values)
+        {
+            M11 = values[0]; M12 = values[4]; M13 = values[8]; M14 = values[12];
+            M21 = values[1]; M22 = values[5]; M23 = values[9]; M24 = values[13];
+            M31 = values[2]; M32 = values[6]; M33 = values[10]; M34 = values[14];
+            M41 = values[3]; M42 = values[7]; M43 = values[11]; M44 = values[15];
+        }
+
+        public void CopyTo(float[] values)
+        {
+            values[0] = M11;
+            values[1] = M21;
+            values[2] = M31;
+            values[3] = M41;
+
+            values[4] = M12;
+            values[5] = M22;
+            values[6] = M32;
+            values[7] = M42;
+
+            values[8] = M13;
+            values[9] = M23;
+            values[10] = M33;
+            values[11] = M43;
+
+            values[12] = M14;
+            values[13] = M24;
+            values[14] = M34;
+            values[15] = M44;
+        }
+
+        public static Matrix CreateTranslation(float x, float y, float z)
+        {
+            Matrix r = Identity;
+
+            r.M41 = x;
+            r.M42 = y;
+            r.M43 = z;
+
+            return r;
+        }
+
+        public static Matrix CreateTranslation(Vector3 v) => CreateTranslation(v.X, v.Y, v.Z);
+
+        public static Matrix CreateScale(float sx, float sy, float sz)
+        {
+            Matrix r = Identity;
+
+            r.M11 = sx;
+            r.M22 = sy;
+            r.M33 = sz;
+
+            return r;
+        }
+
+        public static Matrix CreateScale(float s) => CreateScale(s, s, s);
+        public static Matrix CreateScale(Vector3 s) => CreateScale(s.X, s.Y, s.Z);
+
+        public static Matrix CreateRotationX(float angle)
+        {
+            float cos = FMath.Cos(angle);
+            float sin = FMath.Sin(angle);
+
+            Matrix r = Identity;
+            r.M22 = cos;
+            r.M23 = sin;
+            r.M32 = -sin;
+            r.M33 = cos;
+            return r;
+        }
+
+        public static Matrix CreateRotationY(float angle)
+        {
+            float cos = FMath.Cos(angle);
+            float sin = FMath.Sin(angle);
+
+            Matrix r = Identity;
+            r.M11 = cos;
+            r.M13 = -sin;
+            r.M31 = sin;
+            r.M33 = cos;
+            return r;
+        }
+
+        public static Matrix CreateRotationZ(float angle)
+        {
+            float cos = FMath.Cos(angle);
+            float sin = FMath.Sin(angle);
+
+            Matrix r = Identity;
+            r.M11 = cos;
+            r.M12 = sin;
+            r.M21 = -sin;
+            r.M22 = cos;
+            return r;
+        }
+
+        public static Matrix CreateFromAxisAngle(Vector3 axis, float angle)
+        {
+            float sin = FMath.Sin(angle);
+            float cos = FMath.Cos(angle);
+
+            float x = axis.X;
+            float y = axis.Y;
+            float z = axis.Z;
+            float xx = x * x;
+            float yy = y * y;
+            float zz = z * z;
+            float xy = x * y;
+            float xz = x * z;
+            float yz = y * z;
+
+            Matrix r = Identity;
+            r.M11 = xx + (cos * (1.0f - xx));
+            r.M12 = (xy - (cos * xy)) + (sin * z);
+            r.M13 = (xz - (cos * xz)) - (sin * y);
+            r.M21 = (xy - (cos * xy)) - (sin * z);
+            r.M22 = yy + (cos * (1.0f - yy));
+            r.M23 = (yz - (cos * yz)) + (sin * x);
+            r.M31 = (xz - (cos * xz)) + (sin * y);
+            r.M32 = (yz - (cos * yz)) - (sin * x);
+            r.M33 = zz + (cos * (1.0f - zz));
+            return r;
+        }
+
+        public static Matrix CreateFromQuaternion(Quaternion q)
+        {
+            float xx = q.X * q.X;
+            float yy = q.Y * q.Y;
+            float zz = q.Z * q.Z;
+            float xy = q.X * q.Y;
+            float zw = q.Z * q.W;
+            float zx = q.Z * q.X;
+            float yw = q.Y * q.W;
+            float yz = q.Y * q.Z;
+            float xw = q.X * q.W;
+
+            Matrix r = Identity;
+
+            r.M11 = 1.0f - 2.0f * (yy + zz);
+            r.M12 = 2.0f * (xy + zw);
+            r.M13 = 2.0f * (zx - yw);
+
+            r.M21 = 2.0f * (xy - zw);
+            r.M22 = 1.0f - 2.0f * (zz + xx);
+            r.M23 = 2.0f * (yz + xw);
+
+            r.M31 = 2.0f * (zx + yw);
+            r.M32 = 2.0f * (yz - xw);
+            r.M33 = 1.0f - 2.0f * (yy + xx);
+
+            return r;
+        }
+
+        public static Matrix operator +(Matrix m1, Matrix m2)
+        {
+            m1.M11 += m2.M11;
+            m1.M12 += m2.M12;
+            m1.M13 += m2.M13;
+            m1.M14 += m2.M14;
+            m1.M21 += m2.M21;
+            m1.M22 += m2.M22;
+            m1.M23 += m2.M23;
+            m1.M24 += m2.M24;
+            m1.M31 += m2.M31;
+            m1.M32 += m2.M32;
+            m1.M33 += m2.M33;
+            m1.M34 += m2.M34;
+            m1.M41 += m2.M41;
+            m1.M42 += m2.M42;
+            m1.M43 += m2.M43;
+            m1.M44 += m2.M44;
+
+            return m1;
+        }
+
+        public static Matrix operator -(Matrix m1, Matrix m2)
+        {
+            m1.M11 -= m2.M11;
+            m1.M12 -= m2.M12;
+            m1.M13 -= m2.M13;
+            m1.M14 -= m2.M14;
+            m1.M21 -= m2.M21;
+            m1.M22 -= m2.M22;
+            m1.M23 -= m2.M23;
+            m1.M24 -= m2.M24;
+            m1.M31 -= m2.M31;
+            m1.M32 -= m2.M32;
+            m1.M33 -= m2.M33;
+            m1.M34 -= m2.M34;
+            m1.M41 -= m2.M41;
+            m1.M42 -= m2.M42;
+            m1.M43 -= m2.M43;
+            m1.M44 -= m2.M44;
+
+            return m1;
+        }
+
+        public static Matrix operator *(Matrix m, float s)
+        {
+            m.M11 *= s;
+            m.M12 *= s;
+            m.M13 *= s;
+            m.M14 *= s;
+            m.M21 *= s;
+            m.M22 *= s;
+            m.M23 *= s;
+            m.M24 *= s;
+            m.M31 *= s;
+            m.M32 *= s;
+            m.M33 *= s;
+            m.M34 *= s;
+            m.M41 *= s;
+            m.M42 *= s;
+            m.M43 *= s;
+            m.M44 *= s;
+
+            return m;
+        }
+
+        public static Matrix operator *(float s, Matrix m) => m * s;
+
+        public static Matrix operator /(Matrix m, float s) => m * (1.0f / s);
+
+        public static Matrix operator *(Matrix m1, Matrix m2)
+        {
+            Matrix r;
+
+            r.M11 = m1.M11 * m2.M11 + m1.M12 * m2.M21 + m1.M13 * m2.M31 + m1.M14 * m2.M41;
+            r.M12 = m1.M11 * m2.M12 + m1.M12 * m2.M22 + m1.M13 * m2.M32 + m1.M14 * m2.M42;
+            r.M13 = m1.M11 * m2.M13 + m1.M12 * m2.M23 + m1.M13 * m2.M33 + m1.M14 * m2.M43;
+            r.M14 = m1.M11 * m2.M14 + m1.M12 * m2.M24 + m1.M13 * m2.M34 + m1.M14 * m2.M44;
+            r.M21 = m1.M21 * m2.M11 + m1.M22 * m2.M21 + m1.M23 * m2.M31 + m1.M24 * m2.M41;
+            r.M22 = m1.M21 * m2.M12 + m1.M22 * m2.M22 + m1.M23 * m2.M32 + m1.M24 * m2.M42;
+            r.M23 = m1.M21 * m2.M13 + m1.M22 * m2.M23 + m1.M23 * m2.M33 + m1.M24 * m2.M43;
+            r.M24 = m1.M21 * m2.M14 + m1.M22 * m2.M24 + m1.M23 * m2.M34 + m1.M24 * m2.M44;
+            r.M31 = m1.M31 * m2.M11 + m1.M32 * m2.M21 + m1.M33 * m2.M31 + m1.M34 * m2.M41;
+            r.M32 = m1.M31 * m2.M12 + m1.M32 * m2.M22 + m1.M33 * m2.M32 + m1.M34 * m2.M42;
+            r.M33 = m1.M31 * m2.M13 + m1.M32 * m2.M23 + m1.M33 * m2.M33 + m1.M34 * m2.M43;
+            r.M34 = m1.M31 * m2.M14 + m1.M32 * m2.M24 + m1.M33 * m2.M34 + m1.M34 * m2.M44;
+            r.M41 = m1.M41 * m2.M11 + m1.M42 * m2.M21 + m1.M43 * m2.M31 + m1.M44 * m2.M41;
+            r.M42 = m1.M41 * m2.M12 + m1.M42 * m2.M22 + m1.M43 * m2.M32 + m1.M44 * m2.M42;
+            r.M43 = m1.M41 * m2.M13 + m1.M42 * m2.M23 + m1.M43 * m2.M33 + m1.M44 * m2.M43;
+            r.M44 = m1.M41 * m2.M14 + m1.M42 * m2.M24 + m1.M43 * m2.M34 + m1.M44 * m2.M44;
+
+            return r;
+        }
+
+        public Matrix Transpose()
+        {
+            Matrix t;
+
+            t.M11 = M11;
+            t.M12 = M21;
+            t.M13 = M31;
+            t.M14 = M41;
+            t.M21 = M12;
+            t.M22 = M22;
+            t.M23 = M32;
+            t.M24 = M42;
+            t.M31 = M13;
+            t.M32 = M23;
+            t.M33 = M33;
+            t.M34 = M43;
+            t.M41 = M14;
+            t.M42 = M24;
+            t.M43 = M34;
+            t.M44 = M44;
+
+            return t;
+        }
+
+        public static bool operator ==(Matrix m1, Matrix m2) => m1.Equals(m2);
+        public static bool operator !=(Matrix m1, Matrix m2) => !m1.Equals(m2);
+
+        public Vector3 XAxis
+        {
+            get
+            {
+                return new Vector3(M11, M12, M13);
+            }
+            set
+            {
+                M11 = value.X;
+                M12 = value.Y;
+                M13 = value.Z;
+            }
+        }
+
+        public Vector3 YAxis
+        {
+            get
+            {
+                return new Vector3(M21, M22, M23);
+            }
+            set
+            {
+                M21 = value.X;
+                M22 = value.Y;
+                M23 = value.Z;
+            }
+        }
+
+        public Vector3 ZAxis
+        {
+            get
+            {
+                return new Vector3(M31, M32, M33);
+            }
+            set
+            {
+                M31 = value.X;
+                M32 = value.Y;
+                M33 = value.Z;
+            }
+        }
+
+        public Vector3 Scale
+        {
+            get
+            {
+                return new Vector3(M11, M22, M33);
+            }
+            set
+            {
+                M11 = value.X;
+                M22 = value.Y;
+                M33 = value.Z;
+            }
+        }
+
+        public Vector3 Translation
+        {
+            get
+            {
+                return new Vector3(M41, M42, M43);
+            }
+            set
+            {
+                M41 = value.X;
+                M42 = value.Y;
+                M43 = value.Z;
+            }
+        }
+
+        public bool Equals(Matrix other)
+        {
+            return (M11 == other.M11 && M12 == other.M12 && M13 == other.M13 && M14 == other.M14
+                && M21 == other.M21 && M22 == other.M22 && M23 == other.M23 && M24 == other.M24
+                && M31 == other.M31 && M32 == other.M32 && M33 == other.M33 && M34 == other.M34
+                && M41 == other.M41 && M42 == other.M42 && M43 == other.M43 && M44 == other.M44);
+        }
+
+        public override bool Equals(object obj) => obj is Matrix && Equals((Matrix)obj);
+
+        public override int GetHashCode()
+        {
+            return M11.GetHashCode() ^ M12.GetHashCode() ^ M13.GetHashCode() ^ M14.GetHashCode()
+                 ^ M11.GetHashCode() ^ M12.GetHashCode() ^ M13.GetHashCode() ^ M14.GetHashCode()
+                 ^ M11.GetHashCode() ^ M12.GetHashCode() ^ M13.GetHashCode() ^ M14.GetHashCode()
+                 ^ M11.GetHashCode() ^ M12.GetHashCode() ^ M13.GetHashCode() ^ M14.GetHashCode();
+        }
+
+        public override string ToString()
+        {
+            return string.Format("{{M11:{0} M12:{1} M13:{2} M14:{3}}}\n{{M21:{4} M22:{5} M23:{6} M24:{7}}}\n{{M31:{8} M32:{9} M33:{10} M34:{11}}}\n{{M41:{12} M42:{13} M43:{14} M44:{15}}}",
+                M11, M12, M13, M14,
+                M21, M22, M23, M24,
+                M31, M32, M33, M34,
+                M41, M42, M43, M44);
+        }
+
+        private static readonly Matrix identity = new Matrix(
+            1.0f, 0.0f, 0.0f, 0.0f,
+            0.0f, 1.0f, 0.0f, 0.0f,
+            0.0f, 0.0f, 1.0f, 0.0f,
+            0.0f, 0.0f, 0.0f, 1.0f);
+
+        public static Matrix Identity => identity;
+
+        public Vector3 ToEuler()
+        {
+            float a = M11;
+            float b = M21;
+            float c, s, r;
+
+            if (b == 0.0f)
+            {
+                c = FMath.Sign(a);
+                s = 0.0f;
+                r = Math.Abs(a);
+            }
+            else if (a == 0.0f)
+            {
+                c = 0.0f;
+                s = FMath.Sign(b);
+                r = Math.Abs(b);
+            }
+            else if (Math.Abs(b) > Math.Abs(a))
+            {
+                float t = a / b;
+                float u = FMath.Sign(b) * FMath.Sqrt(1.0f + t * t);
+                s = 1.0f / u;
+                c = s * t;
+                r = b * u;
+            }
+            else
+            {
+                float t = b / a;
+                float u = FMath.Sign(a) * FMath.Sqrt(1.0f + t * t);
+                c = 1.0f / u;
+                s = c * t;
+                r = a * u;
+            }
+
+            Vector3 e;
+            e.Z = MathHelper.ToDegrees(-FMath.Atan2(s, c));
+            e.Y = MathHelper.ToDegrees(FMath.Atan2(M31, r));
+            e.X = MathHelper.ToDegrees(-FMath.Atan2(M32, M33));
+            return e;
+        }
+
+        public float Determinant()
+        {
+            var m11 = M11;
+            var m12 = M12;
+            var m13 = M13;
+            var m14 = M14;
+            var m21 = M21;
+            var m22 = M22;
+            var m23 = M23;
+            var m24 = M24;
+            var m31 = M31;
+            var m32 = M32;
+            var m33 = M33;
+            var m34 = M34;
+            var m41 = M41;
+            var m42 = M42;
+            var m43 = M43;
+            var m44 = M44;
+
+            var d3434 = m33 * m44 - m34 * m43;
+            var d3424 = m32 * m44 - m34 * m42;
+            var d3423 = m32 * m43 - m33 * m42;
+            var d3414 = m31 * m44 - m34 * m41;
+            var d3413 = m31 * m43 - m33 * m41;
+            var d3412 = m31 * m42 - m32 * m41;
+
+            return m11 * (m22 * d3434 - m23 * d3424 + m24 * d3423)
+                 - m12 * (m21 * d3434 - m23 * d3414 + m24 * d3413)
+                 + m13 * (m21 * d3424 - m22 * d3414 + m24 * d3412)
+                 - m14 * (m21 * d3423 - m22 * d3413 + m23 * d3412);
+        }
+
+        //Matrix m = Matrix.Identity;
+        //m *= Matrix.CreateScale(3.3f, 1.3f, 7.6f);
+        //m *= Matrix.CreateTranslation(2.3f, 4.5f, 6.7f);
+        //m *= Matrix.CreateRotationY(1.2f);
+        //m *= Matrix.CreateTranslation(2.3f, 4.5f, 6.7f);
+        //m *= Matrix.CreateRotationY(1.2f);
+        //m *= Matrix.CreateTranslation(2.3f, 4.5f, 6.7f);
+        //m *= Matrix.CreateRotationY(1.2f);
+
+        //Vector3 s, t;
+        //Quaternion r;
+        //m.Decompose(out s, out r, out t);
+        //Matrix m2 = Matrix.CreateScale(s) * Matrix.CreateFromQuaternion(r) * Matrix.CreateTranslation(t);
+
+        //Console.WriteLine(m2 - m);
+        //return 0;
+
+        //[StructLayout(LayoutKind.Sequential)]
+        //private unsafe struct VectorBasis
+        //{
+        //    public Vector3* axis0;
+        //    public Vector3* axis1;
+        //    public Vector3* axis2;
+        //}
+
+        //[StructLayout(LayoutKind.Sequential)]
+        //private struct CanonicalBasis
+        //{
+        //    public Vector3 axis0;
+        //    public Vector3 axis1;
+        //    public Vector3 axis2;
+        //}
+
+        //public unsafe bool Decompose(out Vector3 outScale, out Quaternion outRotation, out Vector3 outTranslation)
+        //{
+        //    outTranslation.X = M41;
+        //    outTranslation.Y = M42;
+        //    outTranslation.Z = M43;
+
+        //    var rotation = new Matrix(
+        //        M11, M12, M13, 0.0f, 
+        //        M21, M22, M23, 0.0f, 
+        //        M31, M32, M33, 0.0f, 
+        //        0.0f, 0.0f, 0.0f, 1.0f);
+
+        //    var vectorBasis = new VectorBasis {
+        //        axis0 = (Vector3*)&rotation.M11,
+        //        axis1 = (Vector3*)&rotation.M21,
+        //        axis2 = (Vector3*)&rotation.M31
+        //    };
+
+        //    var canonicalBasis = new CanonicalBasis {
+        //        axis0 = Vector3.UnitX,
+        //        axis1 = Vector3.UnitY,
+        //        axis2 = Vector3.UnitZ
+        //    };
+
+        //    var scale = new Vector3(
+        //        vectorBasis.axis0->Length(),
+        //        vectorBasis.axis1->Length(),
+        //        vectorBasis.axis2->Length()
+        //    );
+
+        //    int xi, yi, zi;
+
+        //    if (scale.X < scale.Y)
+        //    {
+        //        if (scale.Y < scale.Z)
+        //        {
+        //            xi = 2;
+        //            yi = 1;
+        //            zi = 0;
+        //        }
+        //        else
+        //        {
+        //            xi = 1;
+
+        //            if (scale.X < scale.Z)
+        //            {
+        //                yi = 2;
+        //                zi = 0;
+        //            }
+        //            else
+        //            {
+        //                yi = 0;
+        //                zi = 2;
+        //            }
+        //        }
+        //    }
+        //    else
+        //    {
+        //        if (scale.X < scale.Z)
+        //        {
+        //            xi = 2;
+        //            yi = 0;
+        //            zi = 1;
+        //        }
+        //        else
+        //        {
+        //            xi = 0;
+
+        //            if (scale.Y < scale.Z)
+        //            {
+        //                yi = 2;
+        //                zi = 1;
+        //            }
+        //            else
+        //            {
+        //                yi = 1;
+        //                zi = 2;
+        //            }
+        //        }
+        //    }
+
+        //    var pScale = &scale.X;
+
+        //    var pvBasis = &vectorBasis.axis0;
+        //    var pcBasis = &canonicalBasis.axis0;
+
+        //    if (pScale[xi] < 0.0001f)
+        //    {
+        //        //
+        //        // If the smallest scale is < 0.0001 then use the coresponding cannonical basis instead
+        //        //
+
+        //        pvBasis[xi] = &pcBasis[xi];
+        //    }
+        //    else
+        //    {
+        //        pvBasis[xi]->Normalize();
+        //    }
+
+        //    if (pScale[yi] < 0.0001f)
+        //    {
+        //        //
+        //        // The second smallest scale is < 0.0001 too, build a perpendicular vector
+        //        //
+
+        //        float fx = Math.Abs(pvBasis[xi]->X);
+        //        float fy = Math.Abs(pvBasis[xi]->Y);
+        //        float fz = Math.Abs(pvBasis[xi]->Z);
+
+        //        int yij;
+
+        //        if (fx < fy)
+        //        {
+        //            if (fy < fz)
+        //            {
+        //                yij = 0;
+        //            }
+        //            else
+        //            {
+        //                if (fx < fz)
+        //                    yij = 0;
+        //                else
+        //                    yij = 2;
+        //            }
+        //        }
+        //        else
+        //        {
+        //            if (fx < fz)
+        //            {
+        //                yij = 1;
+        //            }
+        //            else
+        //            {
+        //                if (fy < fz)
+        //                    yij = 1;
+        //                else
+        //                    yij = 2;
+        //            }
+        //        }
+
+        //        pcBasis[yij] = Vector3.Cross(*pvBasis[yi], *pvBasis[xi]);
+        //    }
+
+        //    pvBasis[yi]->Normalize();
+
+        //    if (pScale[zi] < 0.0001f)
+        //        *(pvBasis[zi]) = Vector3.Cross(*pvBasis[yi], *pvBasis[xi]);
+        //    else
+        //        pvBasis[zi]->Normalize();
+
+        //    float rotDet = rotation.Determinant();
+
+        //    if (rotDet < 0.0f)
+        //    {
+        //        pScale[xi] = -pScale[xi];
+        //        *(pvBasis[xi]) = -(*(pvBasis[xi]));
+        //        rotDet = -rotDet;
+        //    }
+
+        //    outScale = scale;
+
+        //    if (Math.Abs(rotDet - 1.0f) > 0.01f)
+        //    {
+        //        outRotation = Quaternion.Identity;
+        //        return false;
+        //    }
+        //    else
+        //    {
+        //        outRotation = Quaternion.CreateFromRotationMatrix(rotation);
+        //        return true;
+        //    }
+        //}
+    }
+}
Index: /OniSplit/Math/Plane.cs
===================================================================
--- /OniSplit/Math/Plane.cs	(revision 1114)
+++ /OniSplit/Math/Plane.cs	(revision 1114)
@@ -0,0 +1,95 @@
+﻿using System;
+
+namespace Oni
+{
+    internal struct Plane : IEquatable<Plane>
+    {
+        public Vector3 Normal;
+        public float D;
+
+        public Plane(Vector3 normal, float d)
+        {
+            Normal = normal;
+            D = d;
+        }
+
+        public Plane(Vector3 point1, Vector3 point2, Vector3 point3)
+        {
+            Normal = Vector3.Normalize(Vector3.Cross(point2 - point1, point3 - point1));
+            D = -Vector3.Dot(Normal, point1);
+        }
+
+        public float DotCoordinate(Vector3 point) => Vector3.Dot(Normal, point) + D;
+
+        public float DotNormal(Vector3 value) => Vector3.Dot(Normal, value);
+
+        public void Flip()
+        {
+            Normal = -Normal;
+            D = -D;
+        }
+
+        public static Plane Flip(Plane plane)
+        {
+            plane.Normal = -plane.Normal;
+            plane.D = -plane.D;
+
+            return plane;
+        }
+
+        public static bool operator ==(Plane p1, Plane p2) => p1.D == p2.D && p1.Normal == p2.Normal;
+        public static bool operator !=(Plane p1, Plane p2) => p1.D != p2.D || p1.Normal != p2.Normal;
+
+        public bool Equals(Plane other) => other.D == D && other.Normal == Normal;
+
+        public override bool Equals(object obj) => obj is Plane && Equals((Plane)obj);
+        public override int GetHashCode() => Normal.GetHashCode() ^ D.GetHashCode();
+
+        public override string ToString() => $"{{Normal:{Normal} D:{D}}}";
+
+        public int Intersects(BoundingBox box)
+        {
+            Vector3 max, min;
+
+            if (Normal.X >= 0.0f)
+            {
+                min.X = box.Min.X;
+                max.X = box.Max.X;
+            }
+            else
+            {
+                min.X = box.Max.X;
+                max.X = box.Min.X;
+            }
+
+            if (Normal.Y >= 0.0f)
+            {
+                min.Y = box.Min.Y;
+                max.Y = box.Max.Y;
+            }
+            else
+            {
+                min.Y = box.Max.Y;
+                max.Y = box.Min.Y;
+            }
+
+            if (Normal.Z >= 0.0f)
+            {
+                min.Z = box.Min.Z;
+                max.Z = box.Max.Z;
+            }
+            else
+            {
+                min.Z = box.Max.Z;
+                max.Z = box.Min.Z;
+            }
+
+            if (Vector3.Dot(Normal, min) + D > 0.0f)
+                return 1;
+            else if (Vector3.Dot(Normal, max) + D < 0.0f)
+                return -1;
+            else
+                return 0;
+        }
+    }
+}
Index: /OniSplit/Math/Polygon2.cs
===================================================================
--- /OniSplit/Math/Polygon2.cs	(revision 1114)
+++ /OniSplit/Math/Polygon2.cs	(revision 1114)
@@ -0,0 +1,16 @@
+﻿namespace Oni
+{
+    internal struct Polygon2
+    {
+        private readonly Vector2[] points;
+
+        public Polygon2(Vector2[] points)
+        {
+            this.points = points;
+        }
+
+        public int Length => points.Length;
+
+        public Vector2 this[int index] => points[index];
+    }
+}
Index: /OniSplit/Math/Polygon3.cs
===================================================================
--- /OniSplit/Math/Polygon3.cs	(revision 1114)
+++ /OniSplit/Math/Polygon3.cs	(revision 1114)
@@ -0,0 +1,16 @@
+﻿namespace Oni
+{
+    internal struct Polygon3
+    {
+        private readonly Vector3[] points;
+
+        public Polygon3(Vector3[] points)
+        {
+            this.points = points;
+        }
+
+        public int Length => points.Length;
+
+        public Vector3 this[int index] => points[index];
+    }
+}
Index: /OniSplit/Math/Quaternion.cs
===================================================================
--- /OniSplit/Math/Quaternion.cs	(revision 1114)
+++ /OniSplit/Math/Quaternion.cs	(revision 1114)
@@ -0,0 +1,326 @@
+﻿using System;
+
+namespace Oni
+{
+    internal struct Quaternion : IEquatable<Quaternion>
+    {
+        public float X;
+        public float Y;
+        public float Z;
+        public float W;
+
+        public Quaternion(Vector3 xyz, float w)
+        {
+            X = xyz.X;
+            Y = xyz.Y;
+            Z = xyz.Z;
+            W = w;
+        }
+
+        public Quaternion(float x, float y, float z, float w)
+        {
+            X = x;
+            Y = y;
+            Z = z;
+            W = w;
+        }
+
+        public Quaternion(Vector4 xyzw)
+        {
+            X = xyzw.X;
+            Y = xyzw.Y;
+            Z = xyzw.Z;
+            W = xyzw.W;
+        }
+
+        private Vector3 XYZ => new Vector3(X, Y, Z);
+
+        public static Quaternion CreateFromAxisAngle(Vector3 axis, float angle)
+        {
+            float halfAngle = angle * 0.5f;
+            float sin = FMath.Sin(halfAngle);
+            float cos = FMath.Cos(halfAngle);
+
+            return new Quaternion(axis * sin, cos);
+        }
+
+        public void ToAxisAngle(out Vector3 axis, out float angle)
+        {
+            float halfAngle = FMath.Acos(W);
+            float sin = FMath.Sqrt(1 - W * W);
+
+            if (sin < 1e-5f)
+            {
+                axis = XYZ;
+                angle = 0.0f;
+            }
+            else
+            {
+                axis = XYZ / sin;
+                angle = halfAngle * 2.0f;
+            }
+        }
+
+        public static Quaternion CreateFromEulerXYZ(float x, float y, float z)
+        {
+            x = MathHelper.ToRadians(x);
+            y = MathHelper.ToRadians(y);
+            z = MathHelper.ToRadians(z);
+
+            return CreateFromAxisAngle(Vector3.UnitX, x)
+                 * CreateFromAxisAngle(Vector3.UnitY, y)
+                 * CreateFromAxisAngle(Vector3.UnitZ, z);
+        }
+
+        public Vector3 ToEulerXYZ()
+        {
+            Vector3 r;
+
+            var p0 = -W;
+            var p1 = X;
+            var p2 = Y;
+            var p3 = Z;
+            var e = -1.0f;
+
+            var s = 2.0f * (p0 * p2 + e * p1 * p3);
+
+            if (s > 0.999f)
+            {
+                r.X = MathHelper.ToDegrees(-2.0f * (float)Math.Atan2(p1, p0));
+                r.Y = -90.0f;
+                r.Z = 0.0f;
+            }
+            else if (s < -0.999f)
+            {
+                r.X = MathHelper.ToDegrees(2.0f * (float)Math.Atan2(p1, p0));
+                r.Y = 90.0f;
+                r.Z = 0.0f;
+            }
+            else
+            {
+                r.X = -MathHelper.ToDegrees((float)Math.Atan2(2.0f * (p0 * p1 - e * p2 * p3), 1.0f - 2.0f * (p1 * p1 + p2 * p2)));
+                r.Y = -MathHelper.ToDegrees((float)Math.Asin(s));
+                r.Z = -MathHelper.ToDegrees((float)Math.Atan2(2.0f * (p0 * p3 - e * p1 * p2), 1.0f - 2.0f * (p2 * p2 + p3 * p3)));
+            }
+
+            return r;
+        }
+
+        public static Quaternion CreateFromYawPitchRoll(float yaw, float pitch, float roll)
+        {
+            float halfRoll = roll * 0.5f;
+            float sinRoll = FMath.Sin(halfRoll);
+            float cosRoll = FMath.Cos(halfRoll);
+
+            float halfPitch = pitch * 0.5f;
+            float sinPitch = FMath.Sin(halfPitch);
+            float cosPitch = FMath.Cos(halfPitch);
+
+            float halfYaw = yaw * 0.5f;
+            float sinYaw = FMath.Sin(halfYaw);
+            float cosYaw = FMath.Cos(halfYaw);
+
+            Quaternion r;
+
+            r.X = (cosYaw * sinPitch * cosRoll) + (sinYaw * cosPitch * sinRoll);
+            r.Y = (sinYaw * cosPitch * cosRoll) - (cosYaw * sinPitch * sinRoll);
+            r.Z = (cosYaw * cosPitch * sinRoll) - (sinYaw * sinPitch * cosRoll);
+            r.W = (cosYaw * cosPitch * cosRoll) + (sinYaw * sinPitch * sinRoll);
+
+            return r;
+        }
+
+        public static Quaternion CreateFromRotationMatrix(Matrix m)
+        {
+            Quaternion q;
+
+            float trace = m.M11 + m.M22 + m.M33;
+
+            if (trace > 0.0f)
+            {
+                float s = FMath.Sqrt(1.0f + trace);
+                float inv2s = 0.5f / s;
+                q.X = (m.M23 - m.M32) * inv2s;
+                q.Y = (m.M31 - m.M13) * inv2s;
+                q.Z = (m.M12 - m.M21) * inv2s;
+                q.W = s * 0.5f;
+            }
+            else if (m.M11 >= m.M22 && m.M11 >= m.M33)
+            {
+                float s = FMath.Sqrt(1.0f + m.M11 - m.M22 - m.M33);
+                float inv2s = 0.5f / s;
+                q.X = s * 0.5f;
+                q.Y = (m.M12 + m.M21) * inv2s;
+                q.Z = (m.M13 + m.M31) * inv2s;
+                q.W = (m.M23 - m.M32) * inv2s;
+            }
+            else if (m.M22 > m.M33)
+            {
+                float s = FMath.Sqrt(1.0f - m.M11 + m.M22 - m.M33);
+                float inv2s = 0.5f / s;
+                q.X = (m.M21 + m.M12) * inv2s;
+                q.Y = s * 0.5f;
+                q.Z = (m.M32 + m.M23) * inv2s;
+                q.W = (m.M31 - m.M13) * inv2s;
+            }
+            else
+            {
+                float s = FMath.Sqrt(1.0f - m.M11 - m.M22 + m.M33);
+                float inv2s = 0.5f / s;
+                q.X = (m.M31 + m.M13) * inv2s;
+                q.Y = (m.M32 + m.M23) * inv2s;
+                q.Z = s * 0.5f;
+                q.W = (m.M12 - m.M21) * inv2s;
+            }
+
+            return q;
+        }
+
+        public static Quaternion Lerp(Quaternion q1, Quaternion q2, float amount)
+        {
+            float invAmount = 1.0f - amount;
+
+            if (Dot(q1, q2) < 0.0f)
+                amount = -amount;
+
+            q1.X = invAmount * q1.X + amount * q2.X;
+            q1.Y = invAmount * q1.Y + amount * q2.Y;
+            q1.Z = invAmount * q1.Z + amount * q2.Z;
+            q1.W = invAmount * q1.W + amount * q2.W;
+
+            q1.Normalize();
+
+            return q1;
+        }
+
+        public static float Dot(Quaternion q1, Quaternion q2)
+            => q1.X * q2.X + q1.Y * q2.Y + q1.Z * q2.Z + q1.W * q2.W;
+
+        public static Quaternion operator +(Quaternion q1, Quaternion q2)
+        {
+            q1.X += q2.X;
+            q1.Y += q2.Y;
+            q1.Z += q2.Z;
+            q1.W += q2.W;
+
+            return q1;
+        }
+
+        public static Quaternion operator -(Quaternion q1, Quaternion q2)
+        {
+            q1.X -= q2.X;
+            q1.Y -= q2.Y;
+            q1.Z -= q2.Z;
+            q1.W -= q2.W;
+
+            return q1;
+        }
+
+        public static Quaternion operator *(Quaternion q1, Quaternion q2) => new Quaternion
+        {
+            X = q1.X * q2.W + q1.Y * q2.Z - q1.Z * q2.Y + q1.W * q2.X,
+            Y = -q1.X * q2.Z + q1.Y * q2.W + q1.Z * q2.X + q1.W * q2.Y,
+            Z = q1.X * q2.Y - q1.Y * q2.X + q1.Z * q2.W + q1.W * q2.Z,
+            W = -q1.X * q2.X - q1.Y * q2.Y - q1.Z * q2.Z + q1.W * q2.W,
+        };
+
+        public static Quaternion operator *(Quaternion q, float s)
+        {
+            q.X *= s;
+            q.Y *= s;
+            q.Z *= s;
+            q.W *= s;
+
+            return q;
+        }
+
+        public static bool operator ==(Quaternion q1, Quaternion q2) => q1.Equals(q2);
+        public static bool operator !=(Quaternion q1, Quaternion q2) => !q1.Equals(q2);
+
+        public static Quaternion Conjugate(Quaternion q)
+        {
+            q.X = -q.X;
+            q.Y = -q.Y;
+            q.Z = -q.Z;
+
+            return q;
+        }
+
+        public Quaternion Inverse()
+        {
+            float inv = 1.0f / SquaredLength();
+
+            Quaternion r;
+            r.X = -X * inv;
+            r.Y = -Y * inv;
+            r.Z = -Z * inv;
+            r.W = W * inv;
+            return r;
+        }
+
+        public void Normalize()
+        {
+            float f = 1.0f / Length();
+
+            X *= f;
+            Y *= f;
+            Z *= f;
+            W *= f;
+        }
+
+        public float Length() => FMath.Sqrt(SquaredLength());
+
+        public float SquaredLength() => X * X + Y * Y + Z * Z + W * W;
+
+        public bool Equals(Quaternion other) => X == other.X && Y == other.Y && Z == other.Z && W == other.W;
+
+        public override bool Equals(object obj) => obj is Quaternion && Equals((Quaternion)obj);
+
+        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode() ^ W.GetHashCode();
+
+        public override string ToString() => $"{{{X} {Y} {Z} {W}}}";
+
+        public Matrix ToMatrix()
+        {
+            float xx = X * X;
+            float yy = Y * Y;
+            float zz = Z * Z;
+            float xy = X * Y;
+            float zw = Z * W;
+            float zx = Z * X;
+            float yw = Y * W;
+            float yz = Y * Z;
+            float xw = X * W;
+
+            Matrix m;
+
+            m.M11 = 1.0f - 2.0f * (yy + zz);
+            m.M12 = 2.0f * (xy + zw);
+            m.M13 = 2.0f * (zx - yw);
+            m.M14 = 0.0f;
+
+            m.M21 = 2.0f * (xy - zw);
+            m.M22 = 1.0f - 2.0f * (zz + xx);
+            m.M23 = 2.0f * (yz + xw);
+            m.M24 = 0.0f;
+
+            m.M31 = 2.0f * (zx + yw);
+            m.M32 = 2.0f * (yz - xw);
+            m.M33 = 1.0f - 2.0f * (yy + xx);
+            m.M34 = 0.0f;
+
+            m.M41 = 0.0f;
+            m.M42 = 0.0f;
+            m.M43 = 0.0f;
+            m.M44 = 1.0f;
+
+            return m;
+        }
+
+        public Vector4 ToVector4() => new Vector4(X, Y, Z, W);
+
+        private static readonly Quaternion identity = new Quaternion(0.0f, 0.0f, 0.0f, 1.0f);
+
+        public static Quaternion Identity => identity;
+    }
+}
Index: /OniSplit/Math/Vector2.cs
===================================================================
--- /OniSplit/Math/Vector2.cs	(revision 1114)
+++ /OniSplit/Math/Vector2.cs	(revision 1114)
@@ -0,0 +1,96 @@
+﻿using System;
+
+namespace Oni
+{
+    internal struct Vector2 : IEquatable<Vector2>
+    {
+        public float X;
+        public float Y;
+
+        public Vector2(float x, float y)
+        {
+            X = x;
+            Y = y;
+        }
+
+        public Vector2(float all)
+        {
+            X = all;
+            Y = all;
+        }
+
+        public static Vector2 operator +(Vector2 v1, Vector2 v2) => new Vector2
+        {
+            X = v1.X + v2.X,
+            Y = v1.Y + v2.Y
+        };
+
+        public static Vector2 operator -(Vector2 v1, Vector2 v2) => new Vector2
+        {
+            X = v1.X - v2.X,
+            Y = v1.Y - v2.Y
+        };
+
+        public static float Dot(Vector2 v1, Vector2 v2) => v1.X * v2.X + v1.Y * v2.Y;
+
+        public static Vector2 Normalize(Vector2 v) => v * (1.0f / v.Length());
+
+        public void Normalize()
+        {
+            float f = 1.0f / Length();
+
+            X *= f;
+            Y *= f;
+        }
+
+        public float Length() => FMath.Sqrt(X * X + Y * Y);
+
+        public static Vector2 operator *(Vector2 v, float s)
+        {
+            v.X *= s;
+            v.Y *= s;
+
+            return v;
+        }
+
+        public static Vector2 operator *(float s, Vector2 v)
+        {
+            v.X *= s;
+            v.Y *= s;
+
+            return v;
+        }
+
+        public static Vector2 operator /(Vector2 v, float s) => v * (1.0f / s);
+
+        public static Vector2 Min(Vector2 v1, Vector2 v2) => new Vector2
+        {
+            X = (v1.X < v2.X) ? v1.X : v2.X,
+            Y = (v1.Y < v2.Y) ? v1.Y : v2.Y
+        };
+
+        public static Vector2 Max(Vector2 v1, Vector2 v2) => new Vector2
+        {
+            X = (v1.X > v2.X) ? v1.X : v2.X,
+            Y = (v1.Y > v2.Y) ? v1.Y : v2.Y
+        };
+
+        public static bool operator ==(Vector2 v1, Vector2 v2) => v1.X == v2.X && v1.Y == v2.Y;
+        public static bool operator !=(Vector2 v1, Vector2 v2) => v1.X != v2.X || v1.Y != v2.Y;
+        public bool Equals(Vector2 other) => X == other.X && Y == other.Y;
+        public override bool Equals(object obj) => obj is Vector2 && Equals((Vector2)obj);
+        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
+
+        public override string ToString() => $"{{{X} {Y}}}";
+
+        private static Vector2 zero = new Vector2(0.0f, 0.0f);
+        private static Vector2 one = new Vector2(1.0f, 1.0f);
+        private static Vector2 unitX = new Vector2(1.0f, 0.0f);
+        private static Vector2 unitY = new Vector2(0.0f, 1.0f);
+
+        public static Vector2 Zero => zero;
+        public static Vector2 One => one;
+        public static Vector2 UnitX => unitX;
+        public static Vector2 UnitY => unitY;
+    }
+}
Index: /OniSplit/Math/Vector3.cs
===================================================================
--- /OniSplit/Math/Vector3.cs	(revision 1114)
+++ /OniSplit/Math/Vector3.cs	(revision 1114)
@@ -0,0 +1,369 @@
+﻿using System;
+
+namespace Oni
+{
+    internal struct Vector3 : IEquatable<Vector3>
+    {
+        public float X;
+        public float Y;
+        public float Z;
+
+        public Vector3(float all)
+        {
+            X = all;
+            Y = all;
+            Z = all;
+        }
+
+        public Vector3(float x, float y, float z)
+        {
+            X = x;
+            Y = y;
+            Z = z;
+        }
+
+        public Vector3(float[] values, int index = 0)
+        {
+            int i = index * 3;
+
+            X = values[i + 0];
+            Y = values[i + 1];
+            Z = values[i + 2];
+        }
+
+        public void CopyTo(float[] values, int index = 0)
+        {
+            values[index + 0] = X;
+            values[index + 1] = Y;
+            values[index + 2] = Z;
+        }
+
+        public Vector2 XZ => new Vector2(X, Z);
+
+        public static Vector3 operator +(Vector3 v1, Vector3 v2)
+        {
+            v1.X += v2.X;
+            v1.Y += v2.Y;
+            v1.Z += v2.Z;
+
+            return v1;
+        }
+
+        public static Vector3 operator -(Vector3 v1, Vector3 v2)
+        {
+            v1.X -= v2.X;
+            v1.Y -= v2.Y;
+            v1.Z -= v2.Z;
+
+            return v1;
+        }
+
+        public static Vector3 operator -(Vector3 v)
+        {
+            v.X = -v.X;
+            v.Y = -v.Y;
+            v.Z = -v.Z;
+
+            return v;
+        }
+
+        public static Vector3 operator *(Vector3 v, float s)
+        {
+            v.X *= s;
+            v.Y *= s;
+            v.Z *= s;
+
+            return v;
+        }
+
+        public static Vector3 operator *(float s, Vector3 v)
+        {
+            v.X *= s;
+            v.Y *= s;
+            v.Z *= s;
+
+            return v;
+        }
+
+        public static Vector3 operator *(Vector3 v1, Vector3 v2) => new Vector3
+        {
+            X = v1.X * v2.X,
+            Y = v1.Y * v2.Y,
+            Z = v1.Z * v2.Z,
+        };
+
+        public static Vector3 operator /(Vector3 v, float s) => v * (1.0f / s);
+
+        public static Vector3 operator /(Vector3 v1, Vector3 v2) => new Vector3
+        {
+            X = v1.X /= v2.X,
+            Y = v1.Y /= v2.Y,
+            Z = v1.Z /= v2.Z
+        };
+
+        public static void Add(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
+        {
+            r.X = v1.X + v2.X;
+            r.Y = v1.Y + v2.Y;
+            r.Z = v1.Z + v2.Z;
+        }
+
+        public static void Substract(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
+        {
+            r.X = v1.X - v2.X;
+            r.Y = v1.Y - v2.Y;
+            r.Z = v1.Z - v2.Z;
+        }
+
+        public static void Multiply(ref Vector3 v, float f, out Vector3 r)
+        {
+            r.X = v.X * f;
+            r.Y = v.Y * f;
+            r.Z = v.Z * f;
+        }
+
+        public void Scale(float scale)
+        {
+            X *= scale;
+            Y *= scale;
+            Z *= scale;
+        }
+
+        public static Vector3 Clamp(Vector3 v, Vector3 min, Vector3 max)
+        {
+            Vector3 r;
+
+            float x = v.X;
+            x = (x > max.X) ? max.X : x;
+            x = (x < min.X) ? min.X : x;
+
+            float y = v.Y;
+            y = (y > max.Y) ? max.Y : y;
+            y = (y < min.Y) ? min.Y : y;
+
+            float z = v.Z;
+            z = (z > max.Z) ? max.Z : z;
+            z = (z < min.Z) ? min.Z : z;
+
+            r.X = x;
+            r.Y = y;
+            r.Z = z;
+
+            return r;
+        }
+
+        public static Vector3 Cross(Vector3 v1, Vector3 v2)
+        {
+            return new Vector3(
+                v1.Y * v2.Z - v1.Z * v2.Y,
+                v1.Z * v2.X - v1.X * v2.Z,
+                v1.X * v2.Y - v1.Y * v2.X);
+        }
+
+        public static void Cross(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
+        {
+            r = new Vector3(
+                v1.Y * v2.Z - v1.Z * v2.Y,
+                v1.Z * v2.X - v1.X * v2.Z,
+                v1.X * v2.Y - v1.Y * v2.X);
+        }
+
+        public static float Dot(Vector3 v1, Vector3 v2)
+        {
+            return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z;
+        }
+
+        public static float Dot(ref Vector3 v1, ref Vector3 v2) => v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z;
+
+        public float Dot(ref Vector3 v) => X * v.X + Y * v.Y + Z * v.Z;
+
+        public static Vector3 Transform(Vector3 v, Quaternion q)
+        {
+            Quaternion vq = new Quaternion(v, 0.0f);
+            q = q * vq * Quaternion.Conjugate(q);
+            return new Vector3(q.X, q.Y, q.Z);
+        }
+
+        public static Vector3 Transform(Vector3 v, ref Matrix m)
+        {
+            return new Vector3(
+                v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31 + m.M41,
+                v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32 + m.M42,
+                v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33 + m.M43);
+        }
+
+        public static void Transform(ref Vector3 v, ref Matrix m, out Vector3 r)
+        {
+            r.X = v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31 + m.M41;
+            r.Y = v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32 + m.M42;
+            r.Z = v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33 + m.M43;
+        }
+
+        public static Vector3 TransformNormal(Vector3 v, ref Matrix m)
+        {
+            return new Vector3(
+                v.X * m.M11 + v.Y * m.M21 + v.Z * m.M31,
+                v.X * m.M12 + v.Y * m.M22 + v.Z * m.M32,
+                v.X * m.M13 + v.Y * m.M23 + v.Z * m.M33);
+        }
+
+        public static void Transform(Vector3[] v, ref Matrix m, Vector3[] r)
+        {
+            for (int i = 0; i < v.Length; i++)
+            {
+                float x = v[i].X;
+                float y = v[i].Y;
+                float z = v[i].Z;
+
+                r[i].X = x * m.M11 + y * m.M21 + z * m.M31 + m.M41;
+                r[i].Y = x * m.M12 + y * m.M22 + z * m.M32 + m.M42;
+                r[i].Z = x * m.M13 + y * m.M23 + z * m.M33 + m.M43;
+            }
+        }
+
+        public static Vector3[] Transform(Vector3[] v, ref Matrix m)
+        {
+            var r = new Vector3[v.Length];
+            Transform(v, ref m, r);
+            return r;
+        }
+
+        public static void TransformNormal(Vector3[] v, ref Matrix m, Vector3[] r)
+        {
+            for (int i = 0; i < v.Length; i++)
+            {
+                float x = v[i].X;
+                float y = v[i].Y;
+                float z = v[i].Z;
+
+                r[i].X = x * m.M11 + y * m.M21 + z * m.M31;
+                r[i].Y = x * m.M12 + y * m.M22 + z * m.M32;
+                r[i].Z = x * m.M13 + y * m.M23 + z * m.M33;
+            }
+        }
+
+        public static Vector3[] TransformNormal(Vector3[] v, ref Matrix m)
+        {
+            var r = new Vector3[v.Length];
+            TransformNormal(v, ref m, r);
+            return r;
+        }
+
+        public static Vector3 Min(Vector3 v1, Vector3 v2)
+        {
+            if (v2.X < v1.X)
+                v1.X = v2.X;
+
+            if (v2.Y < v1.Y)
+                v1.Y = v2.Y;
+
+            if (v2.Z < v1.Z)
+                v1.Z = v2.Z;
+
+            return v1;
+        }
+
+        public static void Min(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
+        {
+            r.X = (v1.X < v2.X) ? v1.X : v2.X;
+            r.Y = (v1.Y < v2.Y) ? v1.Y : v2.Y;
+            r.Z = (v1.Z < v2.Z) ? v1.Z : v2.Z;
+        }
+
+        public static Vector3 Max(Vector3 v1, Vector3 v2)
+        {
+            if (v2.X > v1.X)
+                v1.X = v2.X;
+
+            if (v2.Y > v1.Y)
+                v1.Y = v2.Y;
+
+            if (v2.Z > v1.Z)
+                v1.Z = v2.Z;
+
+            return v1;
+        }
+
+        public static void Max(ref Vector3 v1, ref Vector3 v2, out Vector3 r)
+        {
+            r.X = (v1.X > v2.X) ? v1.X : v2.X;
+            r.Y = (v1.Y > v2.Y) ? v1.Y : v2.Y;
+            r.Z = (v1.Z > v2.Z) ? v1.Z : v2.Z;
+        }
+
+        public static Vector3 Normalize(Vector3 v) => v * (1.0f / v.Length());
+
+        public void Normalize()
+        {
+            float k = 1.0f / Length();
+
+            X *= k;
+            Y *= k;
+            Z *= k;
+        }
+
+        public float LengthSquared() => X * X + Y * Y + Z * Z;
+
+        public float Length() => FMath.Sqrt(LengthSquared());
+
+        public static float Distance(Vector3 v1, Vector3 v2) => FMath.Sqrt((v2 - v1).LengthSquared());
+
+        public static float DistanceSquared(Vector3 v1, Vector3 v2) => (v2 - v1).LengthSquared();
+
+        public static Vector3 Lerp(Vector3 v1, Vector3 v2, float amount) => v1 + (v2 - v1) * amount;
+
+        public static bool EqualsEps(Vector3 v1, Vector3 v2)
+        {
+            Vector3 d = v2 - v1;
+
+            float dx = Math.Abs(d.X);
+            float dy = Math.Abs(d.Y);
+            float dz = Math.Abs(d.Z);
+
+            return (dx < 0.0001f && dy < 0.0001f && dz < 0.0001f);
+        }
+
+        public static bool operator ==(Vector3 v1, Vector3 v2) => v1.X == v2.X && v1.Y == v2.Y && v1.Z == v2.Z;
+        public static bool operator !=(Vector3 v1, Vector3 v2) => v1.X != v2.X || v1.Y != v2.Y || v1.Z != v2.Z;
+
+        public bool Equals(Vector3 other) => X == other.X && Y == other.Y && Z == other.Z;
+        public override bool Equals(object obj) => obj is Vector3 && Equals((Vector3)obj);
+        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode();
+
+        public override string ToString() => $"{{{X} {Y} {Z}}}";
+
+        private static Vector3 zero = new Vector3();
+        private static Vector3 one = new Vector3(1.0f);
+        private static Vector3 up = new Vector3(0.0f, 1.0f, 0.0f);
+        private static Vector3 down = new Vector3(0.0f, -1.0f, 0.0f);
+        private static Vector3 right = new Vector3(1.0f, 0.0f, 0.0f);
+        private static Vector3 left = new Vector3(-1.0f, 0.0f, 0.0f);
+        private static Vector3 backward = new Vector3(0.0f, 0.0f, 1.0f);
+        private static Vector3 forward = new Vector3(0.0f, 0.0f, -1.0f);
+
+        public static Vector3 Zero => zero;
+        public static Vector3 One => one;
+        public static Vector3 Up => up;
+        public static Vector3 Down => down;
+        public static Vector3 Left => left;
+        public static Vector3 Right => right;
+        public static Vector3 Backward => backward;
+        public static Vector3 Forward => forward;
+        public static Vector3 UnitX => right;
+        public static Vector3 UnitY => up;
+        public static Vector3 UnitZ => backward;
+
+        public float this[int i]
+        {
+            get
+            {
+                if (i == 1)
+                    return Y;
+                else if (i < 1)
+                    return X;
+                else
+                    return Z;
+            }
+        }
+    }
+}
Index: /OniSplit/Math/Vector4.cs
===================================================================
--- /OniSplit/Math/Vector4.cs	(revision 1114)
+++ /OniSplit/Math/Vector4.cs	(revision 1114)
@@ -0,0 +1,146 @@
+﻿using System;
+
+namespace Oni
+{
+    internal struct Vector4 : IEquatable<Vector4>
+    {
+        public float X;
+        public float Y;
+        public float Z;
+        public float W;
+
+        public Vector4(float all)
+        {
+            X = all;
+            Y = all;
+            Z = all;
+            W = all;
+        }
+
+        public Vector4(Vector3 v, float w)
+        {
+            X = v.X;
+            Y = v.Y;
+            Z = v.Z;
+            W = w;
+        }
+
+        public Vector4(float x, float y, float z, float w)
+        {
+            X = x;
+            Y = y;
+            Z = z;
+            W = w;
+        }
+
+        public Vector3 XYZ
+        {
+            get
+            {
+                return new Vector3(X, Y, Z);
+            }
+            set
+            {
+                X = value.X;
+                Y = value.Y;
+                Z = value.Z;
+            }
+        }
+
+        public static Vector4 operator +(Vector4 v1, Vector4 v2)
+        {
+            v1.X += v2.X;
+            v1.Y += v2.Y;
+            v1.Z += v2.Z;
+            v1.W += v2.W;
+
+            return v1;
+        }
+
+        public static Vector4 operator -(Vector4 v1, Vector4 v2)
+        {
+            v1.X -= v2.X;
+            v1.Y -= v2.Y;
+            v1.Z -= v2.Z;
+            v1.W -= v2.W;
+
+            return v1;
+        }
+
+        public static Vector4 operator *(Vector4 v, float s)
+        {
+            v.X *= s;
+            v.Y *= s;
+            v.Z *= s;
+            v.W *= s;
+
+            return v;
+        }
+
+        public static Vector4 operator *(float s, Vector4 v) => v * s;
+
+        public static Vector4 operator /(Vector4 v, float s) => v * (1.0f / s);
+
+        public static float Dot(Vector4 v1, Vector4 v2) => v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z + v1.W * v2.W;
+
+        public static Vector4 Min(Vector4 v1, Vector4 v2)
+        {
+            v1.X = (v1.X < v2.X) ? v1.X : v2.X;
+            v1.Y = (v1.Y < v2.Y) ? v1.Y : v2.Y;
+            v1.Z = (v1.Z < v2.Z) ? v1.Z : v2.Z;
+            v1.W = (v1.W < v2.W) ? v1.W : v2.W;
+
+            return v1;
+        }
+
+        public static Vector4 Max(Vector4 v1, Vector4 v2)
+        {
+            v1.X = (v1.X > v2.X) ? v1.X : v2.X;
+            v1.Y = (v1.Y > v2.Y) ? v1.Y : v2.Y;
+            v1.Z = (v1.Z > v2.Z) ? v1.Z : v2.Z;
+            v1.W = (v1.W > v2.W) ? v1.W : v2.W;
+
+            return v1;
+        }
+
+        public static Vector4 Normalize(Vector4 v) => v * (1.0f / v.Length());
+
+        public float LengthSquared() => X * X + Y * Y + Z * Z + W * W;
+        public float Length() => FMath.Sqrt(LengthSquared());
+
+        public static bool EqualsEps(Vector4 v1, Vector4 v2)
+        {
+            Vector4 d = v2 - v1;
+
+            float dx = Math.Abs(d.X);
+            float dy = Math.Abs(d.Y);
+            float dz = Math.Abs(d.Z);
+            float dw = Math.Abs(d.W);
+
+            return (dx < 0.0001f && dy < 0.0001f && dz < 0.0001f && dw < 0.0001f);
+        }
+
+        public static bool operator ==(Vector4 v1, Vector4 v2) => v1.X == v2.X && v1.Y == v2.Y && v1.Z == v2.Z && v1.W == v2.W;
+        public static bool operator !=(Vector4 v1, Vector4 v2) => v1.X != v2.X || v1.Y != v2.Y || v1.Z != v2.Z || v1.W != v2.W;
+
+        public bool Equals(Vector4 other) => X == other.X && Y == other.Y && Z == other.Z && W == other.W;
+        public override bool Equals(object obj) => obj is Vector4 && Equals((Vector4)obj);
+        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode() ^ W.GetHashCode();
+
+        public override string ToString() => $"{{{X} {Y} {Z} {W}}}";
+
+        private static Vector4 zero = new Vector4();
+        private static Vector4 one = new Vector4(1.0f);
+        private static Vector4 unitX = new Vector4(1.0f, 0.0f, 0.0f, 0.0f);
+        private static Vector4 unitY = new Vector4(0.0f, 1.0f, 0.0f, 0.0f);
+        private static Vector4 unitZ = new Vector4(0.0f, 0.0f, 1.0f, 0.0f);
+        private static Vector4 unitW = new Vector4(0.0f, 0.0f, 0.0f, 1.0f);
+
+        public static Vector4 Zero => zero;
+        public static Vector4 One => one;
+        public static Vector4 UnitX => unitX;
+        public static Vector4 UnitY => unitY;
+        public static Vector4 UnitZ => unitZ;
+        public static Vector4 UnitW => unitW;
+    }
+}
Index: /OniSplit/Metadata/BinaryMetadata.cs
===================================================================
--- /OniSplit/Metadata/BinaryMetadata.cs	(revision 1114)
+++ /OniSplit/Metadata/BinaryMetadata.cs	(revision 1114)
@@ -0,0 +1,156 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Metadata
+{
+    internal class BinaryMetadata
+    {
+        private static MetaStruct bobj = new MetaStruct("ObjectHeader",
+            new Field(MetaType.Int32, "ObjectId"),
+            new Field(MetaType.Int32, "ObjectFlags"),
+            new Field(MetaType.Vector3, "Position"),
+            new Field(MetaType.Vector3, "Rotation")
+        );
+
+        private static MetaStruct cons = new MetaStruct("Console", bobj,
+            new Field(MetaType.String(63), "ClassName"),
+            new Field(MetaType.Int16, "DoorId"),
+            new Field(MetaType.Int16, "Flags"),
+            new Field(MetaType.String(63), "DisabledTexture"),
+            new Field(MetaType.String(63), "EnabledTexture"),
+            new Field(MetaType.String(63), "UsedTexture"),
+            new Field(MetaType.ShortVarArray(new MetaStruct("ConsoleAction",
+                new Field(MetaType.Int16, "ActionType"),
+                new Field(MetaType.Int16, "TargetId"),                  // if actionType != 1
+                new Field(MetaType.String32, "ScriptFunction")          // if actionType == 1
+            )), "Actions")
+        );
+
+        private static MetaStruct door = new MetaStruct("Door", bobj,
+            new Field(MetaType.String(63), "ClassName"),
+            new Field(MetaType.Int16, "DoorId"),
+            new Field(MetaType.Int16, ""),
+            new Field(MetaType.Int16, "Flags"),
+            new Field(MetaType.Vector3, "Center"),
+            new Field(MetaType.Float, "ActivationRadius"),
+            new Field(MetaType.String(63), "Texture1"),
+            new Field(MetaType.String(63), "Texture2"),
+            new Field(MetaType.Padding(5))
+        );
+
+        private static MetaStruct flag = new MetaStruct("Flag", bobj,
+            new Field(MetaType.Color, "Color"),
+            new Field(MetaType.Int16, "Prefix"),
+            new Field(MetaType.Int16, "FlagId"),
+            new Field(MetaType.String128, "Notes")
+        );
+
+        private static MetaStruct furn = new MetaStruct("Furniture", bobj,
+            new Field(MetaType.String32, "ClassName"),
+            new Field(MetaType.String48, "Particle")
+        );
+
+        private static MetaStruct part = new MetaStruct("Particle", bobj,
+            new Field(MetaType.String64, "ClassName"),
+            new Field(MetaType.String48, "Name"),
+            new Field(MetaType.Int16, "Flags"),
+            new Field(MetaType.Float, "DecalXScale"),
+            new Field(MetaType.Float, "DecalYScale"),
+            new Field(MetaType.Int16, "DecalRotation")
+        );
+
+        private static MetaStruct pwru = new MetaStruct("Powerup", bobj,
+            new Field(MetaType.Int32, "Type")
+        );
+
+        private static MetaStruct sndg = new MetaStruct("Sound", bobj,
+            new Field(MetaType.String32, "ClassName"),
+            new Field(MetaType.Int32, "GeometryType"),
+            new Field(MetaType.BoundingBox, "BoundingBox"),     // if geometry == VLME
+            new Field(MetaType.Float, "MinVolumeRadius"),       // if geometry == SPHR
+            new Field(MetaType.Float, "MaxVolumeRadius"),       // if geometry == SPHR
+            new Field(MetaType.Float, "Volume"),
+            new Field(MetaType.Float, "Pitch")
+        );
+
+        private static MetaStruct trge = new MetaStruct("Trigger", bobj,
+            new Field(MetaType.String(63), "ClassName"),
+            new Field(MetaType.Int16, "TriggerId"),
+            new Field(MetaType.Int16, "Flags"),
+            new Field(MetaType.Color, "Color"),
+            new Field(MetaType.Float, "StartPosition"),
+            new Field(MetaType.Float, "Speed"),
+            new Field(MetaType.Int32, "Count"),
+            new Field(MetaType.Int16, ""),
+            new Field(MetaType.ShortVarArray(new MetaStruct("TriggerAction",
+                new Field(MetaType.Int16, "ActionType"),
+                new Field(MetaType.Int16, "TurretId"),                  // if actionType != 1
+                new Field(MetaType.String(34), "ScriptFunction")    // if actionType == 1
+            )), "Actions"),
+            new Field(MetaType.Byte, "")
+        );
+
+        private static MetaStruct turr = new MetaStruct("Turret", bobj,
+            new Field(MetaType.String(63), "ClassName"),
+            new Field(MetaType.Int16, "TurretId"),
+            new Field(MetaType.Int16, "Flags"),
+            new Field(MetaType.Padding(36)),
+            new Field(MetaType.Int32, "TargetedTeams"),
+            new Field(MetaType.Padding(1))
+        );
+
+        private static MetaStruct weap = new MetaStruct("Weapon", bobj,
+            new Field(MetaType.String32, "ClassName")
+        );
+
+        private static MetaType osbdAmbient = new MetaStruct("OSBDAmbient",
+            new Field(MetaType.Int32, "Version"),
+            new Field(MetaType.Int32, "Priority"),
+            new Field(MetaType.Int32, "Flags"),
+            new Field(MetaType.Float, "Unknown1"),
+            new Field(MetaType.Float, "MinElapsedTime"),
+            new Field(MetaType.Float, "MaxElapsedTime"),
+            new Field(MetaType.Float, "MinVolumeDistance"),
+            new Field(MetaType.Float, "MaxVolumeDistance"),
+            new Field(MetaType.String32, "DetailGroup"),
+            new Field(MetaType.String32, "Track1Group"),
+            new Field(MetaType.String32, "Track2Group"),
+            new Field(MetaType.String32, "InGroup"),
+            new Field(MetaType.String32, "OutGroup"),
+            new Field(MetaType.Int32, "Unknown2"),              // v >= 5
+            new Field(MetaType.Float, "Unknown3")               // v >= 6
+        );
+
+        private static MetaType osbdImpulse = new MetaStruct("OSBDImpulse",
+            new Field(MetaType.Int32, "Version"),
+            new Field(MetaType.String32, "Group"),
+            new Field(MetaType.Int32, "Priority"),
+            new Field(MetaType.Float, "MinVolumeDistance"),
+            new Field(MetaType.Float, "MaxVolumeDistance"),
+            new Field(MetaType.Float, "MinVolumeAngle"),
+            new Field(MetaType.Float, "MaxVolumeAngle"),
+            new Field(MetaType.Int32, "Unknown1"),
+            new Field(MetaType.Int32, "Unknown2"),              // v >= 4
+            new Field(MetaType.String32, "AlternateImpulse"),   // v >= 4
+            new Field(MetaType.Int32, "Unknown3"),              // v >= 5
+            new Field(MetaType.Int32, "Unknown4")               // v >= 6
+        );
+
+        private static MetaType osbdGroup = new MetaStruct("OSBDGroup",
+            new Field(MetaType.Int32, "Version"),
+            new Field(MetaType.Float, "Volume"),                // v >= 2
+            new Field(MetaType.Float, "Pitch"),                 // v >= 3
+            new Field(MetaType.Int16, "PreventRepeat"),         // v >= 6
+            new Field(MetaType.Int16, "PreviousPermutation"),   // v >= 6
+            new Field(MetaType.Int32, "ChannelCount"),
+            new Field(MetaType.VarArray(new MetaStruct("OSBDGroupPermutation",
+                new Field(MetaType.Int32, "Weight"),
+                new Field(MetaType.Float, "MinVolume"),
+                new Field(MetaType.Float, "MaxVolume"),
+                new Field(MetaType.Float, "MinPitch"),
+                new Field(MetaType.Float, "MaxPitch"),
+                new Field(MetaType.String32, "Sound")
+            )), "Permutations")
+        );
+    }
+}
Index: /OniSplit/Metadata/BinaryPartField.cs
===================================================================
--- /OniSplit/Metadata/BinaryPartField.cs	(revision 1114)
+++ /OniSplit/Metadata/BinaryPartField.cs	(revision 1114)
@@ -0,0 +1,51 @@
+﻿using System;
+
+namespace Oni.Metadata
+{
+    internal class BinaryPartField : Field
+    {
+        private string sizeFieldName;
+        private int sizeMultiplier;
+        private MetaType rawType;
+
+        public BinaryPartField(MetaType offsetType, string name)
+            : this(offsetType, name, null, 0)
+        {
+        }
+
+        public BinaryPartField(MetaType offsetType, string name, string sizeFieldName)
+            : this(offsetType, name, sizeFieldName, 1)
+        {
+        }
+
+        public BinaryPartField(MetaType offsetType, string name, int size)
+            : this(offsetType, name, null, size)
+        {
+        }
+
+        public BinaryPartField(MetaType offsetType, string name, int size, MetaType rawType)
+            : this(offsetType, name, null, size, rawType)
+        {
+        }
+
+        public BinaryPartField(MetaType offsetType, string name, string sizeFieldName, int sizeMultiplier)
+            : this(offsetType, name, sizeFieldName, sizeMultiplier, null)
+        {
+        }
+
+        public BinaryPartField(MetaType offsetType, string name, string sizeFieldName, int sizeMultiplier, MetaType rawType)
+            : base(offsetType, name)
+        {
+            if (offsetType != MetaType.RawOffset && offsetType != MetaType.SepOffset)
+                throw new ArgumentException("Offset type can only be RawOffset or SepOffset", "offsetType");
+
+            this.sizeFieldName = sizeFieldName;
+            this.sizeMultiplier = sizeMultiplier;
+            this.rawType = rawType;
+        }
+
+        public string SizeFieldName => sizeFieldName;
+        public int SizeMultiplier => sizeMultiplier;
+        public MetaType RawType => rawType;
+    }
+}
Index: /OniSplit/Metadata/BinaryTag.cs
===================================================================
--- /OniSplit/Metadata/BinaryTag.cs	(revision 1114)
+++ /OniSplit/Metadata/BinaryTag.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal enum BinaryTag
+    {
+        PAR3 = 0x50415233,
+        OBJC = 0x4f424a43,
+        TMBD = 0x544d4244,
+        ONIE = 0x4f4e4945,
+        SABD = 0x53414244
+    }
+}
Index: /OniSplit/Metadata/CompareVisitor.cs
===================================================================
--- /OniSplit/Metadata/CompareVisitor.cs	(revision 1114)
+++ /OniSplit/Metadata/CompareVisitor.cs	(revision 1114)
@@ -0,0 +1,86 @@
+﻿namespace Oni.Metadata
+{
+    internal class CompareVisitor : MetaTypeVisitor
+    {
+        private readonly BinaryReader reader1;
+        private readonly BinaryReader reader2;
+        private bool equals = true;
+
+        public CompareVisitor(BinaryReader reader1, BinaryReader reader2)
+        {
+            this.reader1 = reader1;
+            this.reader2 = reader2;
+        }
+
+        public bool AreEquals => equals;
+
+        public override void VisitByte(MetaByte type) => equals = (reader1.ReadByte() == reader2.ReadByte());
+        public override void VisitInt16(MetaInt16 type) => equals = (reader1.ReadInt16() == reader2.ReadInt16());
+        public override void VisitInt32(MetaInt32 type) => equals = (reader1.ReadInt32() == reader2.ReadInt32());
+        public override void VisitUInt32(MetaUInt32 type) => equals = (reader1.ReadUInt32() == reader2.ReadUInt32());
+        public override void VisitInt64(MetaInt64 type) => equals = (reader1.ReadInt64() == reader2.ReadInt64());
+        public override void VisitUInt64(MetaUInt64 type) => equals = (reader1.ReadUInt64() == reader2.ReadUInt64());
+        public override void VisitFloat(MetaFloat type) => equals = (reader1.ReadSingle() == reader2.ReadSingle());
+        public override void VisitColor(MetaColor type) => equals = (reader1.ReadInt32() == reader2.ReadInt32());
+        public override void VisitRawOffset(MetaRawOffset type) => equals = (reader1.ReadInt32() == reader2.ReadInt32());
+        public override void VisitSepOffset(MetaSepOffset type) => equals = (reader1.ReadInt32() == reader2.ReadInt32());
+        public override void VisitPointer(MetaPointer type) => equals = (reader1.ReadInt32() == reader2.ReadInt32());
+        public override void VisitString(MetaString type) => equals = (reader1.ReadString(type.Count) == reader2.ReadString(type.Count));
+
+        public override void VisitPadding(MetaPadding type)
+        {
+            reader1.Skip(type.Count);
+            reader2.Skip(type.Count);
+        }
+
+        public override void VisitStruct(MetaStruct type)
+        {
+            foreach (var field in type.Fields)
+            {
+                field.Type.Accept(this);
+
+                if (!equals)
+                    break;
+            }
+        }
+
+        public override void VisitArray(MetaArray type)
+        {
+            CompareArray(type.ElementType, type.Count);
+        }
+
+        public override void VisitVarArray(MetaVarArray type)
+        {
+            int count1, count2;
+
+            if (type.CountField.Type == MetaType.Int16)
+            {
+                count1 = reader1.ReadInt16();
+                count2 = reader2.ReadInt16();
+            }
+            else
+            {
+                count1 = reader1.ReadInt32();
+                count2 = reader2.ReadInt32();
+            }
+
+            equals = (count1 == count2);
+
+            if (!equals)
+                return;
+
+            CompareArray(type.ElementType, count1);
+        }
+
+        private void CompareArray(MetaType elementType, int count)
+        {
+            for (int i = 0; i < count; i++)
+            {
+                elementType.Accept(this);
+
+                if (!equals)
+                    break;
+            }
+        }
+    }
+}
Index: /OniSplit/Metadata/CopyVisitor.cs
===================================================================
--- /OniSplit/Metadata/CopyVisitor.cs	(revision 1114)
+++ /OniSplit/Metadata/CopyVisitor.cs	(revision 1114)
@@ -0,0 +1,300 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Metadata
+{
+    internal class CopyVisitor : MetaTypeVisitor
+    {
+        #region Private data
+        private BinaryReader input;
+        private BinaryWriter output;
+        private Action<CopyVisitor> callback;
+        private Stack<ActiveField> activeFields;
+        private MetaType topLevelType;
+        private MetaType currentType;
+        private byte[] buffer;
+        private int fieldSize;
+        private int position;
+        #endregion
+
+        #region private class ActiveField
+
+        private class ActiveField
+        {
+            public Field Field;
+            public int Index;
+
+            public ActiveField(Field field)
+            {
+                Field = field;
+                Index = -1;
+            }
+        }
+
+        #endregion
+
+        public CopyVisitor(BinaryReader reader, BinaryWriter writer, Action<CopyVisitor> callback)
+        {
+            this.input = reader;
+            this.output = writer;
+            this.callback = callback;
+            this.activeFields = new Stack<ActiveField>();
+        }
+
+        public MetaType TopLevelType => topLevelType;
+
+        public MetaType Type => currentType;
+
+        public Field Field
+        {
+            get
+            {
+                if (activeFields.Count == 0)
+                    return null;
+
+                return activeFields.Peek().Field;
+            }
+        }
+
+        public int Position => position;
+
+        public byte GetByte() => buffer[0];
+
+        public short GetInt16() => (short)(buffer[0] | (buffer[1] << 8));
+
+        public ushort GetUInt16() => (ushort)(buffer[0] | (buffer[1] << 8));
+
+        public int GetInt32() => buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24);
+
+        public uint GetUInt32() => (uint)buffer[0] | ((uint)buffer[1] << 8) | ((uint)buffer[2] << 16) | ((uint)buffer[3] << 24);
+
+        public void SetInt32(int value)
+        {
+            buffer[0] = (byte)value;
+            buffer[1] = (byte)(value >> 8);
+            buffer[2] = (byte)(value >> 16);
+            buffer[3] = (byte)(value >> 24);
+        }
+
+        public string GetCurrentFieldName()
+        {
+            if (activeFields.Count == 0)
+                return null;
+
+            List<string> names = new List<string>();
+
+            foreach (ActiveField state in activeFields)
+            {
+                if (string.IsNullOrEmpty(state.Field.Name))
+                    return null;
+
+                if (state.Index >= 0)
+                    names.Add(string.Format("{0}[{1}]", state.Field.Name, state.Index));
+                else
+                    names.Add(state.Field.Name);
+            }
+
+            names.Add(topLevelType.Name);
+            names.Reverse();
+
+            return string.Join(".", names.ToArray());
+        }
+
+        public string GetParentFieldName()
+        {
+            if (activeFields.Count == 0)
+                return null;
+
+            List<string> names = new List<string>();
+
+            foreach (ActiveField state in activeFields)
+            {
+                if (string.IsNullOrEmpty(state.Field.Name))
+                    return null;
+
+                if (state.Index >= 0)
+                    names.Add(string.Format("{0}[{1}]", state.Field.Name, state.Index));
+                else
+                    names.Add(state.Field.Name);
+            }
+
+            names.Add(topLevelType.Name);
+            names.Reverse();
+            names.RemoveAt(names.Count - 1);
+
+            return string.Join(".", names.ToArray());
+        }
+
+        public override void VisitByte(MetaByte type) => CopyBytes(type);
+        public override void VisitInt16(MetaInt16 type) => CopyBytes(type);
+        public override void VisitUInt16(MetaUInt16 type) => CopyBytes(type);
+        public override void VisitInt32(MetaInt32 type) => CopyBytes(type);
+        public override void VisitUInt32(MetaUInt32 type) => CopyBytes(type);
+        public override void VisitInt64(MetaInt64 type) => CopyBytes(type);
+        public override void VisitUInt64(MetaUInt64 type) => CopyBytes(type);
+        public override void VisitFloat(MetaFloat type) => CopyBytes(type);
+        public override void VisitColor(MetaColor type) => CopyBytes(type);
+        public override void VisitRawOffset(MetaRawOffset type) => CopyBytes(type);
+        public override void VisitSepOffset(MetaSepOffset type) => CopyBytes(type);
+        public override void VisitPointer(MetaPointer type) => CopyBytes(type);
+        public override void VisitBoundingBox(MetaBoundingBox type) => CopyBytes(type);
+        public override void VisitBoundingSphere(MetaBoundingSphere type) => CopyBytes(type);
+        public override void VisitMatrix4x3(MetaMatrix4x3 type) => CopyBytes(type);
+        public override void VisitPlane(MetaPlane type) => CopyBytes(type);
+        public override void VisitQuaternion(MetaQuaternion type) => CopyBytes(type);
+        public override void VisitVector2(MetaVector2 type) => CopyBytes(type);
+        public override void VisitVector3(MetaVector3 type) => CopyBytes(type);
+
+        private void CopyBytes(MetaType type)
+        {
+            BeginCopy(type, 1);
+            EndCopy();
+        }
+
+        public override void VisitString(MetaString type)
+        {
+            BeginCopy(type, 1);
+
+            bool zeroFound = false;
+
+            for (int i = 0; i < type.Size; i++)
+            {
+                if (zeroFound)
+                    buffer[i] = 0;
+                else if (buffer[i] == 0)
+                    zeroFound = true;
+            }
+
+            EndCopy();
+        }
+
+        public override void VisitPadding(MetaPadding type)
+        {
+            BeginCopy(type, 1);
+
+            if (type.FillByte == 0)
+            {
+                Array.Clear(buffer, 0, type.Size);
+            }
+            else
+            {
+                for (int i = 0; i < type.Size; i++)
+                    buffer[i] = type.FillByte;
+            }
+
+            EndCopy();
+        }
+
+        public override void VisitStruct(MetaStruct type)
+        {
+            if (topLevelType == null)
+                topLevelType = type;
+
+            foreach (Field field in type.Fields)
+            {
+                BeginCopyField(field);
+                field.Type.Accept(this);
+                EndCopyField(field);
+            }
+        }
+
+        internal void BeginCopyField(Field field)
+        {
+            activeFields.Push(new ActiveField(field));
+        }
+
+        internal void EndCopyField(Field field)
+        {
+            if (activeFields.Peek().Field != field)
+                throw new InvalidOperationException();
+
+            activeFields.Pop();
+        }
+
+        public override void VisitArray(MetaArray type)
+        {
+            CopyArray(type.ElementType, type.Count);
+        }
+
+        public override void VisitVarArray(MetaVarArray type)
+        {
+            BeginCopyField(type.CountField);
+            type.CountField.Type.Accept(this);
+            EndCopyField(type.CountField);
+
+            int length;
+
+            if (type.CountField.Type == MetaType.Int16)
+                length = GetInt16();
+            else
+                length = GetInt32();
+
+            if (length < 0)
+                throw new InvalidDataException(string.Format("Invalid array length: 0x{0:x} at offset 0x{1:x}", length, position));
+
+            CopyArray(type.ElementType, length);
+        }
+
+        private void CopyArray(MetaType elementType, int count)
+        {
+            if (elementType.IsBlittable)
+            {
+                BeginCopy(elementType, count);
+                EndCopy();
+                return;
+            }
+
+            for (int i = 0; i < count; i++)
+            {
+                BeginCopyArrayElement(i);
+                elementType.Accept(this);
+                EndCopyArrayElement(i);
+            }
+        }
+
+        private void BeginCopyArrayElement(int index)
+        {
+            if (activeFields.Count == 0)
+                return;
+
+            activeFields.Peek().Index = index;
+        }
+
+        private void EndCopyArrayElement(int index)
+        {
+            if (activeFields.Count == 0)
+                return;
+
+            ActiveField field = activeFields.Peek();
+
+            if (field.Index != index)
+                throw new InvalidOperationException();
+
+            field.Index = -1;
+        }
+
+        private void BeginCopy(MetaType type, int count)
+        {
+            currentType = type;
+
+            fieldSize = type.Size * count;
+
+            if (buffer == null || buffer.Length < fieldSize)
+                buffer = new byte[fieldSize * 2];
+
+            input.Read(buffer, 0, fieldSize);
+        }
+
+        private void EndCopy()
+        {
+            if (callback != null)
+                callback(this);
+
+            if (output != null)
+                output.Write(buffer, 0, fieldSize);
+
+            position += fieldSize;
+        }
+    }
+}
Index: /OniSplit/Metadata/DumpVisitor.cs
===================================================================
--- /OniSplit/Metadata/DumpVisitor.cs	(revision 1114)
+++ /OniSplit/Metadata/DumpVisitor.cs	(revision 1114)
@@ -0,0 +1,262 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Metadata
+{
+    internal class DumpVisitor : MetaTypeVisitor
+    {
+        #region Private data
+        private readonly InstanceMetadata metadata;
+        private readonly TextWriter writer;
+        private Stack<ActiveField> activeFields;
+        private MetaType topLevelType;
+        private MetaType currentType;
+        private int fieldSize;
+        private int position;
+        private string indent = "";
+        private Field field;
+        #endregion
+
+        #region private class ActiveField
+
+        private class ActiveField
+        {
+            public Field Field;
+            public int Index;
+
+            public ActiveField(Field field)
+            {
+                this.Field = field;
+                this.Index = -1;
+            }
+        }
+
+        #endregion
+
+        public DumpVisitor(TextWriter writer, InstanceMetadata metadata)
+        {
+            this.writer = writer;
+            this.metadata = metadata;
+        }
+
+        public MetaType TopLevelType => topLevelType;
+        public MetaType Type => currentType;
+
+        public Field Field
+        {
+            get
+            {
+                if (activeFields.Count == 0)
+                    return null;
+
+                return activeFields.Peek().Field;
+            }
+        }
+
+        public int Position => position;
+
+        public string GetCurrentFieldName()
+        {
+            if (activeFields.Count == 0)
+                return null;
+
+            List<string> names = new List<string>();
+
+            foreach (ActiveField state in activeFields)
+            {
+                if (string.IsNullOrEmpty(state.Field.Name))
+                    return null;
+
+                if (state.Index >= 0)
+                    names.Add(string.Format("{0}[{1}]", state.Field.Name, state.Index));
+                else
+                    names.Add(state.Field.Name);
+            }
+
+            names.Add(topLevelType.Name);
+            names.Reverse();
+
+            return string.Join(".", names.ToArray());
+        }
+
+        public string GetParentFieldName()
+        {
+            if (activeFields.Count == 0)
+                return null;
+
+            var names = new List<string>();
+
+            foreach (ActiveField state in activeFields)
+            {
+                if (string.IsNullOrEmpty(state.Field.Name))
+                    return null;
+
+                if (state.Index >= 0)
+                    names.Add(string.Format("{0}[{1}]", state.Field.Name, state.Index));
+                else
+                    names.Add(state.Field.Name);
+            }
+
+            names.Add(topLevelType.Name);
+            names.Reverse();
+            names.RemoveAt(names.Count - 1);
+
+            return string.Join(".", names.ToArray());
+        }
+
+        public override void VisitByte(MetaByte type)
+        {
+            writer.Write(indent);
+            writer.Write("uint8_t");
+        }
+
+        public override void VisitInt16(MetaInt16 type)
+        {
+            writer.Write(indent);
+            writer.Write("int16_t");
+        }
+
+        public override void VisitUInt16(MetaUInt16 type)
+        {
+            writer.Write(indent);
+            writer.Write("uint16_t");
+        }
+
+        public override void VisitInt32(MetaInt32 type)
+        {
+            writer.Write(indent);
+            writer.Write("int32_t");
+        }
+
+        public override void VisitUInt32(MetaUInt32 type)
+        {
+            writer.Write(indent);
+            writer.Write("uint32_t");
+        }
+
+        public override void VisitInt64(MetaInt64 type)
+        {
+            writer.Write(indent);
+            writer.Write("int64_t");
+        }
+
+        public override void VisitUInt64(MetaUInt64 type)
+        {
+            writer.Write(indent);
+            writer.Write("uint64_t");
+        }
+
+        public override void VisitFloat(MetaFloat type)
+        {
+            writer.Write(indent);
+            writer.Write("float");
+        }
+
+        public override void VisitColor(MetaColor type)
+        {
+            writer.Write(indent);
+            writer.Write("uint32_t");
+        }
+
+        public override void VisitRawOffset(MetaRawOffset type)
+        {
+            writer.Write(indent);
+            writer.Write("void*");
+        }
+
+        public override void VisitSepOffset(MetaSepOffset type)
+        {
+            writer.Write(indent);
+            writer.Write("void*");
+        }
+
+        public override void VisitPointer(MetaPointer type)
+        {
+            writer.Write(indent);
+            writer.Write("{0}*", metadata.GetTemplate(type.Tag).Type.Name);
+        }
+
+        public override void VisitBoundingBox(MetaBoundingBox type)
+        {
+            writer.Write(indent);
+            writer.Write("bbox");
+        }
+
+        public override void VisitBoundingSphere(MetaBoundingSphere type)
+        {
+            writer.Write(indent);
+            writer.Write("bsphere");
+        }
+
+        public override void VisitMatrix4x3(MetaMatrix4x3 type)
+        {
+            writer.Write(indent);
+            writer.Write("matrix43");
+        }
+
+        public override void VisitPlane(MetaPlane type)
+        {
+            writer.Write(indent);
+            writer.Write("plane");
+        }
+
+        public override void VisitQuaternion(MetaQuaternion type)
+        {
+            writer.Write(indent);
+            writer.Write("quat");
+        }
+
+        public override void VisitVector2(MetaVector2 type)
+        {
+            writer.Write(indent);
+            writer.Write("vec2");
+        }
+
+        public override void VisitVector3(MetaVector3 type)
+        {
+            writer.Write(indent);
+            writer.Write("vec3");
+        }
+
+        public override void VisitString(MetaString type)
+        {
+            writer.Write(indent);
+            writer.Write("const char*");
+        }
+
+        public override void VisitPadding(MetaPadding type)
+        {
+            writer.Write(indent);
+            writer.Write("std::array<{0}>", type.Size);
+        }
+
+        public override void VisitStruct(MetaStruct type)
+        {
+            if (topLevelType == null)
+                topLevelType = type;
+
+            writer.WriteLine();
+            writer.WriteLine("struct {0} {{", type.Name);
+            indent += "\t";
+
+            foreach (Field field in type.Fields)
+            {
+                field.Type.Accept(this);
+                writer.WriteLine(" {0};", field.Name);
+            }
+
+            indent = indent.Substring(0, indent.Length - 1);
+            writer.WriteLine("};");
+        }
+
+        public override void VisitArray(MetaArray type)
+        {
+            writer.Write(indent);
+        }
+
+        public override void VisitVarArray(MetaVarArray type)
+        {
+        }
+    }
+}
Index: /OniSplit/Metadata/Field.cs
===================================================================
--- /OniSplit/Metadata/Field.cs	(revision 1114)
+++ /OniSplit/Metadata/Field.cs	(revision 1114)
@@ -0,0 +1,17 @@
+﻿namespace Oni.Metadata
+{
+    internal class Field
+    {
+        private readonly string name;
+        private readonly MetaType type;
+
+        public Field(MetaType type, string name = null)
+        {
+            this.type = type;
+            this.name = name;
+        }
+
+        public MetaType Type => type;
+        public string Name => name;
+    }
+}
Index: /OniSplit/Metadata/IMetaTypeVisitor.cs
===================================================================
--- /OniSplit/Metadata/IMetaTypeVisitor.cs	(revision 1114)
+++ /OniSplit/Metadata/IMetaTypeVisitor.cs	(revision 1114)
@@ -0,0 +1,31 @@
+﻿namespace Oni.Metadata
+{
+    internal interface IMetaTypeVisitor
+    {
+        void VisitStruct(MetaStruct type);
+        void VisitArray(MetaArray type);
+        void VisitVarArray(MetaVarArray type);
+        void VisitEnum(MetaEnum type);
+        void VisitByte(MetaByte type);
+        void VisitInt16(MetaInt16 type);
+        void VisitUInt16(MetaUInt16 type);
+        void VisitInt32(MetaInt32 type);
+        void VisitUInt32(MetaUInt32 type);
+        void VisitInt64(MetaInt64 type);
+        void VisitUInt64(MetaUInt64 type);
+        void VisitFloat(MetaFloat type);
+        void VisitString(MetaString type);
+        void VisitColor(MetaColor type);
+        void VisitVector2(MetaVector2 type);
+        void VisitVector3(MetaVector3 type);
+        void VisitQuaternion(MetaQuaternion type);
+        void VisitMatrix4x3(MetaMatrix4x3 type);
+        void VisitPlane(MetaPlane type);
+        void VisitBoundingSphere(MetaBoundingSphere type);
+        void VisitBoundingBox(MetaBoundingBox type);
+        void VisitPointer(MetaPointer type);
+        void VisitRawOffset(MetaRawOffset type);
+        void VisitSepOffset(MetaSepOffset type);
+        void VisitPadding(MetaPadding type);
+    }
+}
Index: /OniSplit/Metadata/InstanceMetadata.cs
===================================================================
--- /OniSplit/Metadata/InstanceMetadata.cs	(revision 1114)
+++ /OniSplit/Metadata/InstanceMetadata.cs	(revision 1114)
@@ -0,0 +1,3091 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Metadata
+{
+    internal abstract class InstanceMetadata
+    {
+        private static readonly MetaStruct aiSoundConstants = new MetaStruct("AISoundConstants",
+            new Field(MetaType.Byte, "TauntProbability"),
+            new Field(MetaType.Byte, "AlertProbability"),
+            new Field(MetaType.Byte, "StartleProbability"),
+            new Field(MetaType.Byte, "CheckBodyProbability"),
+            new Field(MetaType.Byte, "PursueProbability"),
+            new Field(MetaType.Byte, "CoverProbability"),
+            new Field(MetaType.Byte, "SuperPunchProbability"),
+            new Field(MetaType.Byte, "SuperKickProbability"),
+            new Field(MetaType.Byte, "Super3Probability"),
+            new Field(MetaType.Byte, "Super4Probability"),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.String32, "TauntSound"),
+            new Field(MetaType.String32, "AlertSound"),
+            new Field(MetaType.String32, "StartleSound"),
+            new Field(MetaType.String32, "CheckBodySound"),
+            new Field(MetaType.String32, "PursueSound"),
+            new Field(MetaType.String32, "CoverSound"),
+            new Field(MetaType.String32, "SuperPunchSound"),
+            new Field(MetaType.String32, "SuperKickSound"),
+            new Field(MetaType.String32, "Super3Sound"),
+            new Field(MetaType.String32, "Super4Sound"));
+
+        private static readonly MetaStruct aiVisionConstants = new MetaStruct("AIVisionConstants",
+            new Field(MetaType.Float, "CentralDistance"),
+            new Field(MetaType.Float, "PeripheralDistance"),
+            new Field(MetaType.Float, "VerticalRange"),
+            new Field(MetaType.Float, "CentralRange"),
+            new Field(MetaType.Float, "CentralMax"),
+            new Field(MetaType.Float, "PeripheralRange"),
+            new Field(MetaType.Float, "PeripheralMax"));
+
+        private static readonly MetaStruct aiTargeting = new MetaStruct("AITargeting",
+            // parameters for how we shoot if startled
+            new Field(MetaType.Float, "StartleMissAngle"),
+            new Field(MetaType.Float, "StartleMissDistance"),
+            // target prediction
+            new Field(MetaType.Float, "PredictAmount"),
+            new Field(MetaType.Int32, "PredictPositionDelayFrames"),
+            new Field(MetaType.Int32, "PredictDelayFrames"),
+            new Field(MetaType.Int32, "PredictVelocityFrames"),
+            new Field(MetaType.Int32, "PredictTrendFrames"));
+
+        private static readonly MetaType aiWeaponSkill = new MetaStruct("AIWeaponSkill",
+            new Field(MetaType.Float, "RecoilCompensation"),
+            new Field(MetaType.Float, "BestAimingAngle"),
+            new Field(MetaType.Float, "ShotGroupError"),
+            new Field(MetaType.Float, "ShotGroupDecay"),
+            new Field(MetaType.Float, "ShootingInaccuracyMultiplier"),
+            new Field(MetaType.Int16, "MinShotDelay"),
+            new Field(MetaType.Int16, "MaxShotDelay"));
+
+        [Flags]
+        public enum AIFiringModeFlags : uint
+        {
+            None = 0,
+            NoWildShots = 1,
+        }
+
+        private static readonly MetaStruct aiFiringMode = new MetaStruct("AIFiringMode",
+            new Field(MetaType.Enum<AIFiringModeFlags>(), "Flags"),
+            new Field(MetaType.Matrix4x3, "InverseDirection"),
+            new Field(MetaType.Vector3, "Direction"),
+            new Field(MetaType.Vector3, "Origin"),
+            new Field(MetaType.Float, "PredictionSpeed"),
+            new Field(MetaType.Float, "MaxInaccuracyAngle"),
+            new Field(MetaType.Float, "AimRadius"),
+            new Field(MetaType.Float, "AISoundRadius"),
+            new Field(MetaType.Float, "MinShootingDistance"),
+            new Field(MetaType.Float, "MaxShootingDistance"),
+            new Field(MetaType.Int16, "MaxStartleMisses"),
+            new Field(MetaType.Int16, "SkillIndex"),
+            new Field(MetaType.Int32, "FightTimer"),
+            new Field(MetaType.Float, "ProjectileSpeed"),
+            new Field(MetaType.Float, "ProjectileGravity"),
+            new Field(MetaType.Float, "FireSpreadLength"),
+            new Field(MetaType.Float, "FireSpreadWidth"),
+            new Field(MetaType.Float, "FireSpreadSkew")
+        );
+
+        //
+        // AI Character Setup Array template
+        //
+
+        public enum AISACharacterTeam : ushort
+        {
+            Konoko = 0,
+            TCTF = 1,
+            Syndicate = 2,
+            Neutral = 3,
+            SecurityGuard = 4,
+            RogueKonoko = 5,
+            Switzerland = 6,
+            SyndicateAccessory = 7
+        }
+
+        [Flags]
+        public enum AISACharacterFlags : ushort
+        {
+            None = 0,
+            AI = 0x0001,
+            AutoFreeze = 0x0002,
+            Neutral = 0x0004,
+            TurnGuard = 0x0008
+        }
+
+        private static readonly MetaStruct aisa = new MetaStruct("AISAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("AISACharacter",
+                new Field(MetaType.String32, "Name"),
+                new Field(MetaType.Int16, "ScriptId"),
+                new Field(MetaType.Int16, "FlagId"),
+                new Field(MetaType.Enum<AISACharacterFlags>(), "Flags"),
+                new Field(MetaType.Enum<AISACharacterTeam>(), "Team"),
+                new Field(MetaType.Pointer(TemplateTag.ONCC), "Class"),
+                new Field(MetaType.Padding(36)),
+                new Field(new MetaStruct("CharacterScripts",
+                    new Field(MetaType.String32, "Spawn"),
+                    new Field(MetaType.String32, "Die"),
+                    new Field(MetaType.String32, "Combat"),
+                    new Field(MetaType.String32, "Alarm"),
+                    new Field(MetaType.String32, "Hurt"),
+                    new Field(MetaType.String32, "Defeated"),
+                    new Field(MetaType.String32, "OutOfAmmo"),
+                    new Field(MetaType.String32, "NoPath")), "Scripts"),
+                new Field(MetaType.Pointer(TemplateTag.ONWC), "WeaponClass"),
+                new Field(MetaType.Int16, "Ammo"),
+                new Field(MetaType.Padding(10))
+            )), "Characters")
+        );
+
+        //
+        // Adjacency Array template
+        //
+
+        private static readonly MetaStruct akaa = new MetaStruct("AKAAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("AKAAElement",
+                new Field(MetaType.Int32, "Bnv"),
+                new Field(MetaType.Int32, "Quad"),
+                new Field(MetaType.Padding(4))
+            )), "Elements")
+        );
+
+        //
+        // BSP Tree Node Array template
+        //
+
+        private static readonly MetaStruct abna = new MetaStruct("ABNAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("ABNAElement",
+                new Field(MetaType.Int32, "Quad"),
+                new Field(MetaType.Int32, "Plane"),
+                new Field(MetaType.Int32, "Front"),
+                new Field(MetaType.Int32, "Back")
+            )), "Elements")
+        );
+
+        //
+        // BNV Node Array template
+        //
+
+        private static readonly MetaStruct akva = new MetaStruct("AKVAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("AKVANode",
+                new Field(MetaType.Int32, "BspTree"),
+                new Field(MetaType.Int32, "Id"),
+                new Field(MetaType.Int32, "FirstSide"),
+                new Field(MetaType.Int32, "LastSide"),
+                new Field(MetaType.Int32, "ChildBnv"),
+                new Field(MetaType.Int32, "SiblingBnv"),
+                new Field(MetaType.Padding(4, 0xff)),
+                new Field(MetaType.Int32, "GridXTiles"),
+                new Field(MetaType.Int32, "GridZTiles"),
+                new BinaryPartField(MetaType.RawOffset, "DataOffset", "DataSize"),
+                new Field(MetaType.Int32, "DataSize"),
+                new Field(MetaType.Float, "TileSize"),
+                new Field(MetaType.BoundingBox, "BoundingBox"),
+                new Field(MetaType.Int16, "GridXOffset"),
+                new Field(MetaType.Int16, "GridZOffset"),
+                new Field(MetaType.Int32, "NodeId"),
+                new Field(MetaType.Padding(12)),
+                new Field(MetaType.Int32, "Flags"),
+                new Field(MetaType.Plane, "Floor"),
+                new Field(MetaType.Float, "Height")
+            )), "Nodes")
+        );
+
+        //
+        // Side Array template
+        //
+
+        private static readonly MetaStruct akba = new MetaStruct("AKBAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("AKBASide",
+                new Field(MetaType.Int32, "Plane"),
+                //new Field(MetaType.Padding(4, 0x7f)),
+                new Field(MetaType.Int32, "FirstAdjacency"),
+                new Field(MetaType.Int32, "LastAdjacency"),
+                new Field(MetaType.Padding(16))
+            )), "Sides")
+        );
+
+        //
+        // BSP Node Array template
+        //
+
+        private static readonly MetaStruct akbp = new MetaStruct("AKBPInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("AKBPNode",
+                new Field(MetaType.Int32, "Plane"),
+                new Field(MetaType.Int32, "Back"),
+                new Field(MetaType.Int32, "Front")
+            )), "Nodes")
+        );
+
+        //
+        // Akira Environment template
+        //
+
+        private static readonly MetaStruct akev = new MetaStruct("AKEVInstance",
+            new Field(MetaType.Pointer(TemplateTag.PNTA), "Points"),
+            new Field(MetaType.Pointer(TemplateTag.PLEA), "Planes"),
+            new Field(MetaType.Pointer(TemplateTag.TXCA), "TextureCoordinates"),
+            new Field(MetaType.Pointer(TemplateTag.AGQG), "Quads"),
+            new Field(MetaType.Pointer(TemplateTag.AGQR), "QuadTextures"),
+            new Field(MetaType.Pointer(TemplateTag.AGQC), "QuadCollision"),
+            new Field(MetaType.Pointer(TemplateTag.AGDB), "Debug"),
+            new Field(MetaType.Pointer(TemplateTag.TXMA), "Textures"),
+            new Field(MetaType.Pointer(TemplateTag.AKVA), "BnvNodes"),
+            new Field(MetaType.Pointer(TemplateTag.AKBA), "BnvSides"),
+            new Field(MetaType.Pointer(TemplateTag.IDXA), "QuadGroupList"),
+            new Field(MetaType.Pointer(TemplateTag.IDXA), "QuadGroupId"),
+            new Field(MetaType.Pointer(TemplateTag.AKBP), "BnvBspTree"),
+            new Field(MetaType.Pointer(TemplateTag.ABNA), "TransparencyBspTree"),
+            new Field(MetaType.Pointer(TemplateTag.AKOT), "Octtree"),
+            new Field(MetaType.Pointer(TemplateTag.AKAA), "BnvAdjacency"),
+            new Field(MetaType.Pointer(TemplateTag.AKDA), "DoorFrames"),
+            new Field(MetaType.BoundingBox, "BoundingBox"),
+            new Field(MetaType.Padding(24)),
+            new Field(MetaType.Float, "")
+        );
+
+        //
+        // Gunk Quad Collision Array template
+        //
+
+        private static readonly MetaStruct agqc = new MetaStruct("AGQCInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("AGQCElement",
+                new Field(MetaType.Int32, "Plane"),
+                new Field(MetaType.BoundingBox, "BoundingBox")
+            )), "Elements")
+        );
+
+        //
+        // Gunk Quad General Array template
+        //
+
+        [Flags]
+        private enum AGQGFlags : uint
+        {
+            None = 0,
+
+            DoorFrame = 0x01,
+            Ghost = 0x02,
+            StairsUp = 0x04,
+            StairsDown = 0x08,
+
+            Stairs = 0x10,
+            Transparent = 0x80,
+            TwoSided = 0x0200,
+            NoCollision = 0x0800,
+            Invisible = 0x00002000,
+            NoObjectCollision = 0x4000,
+            NoCharacterCollision = 0x8000,
+            NoOcclusion = 0x010000,
+            Danger = 0x020000,
+            GridIgnore = 0x400000,
+            NoDecals = 0x800000,
+            Furniture = 0x01000000,
+
+            SoundTransparent = 0x08000000,
+            Impassable = 0x10000000,
+
+            Triangle = 0x00000040,
+            Horizontal = 0x00080000,
+            Vertical = 0x00100000,
+
+            ProjectionBit0 = 0x02000000,
+            ProjectionBit1 = 0x04000000,
+        }
+
+        private static readonly MetaStruct agqg = new MetaStruct("AGQGInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("AGQGQuad",
+                new Field(MetaType.Array(4, MetaType.Int32), "Points"),
+                new Field(MetaType.Array(4, MetaType.Int32), "TextureCoordinates"),
+                new Field(MetaType.Array(4, MetaType.Color), "Colors"),
+                new Field(MetaType.Enum<AGQGFlags>(), "Flags"),
+                new Field(MetaType.Int32, "ObjectId")
+            )), "Quads")
+        );
+
+        //
+        // Gunk Quad Render Array template
+        //
+
+        private static readonly MetaStruct agqr = new MetaStruct("AGQRInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("AGQRElement",
+                new Field(MetaType.UInt16, "Texture"),
+                new Field(MetaType.Padding(2))
+            )), "Elements")
+        );
+
+        //
+        // Oct tree template
+        //
+
+        private static readonly MetaStruct akot = new MetaStruct("AKOTInstance",
+            new Field(MetaType.Pointer(TemplateTag.OTIT), "Nodes"),
+            new Field(MetaType.Pointer(TemplateTag.OTLF), "Leafs"),
+            new Field(MetaType.Pointer(TemplateTag.QTNA), "QuadTree"),
+            new Field(MetaType.Pointer(TemplateTag.IDXA), "GunkQuad"),
+            new Field(MetaType.Pointer(TemplateTag.IDXA), "Bnv")
+        );
+
+        //
+        // Oct tree interior node Array template
+        //
+
+        private static readonly MetaStruct otit = new MetaStruct("OTITInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("OTITNode",
+                new Field(MetaType.Array(8, MetaType.Int32), "Children")
+            )), "Nodes")
+        );
+
+        //
+        // Oct tree leaf node Array template
+        //
+
+        private static readonly MetaStruct otlf = new MetaStruct("OTLFInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("OTLFNode",
+                new Field(MetaType.Int32, "PackedGunkQuadList"),
+                new Field(MetaType.Array(6, MetaType.Int32), "Neighbours"),
+                new Field(MetaType.Int32, "PackedPositionAndSize"),
+                new Field(MetaType.Int32, "PackedBnvList")
+            )), "Nodes")
+        );
+
+        //
+        // Quad tree node Array template
+        //
+
+        private static readonly MetaStruct qtna = new MetaStruct("QTNAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("QTNANode",
+                new Field(MetaType.Array(4, MetaType.Int32), "Children")
+            )), "Nodes")
+        );
+
+        //
+        // Env Particle Array template
+        //
+
+        [Flags]
+        internal enum ENVPFlags : ushort
+        {
+            None = 0x00,
+            NotInitiallyCreated = 0x02,
+        }
+
+        private static readonly MetaStruct envp = new MetaStruct("ENVPInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("ENVPParticle",
+                new Field(MetaType.String64, "Class"),
+                new Field(MetaType.String48, "Tag"),
+                new Field(MetaType.Matrix4x3, "Transform"),
+                new Field(MetaType.Vector2, "DecalScale"),
+                new Field(MetaType.Enum<ENVPFlags>(), "Flags"),
+                new Field(MetaType.Padding(38))
+            )), "Particles")
+        );
+
+        //
+        // Geometry template
+        //
+
+        private static readonly MetaStruct m3gm = new MetaStruct("M3GMInstance",
+            new Field(MetaType.Padding(4)),
+            new Field(MetaType.Pointer(TemplateTag.PNTA), "Points"),
+            new Field(MetaType.Pointer(TemplateTag.VCRA), "VertexNormals"),
+            new Field(MetaType.Pointer(TemplateTag.VCRA), "FaceNormals"),
+            new Field(MetaType.Pointer(TemplateTag.TXCA), "TextureCoordinates"),
+            new Field(MetaType.Pointer(TemplateTag.IDXA), "TriangleStrips"),
+            new Field(MetaType.Pointer(TemplateTag.IDXA), "FaceNormalIndices"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Texture"),
+            new Field(MetaType.Padding(4))
+        );
+
+        //
+        // GeometryArray template
+        //
+
+        private static readonly MetaStruct m3ga = new MetaStruct("M3GAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.M3GM)), "Geometries")
+        );
+
+        //
+        // Plane Equation Array template
+        //
+
+        private static readonly MetaStruct plea = new MetaStruct("PLEAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Plane), "Planes")
+        );
+
+        //
+        // 3D Point Array template
+        //
+
+        private static readonly MetaStruct pnta = new MetaStruct("PNTAInstance",
+            new Field(MetaType.Padding(12)),
+            new Field(MetaType.BoundingBox, "BoundingBox"),
+            new Field(MetaType.BoundingSphere, "BoundingSphere"),
+            new Field(MetaType.VarArray(MetaType.Vector3), "Positions")
+        );
+
+        //
+        // Texture Coordinate Array template
+        //
+
+        private static readonly MetaStruct txca = new MetaStruct("TXCAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Vector2), "TexCoords")
+        );
+
+        //
+        // Texture Map Animation template
+        //
+
+        private static readonly MetaStruct txan = new MetaStruct("TXANInstance",
+            new Field(MetaType.Padding(12)),
+            new Field(MetaType.Int16, "Speed"),
+            new Field(MetaType.Int16, ""),
+            new Field(MetaType.Padding(4)),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.TXMP)), "Textures")
+        );
+
+        //
+        // Texture map array template
+        //
+
+        private static readonly MetaStruct txma = new MetaStruct("TXMAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.TXMP)), "Textures")
+        );
+
+        //
+        // Texture Map Big template
+        //
+
+        private static readonly MetaStruct txmb = new MetaStruct("TXMBInstance",
+            new Field(MetaType.Padding(8)),
+            new Field(MetaType.Int16, "Width"),
+            new Field(MetaType.Int16, "Height"),
+            new Field(MetaType.Padding(8)),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.TXMP)), "Textures")
+        );
+
+        //
+        // 3D Vector Array template
+        //
+
+        private static readonly MetaStruct vcra = new MetaStruct("VCRAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Vector3), "Normals")
+        );
+
+        //
+        // Impact template
+        //
+
+        private static readonly MetaStruct impt = new MetaStruct("ImptInstance",
+            new Field(MetaType.Padding(2, 0xff)),
+            new Field(MetaType.Padding(6)),
+            new Field(MetaType.Pointer(TemplateTag.Impt), "ParentImpact")
+        );
+
+        //
+        // Material template
+        //
+
+        private static readonly MetaStruct mtrl = new MetaStruct("MtrlInstance",
+            new Field(MetaType.Padding(2, 0xff)),
+            new Field(MetaType.Padding(6)),
+            new Field(MetaType.Pointer(TemplateTag.Mtrl), "ParentMaterial")
+        );
+
+        //
+        // Console template
+        //
+
+        [Flags]
+        public enum CONSFlags : uint
+        {
+            None = 0x00,
+            AlarmConsole = 0x01
+        }
+
+        private static readonly MetaStruct cons = new MetaStruct("CONSInstance",
+            new Field(MetaType.Enum<CONSFlags>(), "Flags"),
+            new Field(MetaType.Vector3, "Position"),
+            new Field(MetaType.Vector3, "Orientation"),
+            new Field(MetaType.Pointer(TemplateTag.OFGA), "ConsoleGeometry"),
+            new Field(MetaType.Pointer(TemplateTag.M3GM), "ScreenGeometry"),
+            new Field(MetaType.Enum<AGQGFlags>(), "ScreenGunkFlags"),
+            new Field(MetaType.String32, "InactiveTexture"),
+            new Field(MetaType.String32, "ActiveTexture"),
+            new Field(MetaType.String32, "UsedTexture")
+        );
+
+        //
+        // Door template
+        //
+
+        public enum DOORSoundType
+        {
+            None = -1,
+            Unimportant = 0,
+            Interesting = 1,
+            Danger = 2,
+            Melee = 3,
+            Gunfire = 4,
+        }
+
+        public enum DOORSoundAllow : uint
+        {
+            All = 0,
+            Combat = 1,
+            Gunfire = 2,
+            None = 3
+        }
+
+        private static readonly MetaStruct door = new MetaStruct("DOORInstance",
+            new Field(MetaType.Array(2, MetaType.Pointer(TemplateTag.OFGA)), "Geometries"),
+            new Field(MetaType.Pointer(TemplateTag.OBAN), "Animation"),
+            new Field(MetaType.Float, "AISoundAttenuation"),
+            new Field(MetaType.Enum<DOORSoundAllow>(), "AISoundAllow"),
+            new Field(MetaType.Enum<DOORSoundType>(), "AISoundType"),
+            new Field(MetaType.Float, "AISoundDistance"),
+            new Field(MetaType.String32, "OpenSound"),
+            new Field(MetaType.String32, "CloseSound"),
+            new Field(MetaType.Padding(8))
+        );
+
+        //
+        // Object Furn Geom Array template
+        //
+
+        private static readonly MetaStruct ofga = new MetaStruct("OFGAInstance",
+            new Field(MetaType.Padding(16)),
+            new Field(MetaType.Pointer(TemplateTag.ENVP), "EnvParticle"),
+            new Field(MetaType.VarArray(new MetaStruct("OFGAElement",
+                new Field(MetaType.Enum<AGQGFlags>(), "GunkFlags"),
+                new Field(MetaType.Pointer(TemplateTag.M3GM), "Geometry"),
+                new Field(MetaType.Padding(4))
+            //new Field(MetaType.InstanceLink(TemplateTag.OBLS), "LightSource")
+            )), "Elements")
+        );
+
+        private static readonly MetaStruct obls = new MetaStruct("OBLSInstance",
+            new Field(MetaType.Int32, "Type"),
+            new Field(MetaType.Int32, "Flags"),
+            new Field(MetaType.Vector3, "Color"),
+            new Field(MetaType.Float, "Intensity"),
+            new Field(MetaType.Float, "BeamAngle"),
+            new Field(MetaType.Float, "FieldAngle")
+        );
+
+        //
+        // Trigger template
+        //
+
+        private static readonly MetaStruct trig = new MetaStruct("TRIGInstance",
+            new Field(MetaType.Color, "Color"),	// ignored
+            new Field(MetaType.UInt16, "TimeOn"),
+            new Field(MetaType.UInt16, "TimeOff"),
+            new Field(MetaType.Float, "StartOffset"),
+            new Field(MetaType.Float, "AnimScale"),
+            new Field(MetaType.Pointer(TemplateTag.M3GM), "BaseGeometry"),
+            new Field(MetaType.Padding(4)), // ls data
+            new Field(MetaType.Enum<AGQGFlags>(), "BaseGunkFlags"),
+            new Field(MetaType.Pointer(TemplateTag.TRGE), "Emitter"),
+            new Field(MetaType.Pointer(TemplateTag.OBAN), "Animation"),
+            new Field(MetaType.String32, "ActiveSound"),
+            new Field(MetaType.String32, "TriggerSound"),
+            new Field(MetaType.Padding(8))
+        );
+
+        //
+        // Trigger Emitter template
+        //
+
+        private static readonly MetaStruct trge = new MetaStruct("TRGEInstance",
+            new Field(MetaType.Vector3, "Position"),
+            new Field(MetaType.Vector3, "Direction"),
+            new Field(MetaType.Pointer(TemplateTag.M3GM), "Geometry"),
+            new Field(MetaType.Enum<AGQGFlags>(), "GunkFlags")
+        );
+
+        //
+        // Turret template
+        //
+
+        private static readonly MetaStruct turr = new MetaStruct("TURRInstance",
+            new Field(MetaType.String32, "Name"),
+            new Field(MetaType.Padding(32)), // base name
+            new Field(MetaType.Padding(14)), // flags, free time, reload time, barrel count, recoil anim type, reload anim type, max ammo
+            new Field(MetaType.Int16, "ParticleCount"), // attachment count
+            new Field(MetaType.Int16, ""), // shooter count
+            new Field(MetaType.Padding(6)), // pad, aiming speed
+            new Field(MetaType.Pointer(TemplateTag.M3GM), "BaseGeometry"),
+            new Field(MetaType.Padding(4)), // ls data
+            new Field(MetaType.Enum<AGQGFlags>(), "BaseGunkFlags"),
+            new Field(MetaType.Pointer(TemplateTag.M3GM), "ArmGeometry"),
+            new Field(MetaType.Enum<AGQGFlags>(), "ArmGunkFlags"),
+            new Field(MetaType.Pointer(TemplateTag.M3GM), "WeaponGeometry"),
+            new Field(MetaType.Enum<AGQGFlags>(), "WeaponGunkFlags"),
+            new Field(MetaType.Vector3, "ArmTranslation"),
+            new Field(MetaType.Vector3, "WeaponTranslation"),
+            new Field(MetaType.Array(16, new MetaStruct("TURRParticle",
+                new Field(MetaType.String16, "ParticleClass"),
+                new Field(MetaType.Padding(4)),
+                new Field(MetaType.Int16, "ShotFrequency"),
+                new Field(MetaType.Int16, "FiringModeOwner"),
+                new Field(MetaType.Matrix4x3, "Transform"),
+                new Field(MetaType.Padding(4))
+            )), "Particles"),
+            new Field(aiFiringMode, "FiringMode"),
+            new Field(aiTargeting, "Targeting"),
+            new Field(aiWeaponSkill, "WeaponSkill"),
+            new Field(MetaType.Int32, "Timeout"),
+            new Field(MetaType.Float, "MinElevation"),
+            new Field(MetaType.Float, "MaxElevation"),
+            new Field(MetaType.Float, "MinAzimuth"),
+            new Field(MetaType.Float, "MaxAzimuth"),
+            new Field(MetaType.Float, "MaxVerticalSpeed"),
+            new Field(MetaType.Float, "MaxHorizontalSpeed"),
+            new Field(MetaType.String32, "ActiveSound"),
+            new Field(MetaType.Padding(4))
+        );
+
+        //
+        // Object animation template
+        //
+
+        [Flags]
+        private enum OBANFlags : uint
+        {
+            None = 0x00,
+            NormalLoop = 0x01,
+            BackToBackLoop = 0x02,
+            RandomStartFrame = 0x04,
+            Autostart = 0x08,
+            ZAxisUp = 0x10
+        }
+
+        private static readonly MetaStruct oban = new MetaStruct("OBANInstance",
+            new Field(MetaType.Padding(12)),
+            new Field(MetaType.Enum<OBANFlags>(), "Flags"),
+            new Field(MetaType.Matrix4x3, "InitialTransform"),
+            new Field(MetaType.Matrix4x3, "BaseTransform"),
+            new Field(MetaType.Int16, "FrameLength"),
+            new Field(MetaType.Int16, "FrameCount"),
+            new Field(MetaType.Int16, "HalfStopFrame"),
+            new Field(MetaType.ShortVarArray(new MetaStruct("OBANKeyFrame",
+                new Field(MetaType.Quaternion, "Rotation"),
+                new Field(MetaType.Vector3, "Translation"),
+                new Field(MetaType.Int32, "Time")
+            )), "KeyFrames")
+        );
+
+        [Flags]
+        public enum OBOAFlags : uint
+        {
+            None = 0,
+            InUse = 0x0200,
+            NoCollision = 0x0400,
+            NoGravity = 0x0800,
+            FaceCollision = 0x1000,
+        }
+
+        public enum OBOAPhysics : uint
+        {
+            None = 0,
+            Static = 1,
+            Linear = 2,
+            Animated = 3,
+            Newton = 4
+        }
+
+        //
+        // Starting Object Array template
+        //
+
+        private static readonly MetaStruct oboa = new MetaStruct("OBOAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("OBOAObject",
+                new Field(MetaType.Pointer(TemplateTag.M3GA), "Geometry"),
+                new Field(MetaType.Pointer(TemplateTag.OBAN), "Animation"),
+                new Field(MetaType.Pointer(TemplateTag.ENVP), "Particle"),
+                new Field(MetaType.Enum<OBOAFlags>(), "Flags"),
+                new Field(MetaType.Int32, "DoorGunkId"),
+                new Field(MetaType.Int32, "DoorId"),
+                new Field(MetaType.Enum<OBOAPhysics>(), "PhysicsType"),
+                new Field(MetaType.Int32, "ScriptId"),
+                new Field(MetaType.Vector3, "Position"),
+                new Field(MetaType.Quaternion, "Rotation"),
+                new Field(MetaType.Float, "Scale"),
+                new Field(MetaType.Matrix4x3, "Transform"),
+                new Field(MetaType.String64, "Name"),
+                new Field(MetaType.Padding(64))
+            )), "Objects")
+        );
+
+        //
+        // Character Body Part Impacts template
+        //
+
+        private static readonly MetaStruct cbpi = new MetaStruct("CBPIInstance",
+            new Field(MetaType.Array(19, MetaType.Pointer(TemplateTag.Impt)), "HitImpacts"),
+            new Field(MetaType.Array(19, MetaType.Pointer(TemplateTag.Impt)), "BlockedImpacts"),
+            new Field(MetaType.Array(19, MetaType.Pointer(TemplateTag.Impt)), "KilledImpacts")
+        );
+
+        //
+        // Character Body Part Material template
+        //
+
+        private static readonly MetaStruct cbpm = new MetaStruct("CBPMInstance",
+            new Field(MetaType.Array(19, MetaType.Pointer(TemplateTag.Mtrl)), "Materials")
+        );
+
+        //
+        // Oni Character Class template
+        //
+
+        [Flags]
+        public enum AICharacterFlags : uint
+        {
+            None = 0x00,
+            NoStartleAnim = 0x01,
+            EnableMeleeFireDodge = 0x02,
+            ShootDodge = 0x04,
+            RunAwayDodge = 0x08,
+            NotUsed = 0x10
+        }
+
+        private static readonly MetaStruct oncc = new MetaStruct("ONCCInstance",
+            new Field(new MetaStruct("ONCCAirConstants",
+                new Field(MetaType.Float, "FallGravity"),
+                new Field(MetaType.Float, "JumpGravity"),
+                new Field(MetaType.Float, "JumpStartVelocity"),
+                new Field(MetaType.Float, "MaxVelocity"),
+                new Field(MetaType.Float, "JetpackAcceleration"),
+                new Field(MetaType.UInt16, "FramesFallGravity"),
+                new Field(MetaType.UInt16, "JetpackTimer"),
+                new Field(MetaType.Float, "MaxNoDamageFallingHeight"),
+                new Field(MetaType.Float, "MaxDamageFallingHeight")),
+                "AirConstants"),
+
+            new Field(new MetaStruct("ONCCShadowConstants",
+                new Field(MetaType.Pointer(TemplateTag.TXMP), "Texture"),
+                new Field(MetaType.Float, "MaxHeight"),
+                new Field(MetaType.Float, "FadeHeight"),
+                new Field(MetaType.Float, "SizeMax"),
+                new Field(MetaType.Float, "SizeFade"),
+                new Field(MetaType.Float, "SizeMin"),
+                new Field(MetaType.Int16, "AlphaMax"),
+                new Field(MetaType.Int16, "AlphaFade")),
+                "ShadowConstants"),
+
+            new Field(new MetaStruct("ONCCJumpConstants",
+                new Field(MetaType.Float, "JumpDistance"),
+                new Field(MetaType.Byte, "JumpHeight"),
+                new Field(MetaType.Byte, "JumpDistanceSquares"),
+                new Field(MetaType.Padding(2))),
+                "JumpConstants"),
+
+            new Field(new MetaStruct("ONCCCoverConstants",
+                new Field(MetaType.Float, "RayIncrement"),
+                new Field(MetaType.Float, "RayMax"),
+                new Field(MetaType.Float, "RayAngle"),
+                new Field(MetaType.Float, "RayAngleMax")),
+                "CoverConstants"),
+
+            new Field(new MetaStruct("ONCCAutoFreezeConstants",
+                new Field(MetaType.Float, "DistanceXZ"),
+                new Field(MetaType.Float, "DistanceY")),
+                "AutoFreezeConstants"),
+
+            new Field(new MetaStruct("ONCCInventoryConstants",
+                new Field(MetaType.Int16, "HypoRegenerationRate"),
+                new Field(MetaType.Padding(2))),
+                "InventoryConstants"),
+
+            new Field(MetaType.Array(5, MetaType.Float), "LODConstants"),
+
+            new Field(new MetaStruct("ONCCHurtSoundConstants",
+                new Field(MetaType.Int16, "BasePercentage"),
+                new Field(MetaType.Int16, "MaxPercentage"),
+                new Field(MetaType.Int16, "PercentageThreshold"),
+                new Field(MetaType.Int16, "Timer"),
+                new Field(MetaType.Int16, "MinTimer"),
+                new Field(MetaType.Int16, "MaxLight"),
+                new Field(MetaType.Int16, "MaxMedium"),
+                new Field(MetaType.Int16, "DeathChance"),
+                new Field(MetaType.Int16, "VolumeTreshold"),
+                new Field(MetaType.Int16, "MediumTreshold"),
+                new Field(MetaType.Int16, "HeavyTreshold"),
+                new Field(MetaType.Padding(2)),
+                new Field(MetaType.Float, "MinVolume"),
+                new Field(MetaType.String32, "LightSound"),
+                new Field(MetaType.String32, "MediumSound"),
+                new Field(MetaType.String32, "HeavySound"),
+                new Field(MetaType.String32, "DeathSound"),
+                new Field(MetaType.Padding(16))),
+                "HurtSoundConstants"),
+
+            new Field(new MetaStruct("ONCCAIConstants",
+                new Field(MetaType.Enum<AICharacterFlags>(), "Flags"),
+                new Field(MetaType.Float, "RotationSpeed"), // turning_nimbleness
+                new Field(MetaType.UInt16, "DazedMinFrames"),
+                new Field(MetaType.UInt16, "DazedMaxFrames"),
+                new Field(MetaType.Int32, "DodgeReactFrames"),
+                new Field(MetaType.Float, "DodgeTimeScale"),
+                new Field(MetaType.Float, "DodgeWeightScale"),
+                new Field(aiTargeting, "Targeting"),
+                new Field(MetaType.Array(13, aiWeaponSkill), "WeaponSkills"),
+                new Field(MetaType.Int32, "DeadMakeSureDelay"),
+                new Field(MetaType.Int32, "InvestigateBodyDelay"),
+                new Field(MetaType.Int32, "LostContactDelay"),
+                new Field(MetaType.Int32, "DeadTauntChance"),
+                new Field(MetaType.Int32, "GoForGunChance"),
+                new Field(MetaType.Int32, "RunPickupChance"),
+                new Field(MetaType.Int16, "CombatId"),
+                new Field(MetaType.Int16, "MeleeId"),
+                new Field(aiSoundConstants, "SoundConstants"),
+                new Field(aiVisionConstants, "VisionConstants"),
+                new Field(MetaType.Int32, "HostileThreatDefiniteTimer"),
+                new Field(MetaType.Int32, "HostileThreatStrongTimer"),
+                new Field(MetaType.Int32, "HostileThreatWeakTimer"),
+                new Field(MetaType.Int32, "FriendlyThreatDefiniteTimer"),
+                new Field(MetaType.Int32, "FriendlyThreatStrongTimer"),
+                new Field(MetaType.Int32, "FriendlyThreatWeakTimer"),
+                new Field(MetaType.Float, "EarshotRadius")),
+                "AIConstants"),
+
+            new Field(MetaType.Pointer(TemplateTag.ONCV), "Variant"),
+            new Field(MetaType.Pointer(TemplateTag.ONCP), "Particles"),
+            new Field(MetaType.Pointer(TemplateTag.ONIA), "Impacts"),
+            new Field(MetaType.Padding(4)),
+            new Field(MetaType.String16, "ImpactModifierName"),
+
+            new Field(MetaType.Array(15, new MetaStruct("ONCCImpact",
+                new Field(MetaType.String128, "Name"),
+                new Field(MetaType.Padding(2, 0xff))
+            )), "Impacts"),
+
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.String64, "DeathParticle"),
+
+            new Field(MetaType.Padding(8)),
+            new Field(MetaType.Pointer(TemplateTag.TRBS), "BodySet"),
+            new Field(MetaType.Pointer(TemplateTag.TRMA), "BodyTextures"),
+            new Field(MetaType.Pointer(TemplateTag.CBPM), "BodyMaterials"),
+            new Field(MetaType.Pointer(TemplateTag.CBPI), "BodyImpacts"),
+            new Field(MetaType.Int32, "FightModeTimer"),
+            new Field(MetaType.Int32, "IdleAnimation1Timer"),
+            new Field(MetaType.Int32, "IdleAnimation2Timer"),
+            new Field(MetaType.Int32, "Health"),
+            new Field(MetaType.Enum<TRAMBoneFlags>(), "FeetBones"),
+
+            new Field(MetaType.Float, "MinBodySizeFactor"),
+            new Field(MetaType.Float, "MaxBodySizeFactor"),
+            new Field(MetaType.Array(7, MetaType.Float), "DamageFactors"),
+            new Field(MetaType.Float, "BossShieldProtectAmount"),
+
+            new Field(MetaType.Pointer(TemplateTag.TRAC), "Animations"),
+            new Field(MetaType.Pointer(TemplateTag.TRSC), "AimingScreens"),
+
+            new Field(MetaType.UInt16, "AIRateOfFire"),
+            new Field(MetaType.UInt16, "DeathDeleteDelay"),
+            new Field(MetaType.Byte, "WeaponHand"),
+            new Field(MetaType.Byte, "HasDaodanPowers"),
+            new Field(MetaType.Byte, "HasSupershield"),
+            new Field(MetaType.Byte, "CantTouchThis")
+        );
+
+        //
+        // Oni Character Impact Array template
+        //
+
+        private static readonly MetaStruct onia = new MetaStruct("ONIAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("ONIAImpact",
+                new Field(MetaType.String16, "Name"),
+                new Field(MetaType.String128, "Type"),
+                new Field(MetaType.String16, "Modifier"),
+                new Field(MetaType.Padding(2, 0xff)),
+                new Field(MetaType.Padding(2))
+            )), "Impacts")
+        );
+
+        //
+        // Oni Character Particle Array template
+        //
+
+        public enum ONCPBodyPart : ushort
+        {
+            Pelvis,
+            LeftThigh,
+            LeftCalf,
+            LeftFoot,
+            RightThigh,
+            RightCalf,
+            RightFoot,
+            Mid,
+            Chest,
+            Neck,
+            Head,
+            LeftShoulder,
+            LeftArm,
+            LeftWrist,
+            LeftFist,
+            RightShoulder,
+            RightArm,
+            RightWrist,
+            RightFist,
+            KillImpact = 0x8000,
+            None = 0xffff
+        }
+
+        private static readonly MetaStruct oncp = new MetaStruct("ONCPInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("ONCPParticle",
+                new Field(MetaType.String16, "Name"),
+                new Field(MetaType.String64, "Type"),
+                new Field(MetaType.Enum<ONCPBodyPart>(), "BodyPart"),
+                new Field(MetaType.Padding(1, 0x5f)),
+                new Field(MetaType.Padding(5))
+            )), "Particles")
+        );
+
+        //
+        // Oni Character Variant template
+        //
+
+        private static readonly MetaStruct oncv = new MetaStruct("ONCVInstance",
+            new Field(MetaType.Pointer(TemplateTag.ONCV), "ParentVariant"),
+            new Field(MetaType.String32, "CharacterClass"),
+            new Field(MetaType.String32, "CharacterClassHard")
+        );
+
+        //
+        // Corpse Array template
+        //
+
+        private static readonly MetaStruct crsa = new MetaStruct("CRSAInstance",
+            new Field(MetaType.Padding(12)),
+            new Field(MetaType.Int32, "FixedCount"),
+            new Field(MetaType.Int32, "UsedCount"),
+            new Field(MetaType.VarArray(new MetaStruct("CRSACorpse",
+                new Field(MetaType.Padding(160)),
+                new Field(MetaType.Pointer(TemplateTag.ONCC), "CharacterClass"),
+                new Field(MetaType.Array(19, MetaType.Matrix4x3), "Transforms"),
+                new Field(MetaType.BoundingBox, "BoundingBox")
+            )), "Corpses")
+        );
+
+        //
+        // Diary Page template
+        //
+
+        private static readonly MetaStruct dpge = new MetaStruct("DPgeInstance",
+            new Field(MetaType.Int16, "LevelNumber"),
+            new Field(MetaType.Int16, "PageNumber"),
+            new Field(MetaType.Byte, "IsLearnedMove"),
+            new Field(MetaType.Padding(3)),
+            new Field(MetaType.Padding(48)),
+            new Field(MetaType.Pointer(TemplateTag.IGPG), "Page")
+        );
+
+        //
+        // Film template
+        //
+
+        [Flags]
+        public enum FILMKeys : ulong
+        {
+            None = 0x00,
+            Escape = 0x01,
+            Console = 0x02,
+            Pause = 0x04,
+            Cutscene1 = 0x08,
+            Cutscene2 = 0x10,
+            F4 = 0x20,
+            F5 = 0x40,
+            F6 = 0x80,
+            F7 = 0x0100,
+            F8 = 0x0200,
+            StartRecord = 0x0400,
+            StopRecord = 0x0800,
+            PlayRecord = 0x1000,
+            F12 = 0x2000,
+            LookMode = 0x8000,
+            Screenshot = 0x010000,
+            Forward = 0x200000,
+            Backward = 0x400000,
+            TurnLeft = 0x800000,
+            TurnRight = 0x01000000,
+            StepLeft = 0x02000000,
+            StepRight = 0x04000000,
+            Jump = 0x08000000,
+            Crouch = 0x10000000,
+            Punch = 0x20000000,
+            Kick = 0x40000000,
+            Block = 0x80000000,
+            Walk = 0x0100000000,
+            Action = 0x0200000000,
+            Hypo = 0x0400000000,
+            Reload = 0x0800000000,
+            Swap = 0x1000000000,
+            Drop = 0x2000000000,
+            Fire1 = 0x4000000000,
+            Fire2 = 0x8000000000,
+            Fire3 = 0x010000000000
+        }
+
+        private static readonly MetaStruct film = new MetaStruct("FILMInstance",
+            new Field(MetaType.Vector3, "Position"),
+            new Field(MetaType.Float, "Facing"),
+            new Field(MetaType.Float, "DesiredFacing"),
+            new Field(MetaType.Float, "HeadFacing"),
+            new Field(MetaType.Float, "HeadPitch"),
+            new Field(MetaType.Int32, "FrameCount"),
+            new Field(MetaType.Array(2, MetaType.Pointer(TemplateTag.TRAM)), "Animations"),
+            new Field(MetaType.Padding(12)),
+            new Field(MetaType.VarArray(new MetaStruct("Frame",
+                new Field(MetaType.Vector2, "MouseDelta"),
+                new Field(MetaType.Enum<FILMKeys>(), "Keys"),
+                new Field(MetaType.Int32, "Frame"),
+                new Field(MetaType.Padding(4))
+            )), "Frames")
+        );
+
+        //
+        // Oni Game Settings template
+        //
+
+        private static readonly MetaStruct ongs = new MetaStruct("ONGSInstance",
+            new Field(MetaType.Float, "MaxOverhealthFactor"),
+            new Field(MetaType.Float, "NormalHypoStrength"),
+            new Field(MetaType.Float, "OverhealthHypoStrength"),
+            new Field(MetaType.Float, "OverhealthMinDamage"),
+            new Field(MetaType.Float, "OverhealthMaxDamage"),
+
+            new Field(MetaType.Int32, "UsedHealthElements"),
+            new Field(MetaType.Array(16, MetaType.Float), "HealthPercent"),
+            new Field(MetaType.Array(16, MetaType.Color), "HealthColor"),
+
+            new Field(MetaType.Array(6, MetaType.String128), "PowerupModels"),
+            new Field(MetaType.Padding(128)),
+            new Field(MetaType.Array(6, MetaType.String128), "PowerupGlowTextures"),
+            new Field(MetaType.Padding(128)),
+            new Field(MetaType.Array(6, MetaType.Vector2), "PowerupGlowSizes"),
+            new Field(MetaType.Padding(8)),
+
+            new Field(MetaType.Array(23, MetaType.String32), "Sounds"),
+
+            new Field(MetaType.Array(3, MetaType.Float), "NoticeFactors"),
+            new Field(MetaType.Array(3, MetaType.Float), "BlockChanceFactors"),
+            new Field(MetaType.Array(3, MetaType.Float), "DodgeFactors"),
+            new Field(MetaType.Array(3, MetaType.Float), "WeaponAccuracyFactors"),
+            new Field(MetaType.Array(3, MetaType.Float), "EnemyHealthFactors"),
+            new Field(MetaType.Array(3, MetaType.Float), "PlayerHealthFactors"),
+
+            new Field(MetaType.Int32, "UsedAutoPrompts"),
+            new Field(MetaType.Array(16, new MetaStruct("ONGSAutoPrompt",
+                new Field(MetaType.String32, "Notes"),
+                new Field(MetaType.Int16, "FirstAutopromptLevel"),
+                new Field(MetaType.Int16, "LastAutopromptLevel"),
+                new Field(MetaType.String32, "SubtitleName")
+            )), "AutoPrompts")
+        );
+
+        //
+        // Help Page template
+        //
+
+        private static readonly MetaStruct hpge = new MetaStruct("HPgeInstance",
+            new Field(MetaType.Padding(4)),
+            new Field(MetaType.Pointer(TemplateTag.IGPG), "Page")
+        );
+
+        //
+        // IGUI HUD Help template
+        //
+
+        private static readonly MetaStruct ighh = new MetaStruct("IGHHInstance",
+            new Field(MetaType.Padding(28)),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "LeftTexture"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "RightTexture"),
+            new Field(MetaType.Int16, "LeftX"),
+            new Field(MetaType.Int16, "LeftY"),
+            new Field(MetaType.Int16, "RightX"),
+            new Field(MetaType.Int16, "RightY"),
+            new Field(MetaType.Int32, "LeftCount"),
+            new Field(MetaType.Int32, "RightCount"),
+            new Field(MetaType.VarArray(new MetaStruct("IGHHLabels",
+                new Field(MetaType.String64, "Text"),
+                new Field(MetaType.Int16, "X"),
+                new Field(MetaType.Int16, "Y")
+            )), "Labels")
+        );
+
+        //
+        // IGUI Page template
+        //
+
+        private enum IGPGFontStyle : uint
+        {
+            Normal = 0,
+            Bold = 1,
+            Italic = 2
+        }
+
+        [Flags]
+        private enum IGPGFlags : ushort
+        {
+            None = 0x00,
+            Family = 0x01,
+            Style = 0x02,
+            Color = 0x04,
+            Size = 0x08
+        }
+
+        private static readonly MetaStruct igpg = new MetaStruct("IGPGInstance",
+            new Field(new MetaStruct("IGPGFont",
+                new Field(MetaType.Pointer(TemplateTag.TSFF), "Family"),
+                new Field(MetaType.Enum<IGPGFontStyle>(), "Style"),
+                new Field(MetaType.Color, "Color"),
+                new Field(MetaType.UInt16, "Size"),
+                new Field(MetaType.Enum<IGPGFlags>(), "Flags")),
+                "Font"),
+            new Field(MetaType.Pointer(TemplateTag.NONE), "Image"),
+            new Field(MetaType.Pointer(TemplateTag.IGSA), "Text1"),
+            new Field(MetaType.Pointer(TemplateTag.IGSA), "Text2")
+        );
+
+        //
+        // IGUI Page Array template
+        //
+
+        private static readonly MetaStruct igpa = new MetaStruct("IGPAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.IGPG)), "Pages")
+        );
+
+        //
+        // IGUI String template
+        //
+
+        private enum IGStFontStyle : uint
+        {
+            Normal = 0,
+            Bold = 1,
+            Italic = 2
+        }
+
+        [Flags]
+        private enum IGStFlags : ushort
+        {
+            None = 0x00,
+            Family = 0x01,
+            Style = 0x02,
+            Color = 0x04,
+            Size = 0x08
+        }
+
+        private static readonly MetaStruct igst = new MetaStruct("IGStInstance",
+            new Field(new MetaStruct("IGStFont",
+                new Field(MetaType.Pointer(TemplateTag.TSFF), "Family"),
+                new Field(MetaType.Enum<IGStFontStyle>(), "Style"),
+                new Field(MetaType.Color, "Color"),
+                new Field(MetaType.Int16, "Size"),
+                new Field(MetaType.Enum<IGStFlags>(), "Flags")),
+                "Font"),
+            new Field(MetaType.String(384), "Text")
+        );
+
+        //
+        // IGUI String Array template
+        //
+
+        private static readonly MetaStruct igsa = new MetaStruct("IGSAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.IGSt)), "Strings")
+        );
+
+        //
+        // Item Page template
+        //
+
+        private static readonly MetaStruct ipge = new MetaStruct("IPgeInstance",
+            new Field(MetaType.Int32, "PageNumber"),
+            new Field(MetaType.Pointer(TemplateTag.IGPG), "Page")
+        );
+
+        //
+        // Key Icons template
+        //
+
+        private static readonly MetaStruct keyi = new MetaStruct("KeyIInstance",
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Punch"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Kick"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Forward"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Backward"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Left"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Right"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Crouch"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Jump"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Hold"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Plus")
+        );
+
+        //
+        // Oni Game Level template
+        //
+
+        private static readonly MetaStruct onlv = new MetaStruct("ONLVInstance",
+            new Field(MetaType.String64, "Name"),
+            new Field(MetaType.Pointer(TemplateTag.AKEV), "Environment"),
+            new Field(MetaType.Pointer(TemplateTag.OBOA), "Objects"),
+            new Field(MetaType.Padding(12)),
+            new Field(MetaType.Pointer(TemplateTag.ONSK), "SkyBox"),
+            new Field(MetaType.Padding(4)),
+            new Field(MetaType.Pointer(TemplateTag.AISA), "Characters"),
+            new Field(MetaType.Padding(12)),
+            new Field(MetaType.Pointer(TemplateTag.ONOA), "ObjectQuadMap"),
+            new Field(MetaType.Pointer(TemplateTag.ENVP), "Particles"),
+            new Field(MetaType.Padding(644)),
+            new Field(MetaType.Pointer(TemplateTag.CRSA), "Corpses")
+        );
+
+        //
+        // Oni Game Level Descriptor template
+        //
+
+        private static readonly MetaStruct onld = new MetaStruct("ONLDInstance",
+            new Field(MetaType.Int16, "LevelNumber"),
+            new Field(MetaType.Int16, "NextLevelNumber"),
+            new Field(MetaType.String64, "DisplayName")
+        );
+
+        //
+        // Object Gunk Array template
+        //
+
+        private static readonly MetaStruct onoa = new MetaStruct("ONOAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("ONOAElement",
+                new Field(MetaType.Int32, "ObjectId"),
+                new Field(MetaType.Pointer(TemplateTag.IDXA), "QuadList")
+            )), "Elements")
+        );
+
+        //
+        // Objective Page template
+        //
+
+        private static readonly MetaStruct opge = new MetaStruct("OPgeInstance",
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.UInt16, "LevelNumber"),
+            new Field(MetaType.Pointer(TemplateTag.IGPA), "Pages")
+        );
+
+        //
+        // Oni Sky class template
+        //
+
+        private static readonly MetaStruct onsk = new MetaStruct("ONSKInstance",
+            new Field(MetaType.Array(6, MetaType.Pointer(TemplateTag.TXMP)), "SkyboxTextures"),
+            new Field(MetaType.Array(8, MetaType.Pointer(TemplateTag.TXMP)), "Planets"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "SunFlare"),
+            new Field(MetaType.Array(5, MetaType.Pointer(TemplateTag.TXMP)), "Stars"),
+            new Field(MetaType.Int32, "PlanetCount"),
+            new Field(MetaType.Int32, "NoSunFlare"),
+            new Field(MetaType.Array(8, MetaType.Float), "PlanetWidths"),
+            new Field(MetaType.Array(8, MetaType.Float), "PlanetHeights"),
+            new Field(MetaType.Array(8, MetaType.Float), "PlanetElevations"),
+            new Field(MetaType.Array(8, MetaType.Float), "PlanetAzimuths"),
+            new Field(MetaType.Float, "SunFlareSize"),
+            new Field(MetaType.Float, "SunFlareIntensity"),
+            new Field(MetaType.Int32, "StarCount"),
+            new Field(MetaType.Int32, "RandomSeed"),
+            new Field(MetaType.Int32, "")
+        );
+
+        //
+        // Text Console template
+        //
+
+        private static readonly MetaStruct txtc = new MetaStruct("TxtCInstance",
+            new Field(MetaType.Pointer(TemplateTag.IGPA), "Pages")
+        );
+
+        //
+        // Oni Variant List template
+        //
+
+        private static readonly MetaStruct onvl = new MetaStruct("ONVLInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.ONCV)), "Variants")
+        );
+
+        //
+        // Weapon Page template
+        //
+
+        private static readonly MetaStruct wpge = new MetaStruct("WPgeInstance",
+            new Field(MetaType.Pointer(TemplateTag.ONWC), "WeaponClass"),
+            new Field(MetaType.Pointer(TemplateTag.IGPG), "Page")
+        );
+
+        //
+        // Part Specification template
+        //
+
+        private static readonly MetaStruct pspc = new MetaStruct("PSpcInstance",
+            new Field(MetaType.Array(9, new MetaStruct("PSpcPoint",
+                new Field(MetaType.Int16, "X"),
+                new Field(MetaType.Int16, "Y")
+            )), "LeftTop"),
+            new Field(MetaType.Array(9, new MetaStruct("PSpcPoint",
+                new Field(MetaType.Int16, "X"),
+                new Field(MetaType.Int16, "Y")
+            )), "RightBottom"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Texture")
+        );
+
+        //
+        // Part Specification List template
+        //
+
+        private enum PSpLType : uint
+        {
+            OutOfGameBackground = 0,
+            InGameBackground = 1,
+            SoundDebugPanelBackground = 5
+        }
+
+        private static readonly MetaStruct pspl = new MetaStruct("PSpLInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("PSpLElement",
+                new Field(MetaType.Enum<PSpLType>(), "Type"),
+                new Field(MetaType.Pointer(TemplateTag.PSpc), "Part")
+            )), "Elements")
+        );
+
+        //
+        // Part Specifications UI template
+        //
+
+        private static readonly MetaStruct psui = new MetaStruct("PSUIInstance",
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Background"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Border"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Title"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Grow"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "CloseIdle"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ClosePressed"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ZoomIdle"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ZoomPressed"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "FlattenIdle"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "FlattenPressed"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "TextCaret"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Outline"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Button"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ButtonOff"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ButtonOn"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "CheckBoxOn"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "CheckBoxOff"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "EditField"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "EditFieldFocused"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "EditFieldHighlighted"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Divider"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Check"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "PopupMenu"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ProgressBarTrack"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ProgressBarFill"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "RadioButtonOn"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "RadioButtonOff"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrowUpIdle"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrowUpPressed"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrowDownIdle"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrowDownPressed"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarVerticalTrack"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrowLeftIdle"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrowLeftPressed"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrowRightIdle"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarArrorRightPressed"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarHorizontalTrack"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "ScrollBarThumb"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "SliderThumb"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "SliderTrack"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Background2"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Background3"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "File"),
+            new Field(MetaType.Pointer(TemplateTag.PSpc), "Folder")
+        );
+
+        //
+        // Subtitle Array template
+        //
+
+        private static readonly MetaStruct subt = new MetaStruct("SUBTInstance",
+            new Field(MetaType.Padding(16)),
+            new BinaryPartField(MetaType.RawOffset, "DataOffset"),
+            new Field(MetaType.VarArray(MetaType.Int32), "Elements")
+        );
+
+        //
+        // Index Array template
+        //
+
+        private static readonly MetaStruct idxa = new MetaStruct("IDXAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(MetaType.Int32), "Indices")
+        );
+
+        //
+        // Totoro Aiming Screen template
+        //
+
+        private static readonly MetaStruct tras = new MetaStruct("TRASInstance",
+            new Field(MetaType.Pointer(TemplateTag.TRAM), "Animation"),
+            new Field(MetaType.Float, "LeftStep"),
+            new Field(MetaType.Float, "RightStep"),
+            new Field(MetaType.UInt16, "LeftFrames"),
+            new Field(MetaType.UInt16, "RightFrames"),
+            new Field(MetaType.Float, "DownStep"),
+            new Field(MetaType.Float, "UpStep"),
+            new Field(MetaType.UInt16, "DownFrames"),
+            new Field(MetaType.UInt16, "UpFrames")
+        );
+
+        //
+        // Totoro Animation Sequence template
+        //
+
+        public enum TRAMType : ushort
+        {
+            None,
+            Anything,
+            Walk,
+            Run,
+            Slide,
+            Jump,
+            Stand,
+            StandingTurnLeft,
+            StandingTurnRight,
+            RunBackwards,
+            RunSidestepLeft,
+            RunSidestepRight,
+            Kick,
+            WalkSidestepLeft,
+            WalkSidestepRight,
+            WalkBackwards,
+            Stance,
+            Crouch,
+            JumpForward,
+            JumpBackward,
+            JumpLeft,
+            JumpRight,
+            Punch,
+            Block,
+            Land,
+            Fly,
+            KickForward,
+            KickLeft,
+            KickRight,
+            KickBack,
+            KickLow,
+            PunchForward,
+            PunchLeft,
+            PunchRight,
+            PunchBack,
+            PunchLow,
+            Kick2,
+            Kick3,
+            Punch2,
+            Punch3,
+            LandForward,
+            LandRight,
+            LandLeft,
+            LandBack,
+            PPK,
+            PKK,
+            PKP,
+            KPK,
+            KPP,
+            KKP,
+            PK,
+            KP,
+            PunchHeavy,
+            KickHeavy,
+            PunchForwardHeavy,
+            KickForwardHeavy,
+            AimingOverlay,
+            HitOverlay,
+            CrouchRun,
+            CrouchWalk,
+            CrouchRunBackwards,
+            CrouchWalkBackwards,
+            CrouchRunSidestepLeft,
+            CrouchRunSidestepRight,
+            CrouchWalkSidestepLeft,
+            CrouchWalkSidestepRight,
+            RunKick,
+            RunPunch,
+            RunBackPunch,
+            RunBackKick,
+            SidestepLeftKick,
+            SidestepLeftPunch,
+            SidestepRightKick,
+            SidestepRightPunch,
+            Prone,
+            Flip,
+            HitHead,
+            HitBody,
+            HitFoot,
+            KnockdownHead,
+            KnockdownBody,
+            KnockdownFoot,
+            HitCrouch,
+            KnockdownCrouch,
+            HitFallen,
+            HitHeadBehind,
+            HitBodyBehind,
+            HitFootBehind,
+            KnockdownHeadBehind,
+            KnockdownBodyBehind,
+            KnockdownFootBehind,
+            HitCrouchBehind,
+            KnockdownCrouchBehind,
+            Idle,
+            Taunt,
+            Throw,
+            Thrown1,
+            Thrown2,
+            Thrown3,
+            Thrown4,
+            Thrown5,
+            Thrown6,
+            Special1,
+            Special2,
+            Special3,
+            Special4,
+            ThrowForwardPunch,
+            ThrowForwardKick,
+            ThrowBackwardPunch,
+            ThrowBackwardKick,
+            RunThrowForwardPunch,
+            RunThrowBackwardPunch,
+            RunThrowForwardKick,
+            RunThrowBackwardKick,
+            Thrown7,
+            Thrown8,
+            Thrown9,
+            Thrown10,
+            Thrown11,
+            Thrown12,
+            StartleLeft,
+            StartleRight,
+            Sit,
+            StandSpecial,
+            Act,
+            Kick3Fw,
+            HitFootOuch,
+            HitJewels,
+            Thrown13,
+            Thrown14,
+            Thrown15,
+            Thrown16,
+            Thrown17,
+            PPKK,
+            PPKKK,
+            PPKKKK,
+            LandHard,
+            LandHardForward,
+            LandHardRight,
+            LandHardLeft,
+            LandHardBack,
+            LandDead,
+            CrouchTurnLeft,
+            CrouchTurnRight,
+            CrouchForward,
+            CrouchBack,
+            CrouchLeft,
+            CrouchRight,
+            GetupKickBack,
+            AutopistolRecoil,
+            PhaseRifleRecoil,
+            PhaseStreamRecoil,
+            SuperballRecoil,
+            VandegrafRecoil,
+            ScramCannonRecoil,
+            MercuryBowRecoil,
+            ScreamerRecoil,
+            PickupObject,
+            PickupPistol,
+            PickupRifle,
+            Holster,
+            DrawPistol,
+            DrawRifle,
+            Punch4,
+            ReloadPistol,
+            ReloadPhaseRifle,
+            ReloadPhaseStream,
+            ReloadSuperball,
+            ReloadVandegraf,
+            ReloadScramCannon,
+            ReloadMercuryBow,
+            ReloadScreamer,
+            PfPf,
+            PfPfPf,
+            PlPl,
+            PlPlPl,
+            PrPr,
+            PrPrPr,
+            PbPb,
+            PbPbPb,
+            PdPd,
+            PdPdPd,
+            KfKf,
+            KfKfKf,
+            KlKl,
+            KlKlKl,
+            KrKr,
+            KrKrKr,
+            KbKb,
+            KbKbKb,
+            KdKd,
+            KdKdKd,
+            StartleLt,
+            StartleRt,
+            StartleBk,
+            StartleFw,
+            Console,
+            ConsoleWalk,
+            Stagger,
+            Watch,
+            ActNo,
+            ActYes,
+            ActTalk,
+            ActShrug,
+            ActShout,
+            ActGive,
+            RunStop,
+            WalkStop,
+            RunStart,
+            WalkStart,
+            RunBackwardsStart,
+            WalkBackwardsStart,
+            Stun,
+            StaggerBehind,
+            Blownup,
+            BlownupBehind,
+            OneStepStop,
+            RunSidestepLeftStart,
+            RunSidestepRightStart,
+            Powerup,
+            FallingFlail,
+            ConsolePunch,
+            TeleportIn,
+            TeleportOut,
+            NinjaFireball,
+            NinjaInvisible,
+            PunchRifle,
+            PickupObjectMid,
+            PickupPistolMid,
+            PickupRifleMid,
+            Hail,
+            MuroThunderbolt,
+            HitOverlayAI
+        }
+
+        [Flags]
+        public enum TRAMFootstepType
+        {
+            None = 0,
+            Left = 1,
+            Right = 2
+        }
+
+        [Flags]
+        public enum TRAMAttackFlags
+        {
+            None = 0,
+            Unblockable = 1,
+            High = 2,
+            Low = 4,
+            HalfDamage = 8
+        }
+
+        [Flags]
+        public enum TRAMVarient
+        {
+            None = 0x0000,
+            Sprint = 0x0100,
+            Combat = 0x0200,
+            RightPistol = 0x0800,
+            LeftPistol = 0x1000,
+            RightRifle = 0x2000,
+            LeftRifle = 0x4000,
+            Panic = 0x8000
+        }
+
+        [Flags]
+        public enum TRAMBoneFlags : uint
+        {
+            None = 0x0000,
+            Pelvis = 0x0001,
+            LeftThigh = 0x0002,
+            LeftCalf = 0x0004,
+            LeftFoot = 0x0008,
+            RightThigh = 0x0010,
+            RightCalf = 0x0020,
+            RightFoot = 0x0040,
+            Mid = 0x0080,
+            Chest = 0x0100,
+            Neck = 0x0200,
+            Head = 0x0400,
+            LeftShoulder = 0x0800,
+            LeftArm = 0x1000,
+            LeftWrist = 0x2000,
+            LeftFist = 0x4000,
+            RightShoulder = 0x8000,
+            RightArm = 0x10000,
+            RightWrist = 0x20000,
+            RightFist = 0x40000
+        }
+
+        public enum TRAMBone
+        {
+            Pelvis,
+            LeftThigh,
+            LeftCalf,
+            LeftFoot,
+            RightThigh,
+            RightCalf,
+            RightFoot,
+            Mid,
+            Chest,
+            Neck,
+            Head,
+            LeftShoulder,
+            LeftArm,
+            LeftWrist,
+            LeftFist,
+            RightShoulder,
+            RightArm,
+            RightWrist,
+            RightFist
+        }
+
+        public enum TRAMDirection
+        {
+            None,
+            Forward,
+            Backward,
+            Left,
+            Right
+        }
+
+        [Flags]
+        public enum TRAMFlags
+        {
+            RuntimeLoaded = 0x00000001,
+            Invulnerable = 0x00000002,
+            BlockHigh = 0x00000004,
+            BlockLow = 0x00000008,
+            Attack = 0x00000010,
+            DropWeapon = 0x00000020,
+            InAir = 0x00000040,
+            Atomic = 0x00000080,
+
+            NoTurn = 0x00000100,
+            AttackForward = 0x00000200,
+            AttackLeft = 0x00000400,
+            AttackRight = 0x00000800,
+            AttackBackward = 0x00001000,
+            Overlay = 0x00002000,
+            DontInterpolateVelocity = 0x00004000,
+            ThrowSource = 0x00008000,
+
+            ThrowTarget = 0x00010000,
+            RealWorld = 0x00020000,
+            DoAim = 0x00040000,
+            DontAim = 0x00080000,
+            CanPickup = 0x00100000,
+            Aim360 = 0x00200000,
+            DisableShield = 0x00400000,
+            NoAIPickup = 0x00800000
+        }
+
+        public enum TRAMState
+        {
+            None,
+            Anything,
+            RunningLeftDown,
+            RunningRightDown,
+            Sliding,
+            WalkingLeftDown,
+            WalkingRightDown,
+            Standing,
+            RunStart,
+            RunAccel,
+            RunSidestepLeft,
+            RunSidestepRight,
+            RunSlide,
+            RunJump,
+            RunJumpLand,
+            RunBackStart,
+            RunningBackRightDown,
+            RunningBackLeftDown,
+            FallenBack,
+            Crouch,
+            RunningUpstairRightDown,
+            RunningUpstairLeftDown,
+            SidestepLeftLeftDown,
+            SidestepLeftRightDown,
+            SidestepRightLeftDown,
+            SidestepRightRightDown,
+            SidestepRightJump,
+            SidestepLeftJump,
+            JumpForward,
+            JumpUp,
+            RunBackSlide,
+            LieBack,
+            SsLtStart,
+            SsRtStart,
+            WalkingSidestepLeft,
+            CrouchWalk,
+            WalkingSidestepRight,
+            Flying,
+            Falling,
+            FlyingForward,
+            FallingForward,
+            FlyingBack,
+            FallingBack,
+            FlyingLeft,
+            FallingLeft,
+            FlyingRight,
+            FallingRight,
+            CrouchStart,
+            WalkingBackLeftDown,
+            WalkingBackRightDown,
+            FallenFront,
+            SidestepLeftStart,
+            SidestepRightStart,
+            Sit,
+            PunchLow,
+            StandSpecial,
+            Acting,
+            CrouchRunLeft,
+            CrouchRunRight,
+            CrouchRunBackLeft,
+            CrouchRunBackRight,
+            Blocking1,
+            Blocking2,
+            Blocking3,
+            CrouchBlocking1,
+            Gliding,
+            WatchIdle,
+            Stunned,
+            Powerup,
+            Thunderbolt
+        }
+
+        private static readonly MetaStruct tram = new MetaStruct("TRAMInstance",
+            new Field(MetaType.Padding(4)),
+            new BinaryPartField(MetaType.RawOffset, "Height", "FrameCount", 4,
+                MetaType.Float),
+
+            new BinaryPartField(MetaType.RawOffset, "Velocity", "FrameCount", 8,
+                MetaType.Vector2),
+
+            new BinaryPartField(MetaType.RawOffset, "Attack", "AttackCount", 32, new MetaStruct("TRAMAttack",
+                new Field(MetaType.Int32, "Bones"),
+                new Field(MetaType.Float, "Unknown1"),
+                new Field(MetaType.Int32, "Flags"),
+                new Field(MetaType.Int16, "HitPoints"),
+                new Field(MetaType.Int16, "StartFrame"),
+                new Field(MetaType.Int16, "EndFrame"),
+                new Field(MetaType.Int16, "AnimationType"),
+                new Field(MetaType.Int16, "Unknown2"),
+                new Field(MetaType.Int16, "BlockStun"),
+                new Field(MetaType.Int16, "Stagger"),
+                new Field(MetaType.Padding(6)))),
+
+            new BinaryPartField(MetaType.RawOffset, "Damage", "DamageCount", 4, new MetaStruct("TRAMDamage",
+                new Field(MetaType.Int16, "Damage"),
+                new Field(MetaType.Int16, "Frame"))),
+
+            new BinaryPartField(MetaType.RawOffset, "MotionBlur", "MotionBlurCount", 12, new MetaStruct("TRAMMotionBlur",
+                new Field(MetaType.Int32, "Bones"),
+                new Field(MetaType.Int16, "StartFrame"),
+                new Field(MetaType.Int16, "EndFrame"),
+                new Field(MetaType.Byte, "Lifetime"),
+                new Field(MetaType.Byte, "Alpha"),
+                new Field(MetaType.Byte, "Interval"),
+                new Field(MetaType.Padding(1)))),
+
+            new BinaryPartField(MetaType.RawOffset, "Shortcut", "ShortcutCount", 8, new MetaStruct("TRAMShortcut",
+                new Field(MetaType.Int16, "FromState"),
+                new Field(MetaType.Byte, "Length"),
+                new Field(MetaType.Padding(1)),
+                new Field(MetaType.Int32, "Flags"))),
+
+            new BinaryPartField(MetaType.RawOffset, "Throw", 22, new MetaStruct("TRAMThrow",
+                new Field(MetaType.Vector3, "PositionAdjustment"),
+                new Field(MetaType.Float, "AngleAdjustment"),
+                new Field(MetaType.Float, "Distance"),
+                new Field(MetaType.Int16, "Type"))),
+
+            new BinaryPartField(MetaType.RawOffset, "Footstep", "FootstepCount", 4, new MetaStruct("TRAMFootstep",
+                new Field(MetaType.Int16, "Frame"),
+                new Field(MetaType.Int16, "Type"))),
+
+            new BinaryPartField(MetaType.RawOffset, "Particle", "ParticleCount", 24, new MetaStruct("TRAMParticle",
+                new Field(MetaType.Int16, "StartFrame"),
+                new Field(MetaType.Int16, "EndFrame"),
+                new Field(MetaType.Int32, "Bone"),
+                new Field(MetaType.String16, "Name"))),
+
+            new BinaryPartField(MetaType.RawOffset, "Position", "FrameCount", 8, new MetaStruct("TRAMPosition",
+                new Field(MetaType.Int16, "X"),
+                new Field(MetaType.Int16, "Z"),
+                new Field(MetaType.Int16, "Height"),
+                new Field(MetaType.Int16, "Y"))),
+
+            new BinaryPartField(MetaType.RawOffset, "Rotation"),
+
+            new BinaryPartField(MetaType.RawOffset, "Sound", "SoundDataCount", 34, new MetaStruct("TRAMSound",
+                new Field(MetaType.String32, "SoundName"),
+                new Field(MetaType.Int16, "StartFrame"))),
+
+            new Field(MetaType.Int32, "Flags"),
+            new Field(MetaType.Array(2, MetaType.Pointer(TemplateTag.TRAM)), "DirectAnimations"),
+            new Field(MetaType.Int32, "OverlayUsedParts"),
+            new Field(MetaType.Int32, "OverlayReplacedParts"),
+            new Field(MetaType.Float, "FinalRotation"),
+            new Field(MetaType.Int16, "MoveDirection"),
+            new Field(MetaType.Int16, "AttackSoundIndex"),
+            new Field(new MetaStruct("TRAMExtentInfo",
+                new Field(MetaType.Float, "MaxHorizontal"),
+                new Field(MetaType.Float, "MinY"),
+                new Field(MetaType.Float, "MaxY"),
+                new Field(MetaType.Array(36, MetaType.Float), "Horizontal"),
+                new Field(new MetaStruct("TRAMExtent",
+                    new Field(MetaType.Int16, "Frame"),
+                    new Field(MetaType.Byte, "Attack"),
+                    new Field(MetaType.Byte, "FrameOffset"),
+                    new Field(MetaType.Vector3, "Location"),
+                    new Field(MetaType.Float, "Length"),
+                    new Field(MetaType.Float, "MinY"),
+                    new Field(MetaType.Float, "MaxY"),
+                    new Field(MetaType.Float, "Angle")),
+                    "FirstExtent"),
+                new Field(new MetaStruct("TRAMExtent",
+                    new Field(MetaType.Int16, "Frame"),
+                    new Field(MetaType.Byte, "Attack"),
+                    new Field(MetaType.Byte, "FrameOffset"),
+                    new Field(MetaType.Vector3, "Location"),
+                    new Field(MetaType.Float, "Length"),
+                    new Field(MetaType.Float, "MinY"),
+                    new Field(MetaType.Float, "MaxY"),
+                    new Field(MetaType.Float, "Angle")),
+                    "MaxExtent"),
+                new Field(MetaType.Int32, "AlternateMoveDirection"),
+                new Field(MetaType.Int32, "ExtentCount"),
+                new BinaryPartField(MetaType.RawOffset, "Extents", "ExtentCount", 12, new MetaStruct("TRAMExtent",
+                    new Field(MetaType.Int16, "Frame"),
+                    new Field(MetaType.Int16, "Angle"),
+                    new Field(MetaType.Int16, "Length"),
+                    new Field(MetaType.Int16, "Offset"),
+                    new Field(MetaType.Int16, "MinY"),
+                    new Field(MetaType.Int16, "MaxY")))),
+                "ExtentInfo"),
+            new Field(MetaType.String16, "ImpactParticle"),
+            new Field(MetaType.Int16, "HardPause"),
+            new Field(MetaType.Int16, "SoftPause"),
+            new Field(MetaType.Int32, "SoundDataCount"),
+            new Field(MetaType.Padding(6)),
+            new Field(MetaType.Int16, "FramesPerSecond"),
+            new Field(MetaType.Int16, "CompressionSize"),
+            new Field(MetaType.Int16, "Type"),
+            new Field(MetaType.Int16, "AimingType"),
+            new Field(MetaType.Int16, "FromState"),
+            new Field(MetaType.Int16, "ToState"),
+            new Field(MetaType.Int16, "BodyPartCount"),
+            new Field(MetaType.Int16, "FrameCount"),
+            new Field(MetaType.Int16, "Duration"),
+            new Field(MetaType.Int16, "Varient"),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.Int16, "AtomicStart"),
+            new Field(MetaType.Int16, "AtomicEnd"),
+            new Field(MetaType.Int16, "EndInterpolation"),
+            new Field(MetaType.Int16, "MaxInterpolation"),
+            new Field(MetaType.Int16, "ActionFrame"),
+            new Field(MetaType.Int16, "FirstLevelAvailable"),
+            new Field(MetaType.Byte, "InvulnerableStart"),
+            new Field(MetaType.Byte, "InvulnerableEnd"),
+
+            new Field(MetaType.Byte, "AttackCount"),
+            new Field(MetaType.Byte, "DamageCount"),
+            new Field(MetaType.Byte, "MotionBlurCount"),
+            new Field(MetaType.Byte, "ShortcutCount"),
+            new Field(MetaType.Byte, "FootstepCount"),
+            new Field(MetaType.Byte, "ParticleCount")
+        );
+
+        //
+        // Animation Collection template
+        //
+
+        private static readonly MetaStruct trac = new MetaStruct("TRACInstance",
+            new Field(MetaType.Padding(16)),
+            new Field(MetaType.Pointer(TemplateTag.TRAC), "ParentCollection"),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("TRACAnimation",
+                new Field(MetaType.Int16, "Weight"),
+                new Field(MetaType.Padding(6)),
+                new Field(MetaType.Pointer(TemplateTag.TRAM), "Animation")
+            )), "Animations")
+        );
+
+        //
+        // Totoro Quaternion Body template
+        //
+
+        private static readonly MetaStruct trcm = new MetaStruct("TRCMInstance",
+            new Field(MetaType.Padding(4)),
+            new Field(MetaType.UInt16, "BodyPartCount"),
+            new Field(MetaType.Padding(78)),
+            new Field(MetaType.Pointer(TemplateTag.TRGA), "Geometry"),
+            new Field(MetaType.Pointer(TemplateTag.TRTA), "Position"),
+            new Field(MetaType.Pointer(TemplateTag.TRIA), "Hierarchy")
+        );
+
+        //
+        // Totoro Body Set template
+        //
+
+        private static readonly MetaStruct trbs = new MetaStruct("TRBSInstance",
+            new Field(MetaType.Array(5, MetaType.Pointer(TemplateTag.TRCM)), "Elements")
+        );
+
+        //
+        // Texture Map Array template
+        //
+
+        private static readonly MetaStruct trma = new MetaStruct("TRMAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(MetaType.Pointer(TemplateTag.TXMP)), "Textures")
+        );
+
+        //
+        // Totoro Quaternion Body Geometry Array template
+        //
+
+        private static readonly MetaStruct trga = new MetaStruct("TRGAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(MetaType.Pointer(TemplateTag.M3GM)), "Geometries")
+        );
+
+        //
+        // Totoro Quaternion Body Index Array template
+        //
+
+        private static readonly MetaStruct tria = new MetaStruct("TRIAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("TRIAElement",
+                new Field(MetaType.Byte, "Parent"),
+                new Field(MetaType.Byte, "Child"),
+                new Field(MetaType.Byte, "Sibling"),
+                new Field(MetaType.Padding(1))
+            )), "Elements")
+        );
+
+        //
+        // Screen (aiming) Collection template
+        //
+
+        private static readonly MetaStruct trsc = new MetaStruct("TRSCInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(MetaType.Pointer(TemplateTag.TRAS)), "AimingScreens")
+        );
+
+        //
+        // Totoro Quaternion Body Translation Array template
+        //
+
+        private static readonly MetaStruct trta = new MetaStruct("TRTAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(MetaType.Vector3), "Translations")
+        );
+
+        //
+        // Font template
+        //
+
+        private static readonly MetaStruct tsft = new MetaStruct("TSFTInstance",
+            new Field(MetaType.Padding(6)),
+            new Field(MetaType.Int16, "FontSize"),
+            new Field(MetaType.Int32, "FontStyle"),
+            new Field(MetaType.Int16, "AscenderHeight"),
+            new Field(MetaType.Int16, "DescenderHeight"),
+            new Field(MetaType.Int16, "LeadingHeight"),
+            new Field(MetaType.Int16, ""),
+            new Field(MetaType.Array(256, MetaType.Pointer(TemplateTag.TSGA)), "Glyphs"),
+            new Field(MetaType.VarArray(MetaType.Int32), "GlyphBitmaps")
+        );
+
+        //
+        // Font Family template
+        //
+
+        private static readonly MetaStruct tsff = new MetaStruct("TSFFInstance",
+            new Field(MetaType.Padding(16)),
+            new Field(MetaType.Pointer(TemplateTag.TSFL), "Language"),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.TSFT)), "Fonts")
+        );
+
+        //
+        // Font Language template
+        //
+
+        private static readonly MetaStruct tsfl = new MetaStruct("TSFLInstance",
+            new Field(MetaType.String64, ""),
+            new Field(MetaType.String64, "Breaking"),
+            new Field(MetaType.String64, ""),
+            new Field(MetaType.String64, ""),
+            new Field(MetaType.String64, "")
+        );
+
+        //
+        // Glyph Array template
+        //
+
+        private static readonly MetaStruct tsga = new MetaStruct("TSGAInstance",
+            new Field(MetaType.Array(256, new MetaStruct("TSGAGlyph",
+                new Field(MetaType.Int16, "Index"),
+                new Field(MetaType.Int16, "Width"),
+                new Field(MetaType.Int16, "GlyphWidth"),
+                new Field(MetaType.Int16, "GlyphHeight"),
+                new Field(MetaType.Int16, "GlyphXOrigin"),
+                new Field(MetaType.Int16, "GlyphYOrigin"),
+                new Field(MetaType.Int32, "GlyphBitmapOffset"),
+                new Field(MetaType.Padding(4))
+            )), "Glyphs")
+        );
+
+        //
+        // WM Cursor List template
+        //
+
+        private static readonly MetaStruct wmcl = new MetaStruct("WMCLInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("WMCLCursor",
+                new Field(MetaType.Int32, "Id"),
+                new Field(MetaType.Pointer(TemplateTag.PSpc), "Part")
+            )), "Cursors")
+        );
+
+        //
+        // WM Dialog Data template
+        //
+
+        [Flags]
+        private enum WMDDState
+        {
+            None = 0x00,
+            Visible = 0x01,
+            Disabled = 0x02,
+            State04 = 0x04
+        }
+
+        [Flags]
+        private enum WMDDStyle : uint
+        {
+            None = 0x00,
+            ThinBorder = 0x01,
+            ThickBorder = 0x02,
+            TitleBar = 0x04,
+            Title = 0x08,
+            CloseButton = 0x10,
+            RestoreButton = 0x20,
+            MinimizeButton = 0x40,
+            Center = 0x00010000
+        }
+
+        private enum WMDDControlFontStyle : uint
+        {
+            Normal = 0,
+            Bold = 1,
+            Italic = 2
+        }
+
+        private enum WMDDControlClass : ushort
+        {
+            Desktop = 1,
+            Title = 3,
+            Button = 4,
+            Checkbox = 5,
+            Dialog = 6,
+            Textbox = 7,
+            Listbox = 8,
+            MenuBar = 9,
+            Menu = 10,
+            Image = 11,
+            Dropdown = 12,
+            ProgressBar = 13,
+            RadioButton = 14,
+            Slider = 17,
+            Label = 20
+        }
+
+        private static readonly MetaStruct wmdd = new MetaStruct("WMDDInstance",
+            new Field(MetaType.String256, "Caption"),
+            new Field(MetaType.Int16, "Id"),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.Enum<WMDDState>(), "State"),
+            new Field(MetaType.Enum<WMDDStyle>(), "Style"),
+            new Field(MetaType.Int16, "X"),
+            new Field(MetaType.Int16, "Y"),
+            new Field(MetaType.Int16, "Width"),
+            new Field(MetaType.Int16, "Height"),
+            new Field(MetaType.VarArray(new MetaStruct("WMDDControl",
+                new Field(MetaType.String256, "Text"),
+                new Field(MetaType.Enum<WMDDControlClass>(), "Class"),
+                new Field(MetaType.Int16, "Id"),
+                new Field(MetaType.Int32, "State"),
+                new Field(MetaType.Int32, "Style"),
+                new Field(MetaType.Int16, "X"),
+                new Field(MetaType.Int16, "Y"),
+                new Field(MetaType.Int16, "Width"),
+                new Field(MetaType.Int16, "Height"),
+                new Field(new MetaStruct("WMDDFont",
+                    new Field(MetaType.Pointer(TemplateTag.TSFF), "Family"),
+                    new Field(MetaType.Enum<WMDDControlFontStyle>(), "Style"),
+                    new Field(MetaType.Color, "Color"),
+                    new Field(MetaType.Padding(1, 1)),
+                    new Field(MetaType.Padding(1, 0)),
+                    new Field(MetaType.Int16, "Size")),
+                    "Font")
+            )), "Controls")
+        );
+
+        //
+        // WM Menu Bar template
+        //
+
+        private static readonly MetaStruct wmmb = new MetaStruct("WMMBInstance",
+            new Field(MetaType.Padding(18)),
+            new Field(MetaType.Int16, "Id"),
+            new Field(MetaType.VarArray(MetaType.Pointer(TemplateTag.WMM_)), "Items")
+        );
+
+        //
+        // WM Menu template
+        //
+
+        private enum WMM_MenuItemType : ushort
+        {
+            Separator = 1,
+            Option = 2
+        }
+
+        private static readonly MetaStruct wmm_ = new MetaStruct("WMM_Instance",
+            new Field(MetaType.Padding(18)),
+            new Field(MetaType.Int16, "Id"),
+            new Field(MetaType.String64, "Text"),
+            new Field(MetaType.VarArray(new MetaStruct("WMM_MenuItem",
+                new Field(MetaType.Enum<WMM_MenuItemType>(), "Type"),
+                new Field(MetaType.Int16, "Id"),
+                new Field(MetaType.String64, "Text")
+            )), "Items")
+        );
+
+        //
+        // Oni Weapon Class template
+        //
+
+        [Flags]
+        public enum ONWCFlags : uint
+        {
+            None = 0x00000000,
+            NoHolster = 0x00000002,
+            UsesCells = 0x00000004,
+            TwoHanded = 0x00000008,
+
+            RecoilAffectsAiming = 0x00000010,
+            Automatic = 0x00000020,
+            StunSwitcher = 0x00000080,
+
+            KnockdownSwitcher = 0x00000100,
+            Explosive = 0x00000200,
+            SecondaryFire = 0x00000400,
+            BarabbasWeapon = 0x00000800,
+
+            Heavy = 0x00001000,
+            AutoRelease = 0x00002000,
+            HasReleaseDelay = 0x00004000,
+            HasLaserSight = 0x00008000,
+
+            ScaleCrosshair = 0x00020000,
+            NoFade = 0x00080000,
+            DrainAmmo = 0x00100000
+        }
+
+        private static readonly MetaStruct onwc = new MetaStruct("ONWCInstance",
+            new Field(new MetaStruct("ONWCLaserSight",
+                new Field(MetaType.Vector3, "Origin"),
+                new Field(MetaType.Float, "Stiffness"),
+                new Field(MetaType.Float, "AdditionalAzimuth"),
+                new Field(MetaType.Float, "AdditionalElevation"),
+                new Field(MetaType.Float, "LaserMaxLength"),
+                new Field(MetaType.Color, "LaserColor"),
+                new Field(MetaType.Pointer(TemplateTag.TXMP), "NormalTexture"),
+                new Field(MetaType.Color, "NormalColor"),
+                new Field(MetaType.Float, "NormalScale"),
+                new Field(MetaType.Pointer(TemplateTag.TXMP), "LockedTexture"),
+                new Field(MetaType.Color, "LockedColor"),
+                new Field(MetaType.Float, "LockedScale"),
+                new Field(MetaType.Pointer(TemplateTag.TXMP), "TunnelTexture"),
+                new Field(MetaType.Color, "TunnelColor"),
+                new Field(MetaType.Float, "TunnelScale"),
+                new Field(MetaType.Int32, "TunnelCount"),
+                new Field(MetaType.Float, "TunnelSpacing")),
+                "LaserSight"),
+            new Field(new MetaStruct("ONWCAmmoMeter",
+                new Field(MetaType.Pointer(TemplateTag.TXMP), "Icon"),
+                new Field(MetaType.Pointer(TemplateTag.TXMP), "Empty"),
+                new Field(MetaType.Pointer(TemplateTag.TXMP), "Fill")),
+                "AmmoMeter"),
+            new Field(MetaType.Pointer(TemplateTag.M3GM), "Geometry"),
+            new Field(MetaType.String32, "Name"),
+            new Field(MetaType.Float, "MouseSensitivity"),
+            new Field(new MetaStruct("ONWCRecoil",
+                new Field(MetaType.Float, "Base"),
+                new Field(MetaType.Float, "Max"),
+                new Field(MetaType.Float, "Factor"),
+                new Field(MetaType.Float, "ReturnSpeed"),
+                new Field(MetaType.Float, "FiringReturnSpeed")),
+                "Recoil"),
+            new Field(MetaType.Padding(36)),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.Enum<TRAMType>(), "RecoilAnimationType"),
+            new Field(MetaType.Enum<TRAMType>(), "ReloadAnimationType"),
+            new Field(MetaType.Int16, "PauseAfterReload"),
+            new Field(MetaType.Int16, "MaxShots"),
+            new Field(MetaType.Int16, "ParticleCount"),
+            new Field(MetaType.Int16, "FiringModeCount"),
+            new Field(MetaType.Int16, "PauseBeforeReload"),
+            new Field(MetaType.Int16, "ReleaseDelay"),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.Enum<ONWCFlags>(), "Flags"),
+
+            new Field(MetaType.Array(2, aiFiringMode), "FiringModes"),
+
+            new Field(MetaType.Array(16, new MetaStruct("ONWCParticle",
+                new Field(MetaType.Matrix4x3, "Transform"),
+                new Field(MetaType.String16, "ParticleClass"),
+                new Field(MetaType.Padding(4)),
+                new Field(MetaType.Int16, "UsedAmmo"),
+                new Field(MetaType.Int16, "ShotDelay"),
+                new Field(MetaType.Int16, "RoughJusticeShotDelay"),
+                new Field(MetaType.Int16, "ActiveFrames"),
+                new Field(MetaType.Int16, "TriggeredBy"),
+                new Field(MetaType.Int16, "DelayBeforeFiring")
+            )), "Particles"),
+
+            new Field(MetaType.String32, "EmptyWeaponSound"),
+            new Field(MetaType.Padding(4)),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "Glow"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "GlowAmmo"),
+            new Field(MetaType.Vector2, "GlowTextureScale"),
+            new Field(MetaType.Vector3, "PickupHandleOffset"),
+            new Field(MetaType.Float, "HoveringHeight")
+        );
+
+        public enum TXMPFormat : uint
+        {
+            BGRA4444 = 0,
+            BGR555 = 1,
+            BGRA5551 = 2,
+            RGBA = 7,
+            BGR = 8,
+            DXT1 = 9
+        }
+
+        [Flags]
+        public enum TXMPFlags : uint
+        {
+            None = 0x0000,
+            HasMipMaps = 0x0001,
+            DisableUWrap = 0x0004,
+            DisableVWrap = 0x0008,
+            Unknown0010 = 0x0010,
+            AnimBackToBack = 0x0040,
+            AnimRandom = 0x0080,
+            AnimUseLocalTime = 0x0100,
+            HasEnvMap = 0x0200,
+            AdditiveBlend = 0x0400,
+            SwapBytes = 0x1000,
+            AnimIgnoreGlobalTime = 0x4000,
+            ShieldEffect = 0x8000,
+            InvisibilityEffect = 0x10000,
+            DaodanEffect = 0x20000,
+        }
+
+        //
+        // Obsolete instances
+        //
+
+        //
+        // AI script trigger array template
+        //
+
+        private static readonly MetaStruct aitr = new MetaStruct("AITRInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("AITRElement",
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.String64, "")
+            )), "Elements")
+        );
+
+        //
+        // Gunk Quad Debug Array template
+        //
+
+        private static readonly MetaStruct agdb = new MetaStruct("AGDBInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.VarArray(new MetaStruct("AGDBElement",
+                new BinaryPartField(MetaType.RawOffset, "ObjectNameDataOffset"),
+                new BinaryPartField(MetaType.RawOffset, "FileNameDataOffset")
+            )), "Elements")
+        );
+
+        //
+        // Door Frame Array template
+        //
+
+        private static readonly MetaStruct akda = new MetaStruct("AKDAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.Padding(4))
+        //new Field(MetaType.LongVarArray(new MetaStruct("AKDADoorFrame",
+        //    new Field(MetaType.Int32, "QuadId"),
+        //    new Field(MetaType.BoundingBox, "BoundingBox"),
+        //    new Field(MetaType.Vector3, "Center"),
+        //    new Field(MetaType.Vector3, "Size")
+        //)), "DoorFrames")
+        );
+
+        //
+        // Door class array template
+        //
+
+        private static readonly MetaStruct obdc = new MetaStruct("OBDCInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("OBDCElement",
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Int16, ""),
+                new Field(MetaType.Pointer(TemplateTag.OBAN), "Animation"),
+                new Field(MetaType.Padding(4)),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Padding(8))
+            )), "Elements")
+        );
+
+        //
+        // Imported Flag Node Array template
+        //
+
+        private static readonly MetaStruct onfa = new MetaStruct("ONFAInstance",
+            new Field(MetaType.Padding(20)),
+            new Field(MetaType.Int16, "UsedElements"),
+            new Field(MetaType.ShortVarArray(new MetaStruct("ONFAElement",
+                new Field(MetaType.Matrix4x3, "Transform"),
+                new Field(MetaType.Vector3, "Position"),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int16, "FlagId"),
+                new Field(MetaType.Byte, ""),
+                new Field(MetaType.Byte, "")
+            )), "Elements")
+        );
+
+        //
+        // Imported Marker Node Array template
+        //
+
+        private static readonly MetaStruct onma = new MetaStruct("ONMAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(new MetaStruct("ONMAElement",
+                new Field(MetaType.String64, "Name"),
+                new Field(MetaType.Vector3, "Position"),
+                new Field(MetaType.Vector3, "Direction")
+            )), "Markers")
+        );
+
+        //
+        // Imported Spawn Array template
+        //
+
+        private static readonly MetaStruct onsa = new MetaStruct("ONSAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(MetaType.Int16), "Elements")
+        );
+
+        //
+        // Trigger Array template
+        //
+
+        private static readonly MetaStruct onta = new MetaStruct("ONTAInstance",
+            new Field(MetaType.Padding(16)),
+            new Field(MetaType.Int32, ""),
+            new Field(MetaType.VarArray(new MetaStruct(
+                new Field(MetaType.Array(8, new MetaStruct(
+                    new Field(MetaType.Int32, ""),
+                    new Field(MetaType.Int32, ""),
+                    new Field(MetaType.Int32, "")
+                )), ""),
+                new Field(MetaType.Array(6, new MetaStruct(
+                    new Field(MetaType.Array(4, MetaType.Int32), "")
+                )), ""),
+                new Field(MetaType.Array(6, new MetaStruct(
+                    new Field(MetaType.Int32, ""),
+                    new Field(MetaType.Int32, ""),
+                    new Field(MetaType.Int32, "")
+                )), ""),
+                new Field(MetaType.Array(6, new MetaStruct(
+                    new Field(MetaType.Int32, ""),
+                    new Field(MetaType.Int32, ""),
+                    new Field(MetaType.Int32, ""),
+                    new Field(MetaType.Int32, "")
+                )), ""),
+                new Field(MetaType.Array(6, MetaType.Int16), ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, ""),
+                new Field(MetaType.Int32, "")
+            )), "")
+        );
+
+        //
+        // String Array template
+        //
+
+        private static readonly MetaStruct stna = new MetaStruct("StNAInstance",
+            new Field(MetaType.Padding(22)),
+            new Field(MetaType.ShortVarArray(MetaType.Pointer(TemplateTag.TStr)), "Names")
+        );
+
+        //
+        // String template
+        //
+
+        private static readonly MetaStruct tstr = new MetaStruct("TStrInstance",
+            new Field(MetaType.String128, "Text")
+        );
+
+        protected virtual void InitializeTemplates(IList<Template> templates)
+        {
+            templates.Add(new Template(TemplateTag.AISA, aisa, 0x2a224c6be9, "AI Character Setup Array"));
+            templates.Add(new Template(TemplateTag.AITR, aitr, 0x1aea55, "AI Script Trigger Array"));
+            templates.Add(new Template(TemplateTag.AKAA, akaa, 0x11de77, "Adjacency Array"));
+            templates.Add(new Template(TemplateTag.ABNA, abna, 0x126da0, "BSP Tree Node Array"));
+            templates.Add(new Template(TemplateTag.AKVA, akva, 0xdf05e0, "BNV Node Array"));
+            templates.Add(new Template(TemplateTag.AKBA, akba, 0x3a2884, "Side Array"));
+            templates.Add(new Template(TemplateTag.AKBP, akbp, 0xcf449, "BSP Node Array"));
+            templates.Add(new Template(TemplateTag.AKDA, akda, 0x2e5464, "Door Frame Array"));
+            templates.Add(new Template(TemplateTag.AKEV, akev, 0x883014de75, "Akira Environment"));
+            templates.Add(new Template(TemplateTag.AGQC, agqc, 0x1ccb91, "Gunk Quad Collision Array"));
+            templates.Add(new Template(TemplateTag.AGDB, agdb, 0x72e17, "Gunk Quad Debug Array"));
+            templates.Add(new Template(TemplateTag.AGQG, agqg, 0x1c03d2, "Gunk Quad General Array"));
+            templates.Add(new Template(TemplateTag.AGQR, agqr, 0x83a3b, "Gunk Quad Render Array"));
+            templates.Add(new Template(TemplateTag.AKOT, akot, 0x11e7b8da08, "Oct tree"));
+            templates.Add(new Template(TemplateTag.OTIT, otit, 0xa51d2, "Oct Tree Interior Node Array"));
+            templates.Add(new Template(TemplateTag.OTLF, otlf, 0x1eac0b, "Oct Tree Leaf Node Array"));
+            templates.Add(new Template(TemplateTag.QTNA, qtna, 0x66ecc, "Quad Tree Node Array"));
+            templates.Add(new Template(TemplateTag.ENVP, envp, 0x67c1c3, "Env Particle Array"));
+            templates.Add(new Template(TemplateTag.M3GM, m3gm, 0x27a078e436, "Geometry"));
+            templates.Add(new Template(TemplateTag.M3GA, m3ga, 0x5206b20b2, "GeometryArray"));
+            templates.Add(new Template(TemplateTag.PLEA, plea, 0x7bc38, "Plane Equation Array"));
+            templates.Add(new Template(TemplateTag.PNTA, pnta, 0x37676c, "3D Point Array"));
+            templates.Add(new Template(TemplateTag.TXCA, txca, 0x9141a, "Texture Coordinate Array"));
+            templates.Add(new Template(TemplateTag.TXAN, txan, 0xa8b134387, "Texture Map Animation"));
+            templates.Add(new Template(TemplateTag.TXMA, txma, 0x599de7f90, "Texture map array"));
+            templates.Add(new Template(TemplateTag.TXMB, txmb, 0xa8b166a52, "Texture Map Big"));
+            templates.Add(new Template(TemplateTag.VCRA, vcra, 0x54739, "3D Vector Array"));
+            templates.Add(new Template(TemplateTag.Impt, impt, 0x44f16, "Impact"));
+            templates.Add(new Template(TemplateTag.Mtrl, mtrl, 0x28e0d, "Material"));
+            templates.Add(new Template(TemplateTag.CONS, cons, 0x13da8b0bdd, "Console"));
+            templates.Add(new Template(TemplateTag.DOOR, door, 0x63172fd67, "Door"));
+            templates.Add(new Template(TemplateTag.OBLS, obls, 0xb703d, "Object LS Data"));
+            templates.Add(new Template(TemplateTag.OFGA, ofga, 0x1374fac362, "Object Furn Geom Array"));
+            templates.Add(new Template(TemplateTag.TRIG, trig, 0x21dcd0cd2c, "Trigger"));
+            templates.Add(new Template(TemplateTag.TRGE, trge, 0x871a6b93c, "Trigger Emitter"));
+            templates.Add(new Template(TemplateTag.TURR, turr, 0x49c85805be, "Turret"));
+            templates.Add(new Template(TemplateTag.OBAN, oban, 0x4e0c24, "Object animation"));
+            templates.Add(new Template(TemplateTag.OBDC, obdc, 0x7bd9eca0b, "Door Class Array"));
+            templates.Add(new Template(TemplateTag.OBOA, oboa, 0x134f8986e1, "Starting Object Array"));
+            templates.Add(new Template(TemplateTag.CBPI, cbpi, 0xc0bf9d6c2, "Character Body Part Impacts"));
+            templates.Add(new Template(TemplateTag.CBPM, cbpm, 0x26ba4351f, "Character Body Part Material"));
+            templates.Add(new Template(TemplateTag.ONCC, oncc, 0x4a5aac759ef, "Oni Character Class"));
+            templates.Add(new Template(TemplateTag.ONIA, onia, 0x2b2f9a, "Oni Character Impact Array"));
+            templates.Add(new Template(TemplateTag.ONCP, oncp, 0x2f7321, "Oni Character Particle Array"));
+            templates.Add(new Template(TemplateTag.ONCV, oncv, 0x299f5, "Oni Character Variant"));
+            templates.Add(new Template(TemplateTag.CRSA, crsa, 0xc1543d4cc, "Corpse Array"));
+            templates.Add(new Template(TemplateTag.DPge, dpge, 0x7ba8a686b, "Diary Page"));
+            templates.Add(new Template(TemplateTag.FILM, film, 0xb331b62ad, "Film"));
+            templates.Add(new Template(TemplateTag.ONFA, onfa, 0x1b0ce7, "Imported Flag Node Array"));
+            templates.Add(new Template(TemplateTag.ONGS, ongs, 0x226ebb6, "Oni Game Settings"));
+            templates.Add(new Template(TemplateTag.HPge, hpge, 0x44b2f713b, "Help Page"));
+            templates.Add(new Template(TemplateTag.IGHH, ighh, 0x8e58e58de, "IGUI HUD Help"));
+            templates.Add(new Template(TemplateTag.IGPG, igpg, 0x11ce67887d, "IGUI Page"));
+            templates.Add(new Template(TemplateTag.IGPA, igpa, 0x4ddbe0905, "IGUI Page Array"));
+            templates.Add(new Template(TemplateTag.IGSt, igst, 0x2a2a47725, "IGUI String"));
+            templates.Add(new Template(TemplateTag.IGSA, igsa, 0x4ddbea408, "IGUI String Array"));
+            templates.Add(new Template(TemplateTag.IPge, ipge, 0x2938369ba, "Item Page"));
+            templates.Add(new Template(TemplateTag.KeyI, keyi, 0x403f4757ad, "Key Icons"));
+            templates.Add(new Template(TemplateTag.ONLV, onlv, 0x7db79a2ea3, "Oni Game Level"));
+            templates.Add(new Template(TemplateTag.ONLD, onld, 0x412a1, "Oni Game Level Descriptor"));
+            templates.Add(new Template(TemplateTag.ONMA, onma, 0x124779, "Imported Marker Node Array"));
+            templates.Add(new Template(TemplateTag.ONOA, onoa, 0x64be75c7c, "Object Gunk Array"));
+            templates.Add(new Template(TemplateTag.OPge, opge, 0x44b30bbfb, "Objective Page"));
+            templates.Add(new Template(TemplateTag.ONSK, onsk, 0x14c2261067, "Oni Sky class"));
+            templates.Add(new Template(TemplateTag.ONSA, onsa, 0x44634, "Imported Spawn Array"));
+            templates.Add(new Template(TemplateTag.TxtC, txtc, 0x1b7ac8b27, "Text Console"));
+            templates.Add(new Template(TemplateTag.ONTA, onta, 0xa0fcc0, "Trigger Array"));
+            templates.Add(new Template(TemplateTag.ONVL, onvl, 0x54434c58a, "Oni Variant List"));
+            templates.Add(new Template(TemplateTag.WPge, wpge, 0x46f5889b5, "Weapon Page"));
+            templates.Add(new Template(TemplateTag.PSpc, pspc, 0x82648, "Part Specification"));
+            templates.Add(new Template(TemplateTag.PSpL, pspl, 0xccc05, "Part Specification List"));
+            templates.Add(new Template(TemplateTag.PSUI, psui, 0x3cd544e96fb, "Part Specifications UI"));
+            templates.Add(new Template(TemplateTag.SUBT, subt, 0x46c68, "Subtitle Array"));
+            templates.Add(new Template(TemplateTag.IDXA, idxa, 0x2708f, "Index Array"));
+            templates.Add(new Template(TemplateTag.TStr, tstr, 0x64a0, "String"));
+            templates.Add(new Template(TemplateTag.StNA, stna, 0x5998cb520, "String Array"));
+            templates.Add(new Template(TemplateTag.TRAS, tras, 0x1fa21a930, "Totoro Aiming Screen"));
+            templates.Add(new Template(TemplateTag.TRAM, tram, 0x107e3cc918, "Totoro Animation Sequence"));
+            templates.Add(new Template(TemplateTag.TRAC, trac, 0xf26e9fb2f, "Animation Collection"));
+            templates.Add(new Template(TemplateTag.TRCM, trcm, 0x2392de054e, "Totoro Quaternion Body"));
+            templates.Add(new Template(TemplateTag.TRBS, trbs, 0x2a2924239, "Totoro Body Set"));
+            templates.Add(new Template(TemplateTag.TRMA, trma, 0x599de6d57, "Texture Map Array"));
+            templates.Add(new Template(TemplateTag.TRGA, trga, 0x5206b20f8, "Totoro Quaternion Body Geometry Array"));
+            templates.Add(new Template(TemplateTag.TRIA, tria, 0xac482, "Totoro Quaternion Body Index Array"));
+            templates.Add(new Template(TemplateTag.TRSC, trsc, 0x599786b17, "Screen (aiming) Collection"));
+            templates.Add(new Template(TemplateTag.TRTA, trta, 0x759e8, "Totoro Quaternion Body Translation Array"));
+            templates.Add(new Template(TemplateTag.TSFT, tsft, 0x16ba91deea, "Font"));
+            templates.Add(new Template(TemplateTag.TSFF, tsff, 0xa8a6c488a, "Font Family"));
+            templates.Add(new Template(TemplateTag.TSFL, tsfl, 0x8de29, "Font Language"));
+            templates.Add(new Template(TemplateTag.TSGA, tsga, 0x2a4e98, "Glyph Array"));
+            templates.Add(new Template(TemplateTag.WMCL, wmcl, 0x9d076, "WM Cursor List"));
+            templates.Add(new Template(TemplateTag.WMDD, wmdd, 0x1c001df3c4, "WM Dialog Data"));
+            templates.Add(new Template(TemplateTag.WMMB, wmmb, 0x6d20c6737, "WM Menu Bar"));
+            templates.Add(new Template(TemplateTag.WMM_, wmm_, 0xc1a38, "WM Menu"));
+            templates.Add(new Template(TemplateTag.ONWC, onwc, 0x193a3e0eeb5, "Oni Weapon Class"));
+        }
+
+        private static void GetRawAndSepPartsV32(InstanceFile file, Dictionary<int, int> rawParts, Dictionary<int, int> sepParts)
+        {
+            List<int> rawOffsets = new List<int>();
+
+            using (BinaryReader reader = new BinaryReader(file.FilePath))
+            {
+                foreach (InstanceDescriptor descriptor in file.Descriptors)
+                {
+                    if (!descriptor.HasRawParts())
+                        continue;
+
+                    reader.Position = descriptor.DataOffset;
+
+                    descriptor.Template.Type.Copy(reader, null, state =>
+                    {
+                        BinaryPartField field = state.Field as BinaryPartField;
+
+                        if (field != null)
+                        {
+                            int offset = state.GetInt32();
+
+                            if (offset != 0)
+                                rawOffsets.Add(offset);
+                        }
+                    });
+                }
+            }
+
+            rawOffsets.Sort();
+
+            for (int i = 0; i < rawOffsets.Count; i++)
+            {
+                int offset = rawOffsets[i];
+                int size;
+
+                if (i + 1 < rawOffsets.Count)
+                    size = rawOffsets[i + 1] - offset;
+                else
+                    size = file.Header.RawTableSize - offset;
+
+                if (size > 0)
+                    rawParts.Add(offset, size);
+            }
+        }
+
+        public static void GetRawAndSepParts(InstanceFile file, Dictionary<int, int> rawParts, Dictionary<int, int> sepParts)
+        {
+            //if (file.Header.Version == InstanceFileHeader.Version32)
+            //{
+            //    GetRawAndSepPartsV32(file, rawParts, sepParts);
+            //    return;
+            //}
+
+            Dictionary<string, int> values = new Dictionary<string, int>();
+
+            using (BinaryReader reader = new BinaryReader(file.FilePath))
+            {
+                foreach (InstanceDescriptor descriptor in file.Descriptors)
+                {
+                    if (!descriptor.HasRawParts())
+                        continue;
+
+                    values.Clear();
+                    reader.Position = descriptor.DataOffset;
+
+                    descriptor.Template.Type.Copy(reader, null, state =>
+                    {
+                        string name = state.GetCurrentFieldName();
+
+                        if (!string.IsNullOrEmpty(name))
+                        {
+                            if (state.Type == MetaType.Int32)
+                                values[name] = state.GetInt32();
+                            else if (state.Type == MetaType.UInt32)
+                                values[name] = (int)state.GetUInt32();
+                            else if (state.Type == MetaType.Int16)
+                                values[name] = state.GetInt16();
+                            else if (state.Type == MetaType.UInt16)
+                                values[name] = state.GetUInt16();
+                            else if (state.Type == MetaType.Byte)
+                                values[name] = state.GetByte();
+                        }
+                    });
+
+                    reader.Position = descriptor.DataOffset;
+
+                    descriptor.Template.Type.Copy(reader, null, state =>
+                    {
+                        BinaryPartField field = state.Field as BinaryPartField;
+
+                        if (field != null)
+                        {
+                            int offset = state.GetInt32();
+
+                            if (offset != 0)
+                            {
+                                if (field.Type == MetaType.RawOffset)
+                                {
+                                    if (!rawParts.ContainsKey(offset))
+                                        rawParts.Add(offset, GetBinaryPartSize(descriptor, state, values));
+                                }
+                                else
+                                {
+                                    if (!sepParts.ContainsKey(offset))
+                                        sepParts.Add(offset, GetBinaryPartSize(descriptor, state, values));
+                                }
+                            }
+                        }
+                    });
+                }
+            }
+        }
+
+        private static int GetBinaryPartSize(InstanceDescriptor descriptor, CopyVisitor state, Dictionary<string, int> values)
+        {
+            BinaryPartField field = (BinaryPartField)state.Field;
+
+            if (field.SizeFieldName != null)
+                return values[state.GetParentFieldName() + "." + field.SizeFieldName] * field.SizeMultiplier;
+
+            if (field.SizeMultiplier != 0)
+                return field.SizeMultiplier;
+
+            return GetSpecialBinaryPartSize(descriptor, state, values);
+        }
+
+        private static int GetSpecialBinaryPartSize(InstanceDescriptor descriptor, CopyVisitor state, Dictionary<string, int> values)
+        {
+            switch (descriptor.Template.Tag)
+            {
+                case TemplateTag.AGDB:
+                    return GetAGDBRawDataSize(descriptor, state.GetInt32(), values);
+
+                case TemplateTag.TRAM:
+                    return GetTRAMRotationsRawDataSize(descriptor, state.GetInt32(), values);
+
+                case TemplateTag.SUBT:
+                    return GetSUBTRawDataSize(descriptor, state.GetInt32(), values);
+
+                case TemplateTag.TXMP:
+                    return GetTXMPRawDataSize(descriptor, state.GetInt32(), values);
+
+                default:
+                    throw new NotSupportedException(string.Format("Cannot get the raw data part size of type {0}", state.TopLevelType.Name));
+            }
+        }
+
+        private static int GetAGDBRawDataSize(InstanceDescriptor descriptor, int rawOffset, Dictionary<string, int> values)
+        {
+            using (var rawReader = descriptor.GetRawReader(rawOffset))
+            {
+                var startOffset = rawReader.Position;
+                rawReader.SkipCString();
+                return (int)(rawReader.Position - startOffset);
+            }
+        }
+
+        private static int GetSUBTRawDataSize(InstanceDescriptor descriptor, int rawOffset, Dictionary<string, int> values)
+        {
+            int lastEntry = 0;
+
+            using (var datReader = descriptor.OpenRead(20))
+            {
+                var entries = datReader.ReadInt32Array(datReader.ReadInt32());
+
+                foreach (int entry in entries)
+                {
+                    if (entry > lastEntry)
+                        lastEntry = entry;
+                }
+            }
+
+            using (var rawReader = descriptor.GetRawReader(rawOffset))
+            {
+                int startOffset = rawReader.Position;
+                rawReader.Position += lastEntry;
+                rawReader.SkipCString();
+                rawReader.SkipCString();
+                return rawReader.Position - startOffset;
+            }
+        }
+
+        private static int GetTRAMRotationsRawDataSize(InstanceDescriptor descriptor, int rawOffset, Dictionary<string, int> values)
+        {
+            int numParts = values["TRAMInstance.BodyPartCount"];
+            int compressionSize = values["TRAMInstance.CompressionSize"];
+            int numFrames = values["TRAMInstance.FrameCount"];
+
+            using (var rawReader = descriptor.GetRawReader(rawOffset))
+            {
+                int startOffset = rawReader.Position;
+
+                rawReader.Skip((numParts - 1) * 2);
+                int lastBoneOffset = rawReader.ReadInt16();
+                rawReader.Skip(lastBoneOffset - numParts * 2);
+
+                int time = 1;
+
+                for (int totalTime = 0; time > 0; totalTime += time)
+                {
+                    rawReader.Skip(compressionSize);
+
+                    if (totalTime < numFrames - 1)
+                        time = rawReader.ReadByte();
+                    else
+                        time = 0;
+                }
+
+                return rawReader.Position - startOffset;
+            }
+        }
+
+        private static int GetTXMPRawDataSize(InstanceDescriptor descriptor, int rawOffset, Dictionary<string, int> values)
+        {
+            int width = values["TXMPInstance.Width"];
+            int height = values["TXMPInstance.Height"];
+            Motoko.TextureFlags flags = (Motoko.TextureFlags)values["TXMPInstance.Flags"];
+            TXMPFormat format = (TXMPFormat)values["TXMPInstance.Format"];
+            int length;
+
+            switch (format)
+            {
+                case TXMPFormat.BGRA4444:
+                case TXMPFormat.BGR555:
+                case TXMPFormat.BGRA5551:
+                    length = width * height * 2;
+                    break;
+
+                case TXMPFormat.BGR:
+                case TXMPFormat.RGBA:
+                    length = width * height * 4;
+                    break;
+
+                case TXMPFormat.DXT1:
+                    length = width * height / 2;
+                    break;
+
+                default:
+                    throw new NotSupportedException("Unsupported texture format");
+            }
+
+            int totalLength = length;
+
+            if ((flags & Motoko.TextureFlags.HasMipMaps) != 0)
+            {
+                if (format == TXMPFormat.DXT1)
+                {
+                    do
+                    {
+                        if (width > 1)
+                            width >>= 1;
+
+                        if (height > 1)
+                            height >>= 1;
+
+                        totalLength += Math.Max(1, width / 4) * Math.Max(1, height / 4) * 8;
+                    }
+                    while (height > 1 || width > 1);
+                }
+                else
+                {
+                    do
+                    {
+                        if (width > 1)
+                        {
+                            width >>= 1;
+                            length >>= 1;
+                        }
+
+                        if (height > 1)
+                        {
+                            height >>= 1;
+                            length >>= 1;
+                        }
+
+                        totalLength += length;
+                    }
+                    while (height > 1 || width > 1);
+                }
+            }
+
+            return totalLength;
+        }
+
+        #region Private data
+        private static InstanceMetadata pcMetadata;
+        private static InstanceMetadata macMetadata;
+        private Dictionary<TemplateTag, Template> templateIndex;
+        #endregion
+
+        public static InstanceMetadata GetMetadata(InstanceFile instanceFile)
+        {
+            return GetMetadata(instanceFile.Header.TemplateChecksum);
+        }
+
+        public static InstanceMetadata GetMetadata(long templateChecksum)
+        {
+            if (templateChecksum == InstanceFileHeader.OniPCTemplateChecksum)
+            {
+                if (pcMetadata == null)
+                    pcMetadata = new OniPcMetadata();
+
+                return pcMetadata;
+            }
+
+            if (templateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
+            {
+                if (macMetadata == null)
+                    macMetadata = new OniMacMetadata();
+
+                return macMetadata;
+            }
+
+            throw new NotSupportedException();
+        }
+
+        public Template GetTemplate(TemplateTag tag)
+        {
+            if (templateIndex == null)
+            {
+                templateIndex = new Dictionary<TemplateTag, Template>();
+
+                List<Template> templates = new List<Template>();
+                InitializeTemplates(templates);
+
+                foreach (Template template in templates)
+                    templateIndex.Add(template.Tag, template);
+            }
+
+            Template result;
+            templateIndex.TryGetValue(tag, out result);
+            return result;
+        }
+
+        public static void DumpCStructs(TextWriter writer)
+        {
+            OniPcMetadata metadata = new OniPcMetadata();
+            DumpVisitor visitor = new DumpVisitor(writer, metadata);
+
+            foreach (TemplateTag tag in Enum.GetValues(typeof(TemplateTag)))
+            {
+                Template template = metadata.GetTemplate(tag);
+
+                if (template == null)
+                    continue;
+
+                visitor.VisitStruct(template.Type);
+                DumpCStruct(writer, template.Type);
+            }
+        }
+
+        private static void DumpCStruct(TextWriter writer, MetaStruct type)
+        {
+            writer.WriteLine();
+            writer.WriteLine("struct {0} {{", type.Name);
+            string indent = "\t";
+
+            foreach (Field field in type.Fields)
+            {
+                if (field.Type is MetaPointer)
+                {
+                    MetaPointer ptr = field.Type as MetaPointer;
+
+                    writer.WriteLine("{0}{1} *{2};", indent, new OniPcMetadata().GetTemplate(ptr.Tag).Type.Name, field.Name);
+                }
+                else if (field.Type is MetaVarArray)
+                {
+                    MetaVarArray varArray = field.Type as MetaVarArray;
+
+                    writer.WriteLine("{0}{1} {2}[1];", indent, varArray.ElementType, field.Name);
+                }
+                else
+                {
+                    writer.WriteLine("{0}{1} {2};", indent, field.Type.Name, field.Name);
+                }
+            }
+
+            indent = indent.Substring(0, indent.Length - 1);
+            writer.WriteLine("};");
+        }
+    }
+}
Index: /OniSplit/Metadata/LinkVisitor.cs
===================================================================
--- /OniSplit/Metadata/LinkVisitor.cs	(revision 1114)
+++ /OniSplit/Metadata/LinkVisitor.cs	(revision 1114)
@@ -0,0 +1,74 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Metadata
+{
+    internal class LinkVisitor : MetaTypeVisitor
+    {
+        private readonly List<int> links = new List<int>();
+        private readonly BinaryReader reader;
+
+        public LinkVisitor(BinaryReader reader)
+        {
+            this.reader = reader;
+        }
+
+        public ICollection<int> Links => links;
+
+        public override void VisitByte(MetaByte type) => reader.Position += type.Size;
+        public override void VisitInt16(MetaInt16 type) => reader.Position += type.Size;
+        public override void VisitUInt16(MetaUInt16 type) => reader.Position += type.Size;
+        public override void VisitInt32(MetaInt32 type) => reader.Position += type.Size;
+        public override void VisitUInt32(MetaUInt32 type) => reader.Position += type.Size;
+        public override void VisitInt64(MetaInt64 type) => reader.Position += type.Size;
+        public override void VisitUInt64(MetaUInt64 type) => reader.Position += type.Size;
+        public override void VisitFloat(MetaFloat type) => reader.Position += type.Size;
+        public override void VisitColor(MetaColor type) => reader.Position += type.Size;
+        public override void VisitRawOffset(MetaRawOffset type) => reader.Position += type.Size;
+        public override void VisitSepOffset(MetaSepOffset type) => reader.Position += type.Size;
+
+        public override void VisitPointer(MetaPointer type)
+        {
+            int id = reader.ReadInt32();
+
+            if (id != 0)
+                links.Add(id);
+        }
+
+        public override void VisitString(MetaString type) => reader.Position += type.Size;
+        public override void VisitPadding(MetaPadding type) => reader.Position += type.Size;
+
+        public override void VisitStruct(MetaStruct type)
+        {
+            foreach (Field field in type.Fields)
+                field.Type.Accept(this);
+        }
+
+        public override void VisitArray(MetaArray type) => VisitArray(type.ElementType, type.Count);
+
+        public override void VisitVarArray(MetaVarArray type)
+        {
+            int count;
+
+            if (type.CountField.Type == MetaType.Int16)
+                count = reader.ReadUInt16();
+            else
+                count = reader.ReadInt32();
+
+            VisitArray(type.ElementType, count);
+        }
+
+        private void VisitArray(MetaType elementType, int count)
+        {
+            if (elementType is MetaPointer || elementType is MetaStruct)
+            {
+                for (int i = 0; i < count; i++)
+                    elementType.Accept(this);
+            }
+            else
+            {
+                reader.Position += elementType.Size * count;
+            }
+        }
+    }
+}
Index: /OniSplit/Metadata/MetaArray.cs
===================================================================
--- /OniSplit/Metadata/MetaArray.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaArray.cs	(revision 1114)
@@ -0,0 +1,28 @@
+﻿using System;
+using System.Globalization;
+
+namespace Oni.Metadata
+{
+    internal class MetaArray : MetaType
+    {
+        private readonly MetaType elementType;
+        private readonly int count;
+
+        public MetaArray(MetaType elementType, int count)
+        {
+            this.elementType = elementType;
+            this.count = count;
+
+            Name = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", elementType.Name, count);
+            Size = elementType.Size * count;
+        }
+
+        public MetaType ElementType => elementType;
+
+        public int Count => count;
+
+        protected override bool IsLeafImpl() => elementType.IsLeaf;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitArray(this);
+    }
+}
Index: /OniSplit/Metadata/MetaBoundingBox.cs
===================================================================
--- /OniSplit/Metadata/MetaBoundingBox.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaBoundingBox.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaBoundingBox : MetaStruct
+    {
+        internal MetaBoundingBox()
+            : base("BoundingBox", new Field(Vector3, "Min"), new Field(Vector3, "Max"))
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitBoundingBox(this);
+    }
+}
Index: /OniSplit/Metadata/MetaBoundingSphere.cs
===================================================================
--- /OniSplit/Metadata/MetaBoundingSphere.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaBoundingSphere.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaBoundingSphere : MetaStruct
+    {
+        internal MetaBoundingSphere()
+            : base("BoundingSphere", new Field(Vector3, "Center"), new Field(Float, "Radius"))
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitBoundingSphere(this);
+    }
+}
Index: /OniSplit/Metadata/MetaByte.cs
===================================================================
--- /OniSplit/Metadata/MetaByte.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaByte.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaByte : MetaPrimitiveType
+    {
+        internal MetaByte() : base("UInt8", 1)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitByte(this);
+    }
+}
Index: /OniSplit/Metadata/MetaChar.cs
===================================================================
--- /OniSplit/Metadata/MetaChar.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaChar.cs	(revision 1114)
@@ -0,0 +1,20 @@
+﻿using System;
+
+namespace Oni.Metadata
+{
+    internal class MetaChar : MetaPrimitiveType
+    {
+        internal MetaChar() : base("Char", 1)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor)
+        {
+            //
+            // There are no Char typed fields. Char is only used as a String element.
+            //
+
+            throw new NotSupportedException();
+        }
+    }
+}
Index: /OniSplit/Metadata/MetaColor.cs
===================================================================
--- /OniSplit/Metadata/MetaColor.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaColor.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaColor : MetaType
+    {
+        internal MetaColor() : base("Color", 4)
+        {
+        }
+
+        protected override bool IsLeafImpl() => true;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitColor(this);
+    }
+}
Index: /OniSplit/Metadata/MetaEnum.cs
===================================================================
--- /OniSplit/Metadata/MetaEnum.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaEnum.cs	(revision 1114)
@@ -0,0 +1,149 @@
+﻿using System;
+using System.Xml;
+
+namespace Oni.Metadata
+{
+    internal class MetaEnum : MetaType
+    {
+        private MetaType baseType;
+        private Type enumType;
+
+        public MetaEnum(MetaType baseType, Type enumType) : base("Enum", baseType.Size)
+        {
+            if (baseType != MetaType.Byte
+                && baseType != MetaType.Int16
+                && baseType != MetaType.UInt16
+                && baseType != MetaType.Int32
+                && baseType != MetaType.UInt32
+                && baseType != MetaType.Int64
+                && baseType != MetaType.UInt64)
+            {
+                throw new ArgumentException("Invalid enum base type", "baseType");
+            }
+
+            this.baseType = baseType;
+            this.enumType = enumType;
+        }
+
+        public MetaType BaseType => baseType;
+
+        public Type EnumType => enumType;
+
+        public bool IsFlags => Utils.IsFlagsEnum(enumType);
+
+        protected override bool IsLeafImpl() => true;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitEnum(this);
+
+        public void BinaryToXml(BinaryReader reader, XmlWriter writer)
+        {
+            object value;
+
+            if (baseType == MetaType.Byte)
+                value = System.Enum.ToObject(enumType, reader.ReadByte());
+            else if (baseType == MetaType.Int16)
+                value = System.Enum.ToObject(enumType, reader.ReadInt16());
+            else if (baseType == MetaType.UInt16)
+                value = System.Enum.ToObject(enumType, reader.ReadUInt16());
+            else if (baseType == MetaType.Int32)
+                value = System.Enum.ToObject(enumType, reader.ReadInt32());
+            else if (baseType == MetaType.UInt32)
+                value = System.Enum.ToObject(enumType, reader.ReadUInt32());
+            else if (baseType == MetaType.Int64)
+                value = System.Enum.ToObject(enumType, reader.ReadInt64());
+            else
+                value = System.Enum.ToObject(enumType, reader.ReadUInt64());
+
+            string text = value.ToString().Replace(",", string.Empty);
+
+            if (text == "None" && IsFlags)
+                text = string.Empty;
+
+            writer.WriteValue(text);
+        }
+
+        public void XmlToBinary(XmlReader reader, BinaryWriter writer)
+        {
+            string text = reader.ReadElementContentAsString();
+
+            if (string.IsNullOrEmpty(text) && IsFlags)
+            {
+                if (baseType == MetaType.Byte)
+                    writer.WriteByte(0);
+                else if (baseType == MetaType.Int16 || baseType == MetaType.UInt16)
+                    writer.WriteUInt16(0);
+                else if (baseType == MetaType.Int32 || baseType == MetaType.UInt32)
+                    writer.Write(0);
+                else
+                    writer.Write(0L);
+
+                return;
+            }
+
+            object value = null;
+
+            try
+            {
+                value = System.Enum.Parse(enumType, text.Trim().Replace(' ', ','));
+            }
+            catch
+            {
+            }
+
+            if (value == null)
+                throw new FormatException(string.Format("{0} is not a valid value name. Run onisplit -help enums to see a list of possible names.", text));
+
+            if (baseType == MetaType.Byte)
+                writer.WriteByte(Convert.ToByte(value));
+            else if (baseType == MetaType.Int16)
+                writer.Write(Convert.ToInt16(value));
+            else if (baseType == MetaType.UInt16)
+                writer.Write(Convert.ToUInt16(value));
+            else if (baseType == MetaType.Int32)
+                writer.Write(Convert.ToInt32(value));
+            else if (baseType == MetaType.UInt32)
+                writer.Write(Convert.ToUInt32(value));
+            else if (baseType == MetaType.Int64)
+                writer.Write(Convert.ToInt64(value));
+            else
+                writer.Write(Convert.ToUInt64(value));
+        }
+
+        public static T Parse<T>(string text) where T : struct
+        {
+            object value = null;
+
+            if (string.IsNullOrEmpty(text) && Utils.IsFlagsEnum(typeof(T)))
+            {
+                value = System.Enum.Parse(typeof(T), "None");
+            }
+            else
+            {
+                try
+                {
+                    string[] values = text.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
+                    text = string.Join(", ", values);
+                    value = System.Enum.Parse(typeof(T), text, true);
+                }
+                catch
+                {
+                }
+            }
+
+            if (value == null)
+                throw new FormatException(string.Format("{0} is not a valid value name. Run onisplit -help enums to see a list of possible names.", text));
+
+            return (T)value;
+        }
+
+        public static string ToString<T>(T value) where T : struct
+        {
+            string text = value.ToString().Replace(",", string.Empty);
+
+            if (text == "None" && Utils.IsFlagsEnum(typeof(T)))
+                text = string.Empty;
+
+            return text;
+        }
+    }
+}
Index: /OniSplit/Metadata/MetaFloat.cs
===================================================================
--- /OniSplit/Metadata/MetaFloat.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaFloat.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaFloat : MetaPrimitiveType
+    {
+        internal MetaFloat() : base("Float", 4)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitFloat(this);
+    }
+}
Index: /OniSplit/Metadata/MetaInt16.cs
===================================================================
--- /OniSplit/Metadata/MetaInt16.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaInt16.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaInt16 : MetaPrimitiveType
+    {
+        internal MetaInt16() : base("Int16", 2)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitInt16(this);
+    }
+}
Index: /OniSplit/Metadata/MetaInt32.cs
===================================================================
--- /OniSplit/Metadata/MetaInt32.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaInt32.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaInt32 : MetaPrimitiveType
+    {
+        internal MetaInt32() : base("Int32", 4)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitInt32(this);
+    }
+}
Index: /OniSplit/Metadata/MetaInt64.cs
===================================================================
--- /OniSplit/Metadata/MetaInt64.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaInt64.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaInt64 : MetaPrimitiveType
+    {
+        internal MetaInt64() : base("Int64", 8)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitInt64(this);
+    }
+}
Index: /OniSplit/Metadata/MetaLink.cs
===================================================================
--- /OniSplit/Metadata/MetaLink.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaLink.cs	(revision 1114)
@@ -0,0 +1,30 @@
+﻿using System;
+
+namespace Oni.Metadata
+{
+	internal class MetaLink : MetaType
+	{
+		private TemplateTag tag;
+
+		internal MetaLink(TemplateTag tag)
+		{
+			this.tag = tag;
+
+			Name = "Link";
+			Size = 4;
+		}
+
+		public TemplateTag Tag
+		{
+			get
+			{
+				return tag;
+			}
+		}
+
+		public override void Accept(IMetaTypeVisitor visitor)
+		{
+			visitor.VisitLink(this);
+		}
+	}
+}
Index: /OniSplit/Metadata/MetaMatrix4x3.cs
===================================================================
--- /OniSplit/Metadata/MetaMatrix4x3.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaMatrix4x3.cs	(revision 1114)
@@ -0,0 +1,16 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaMatrix4x3 : MetaStruct
+    {
+        internal MetaMatrix4x3()
+            : base("Matrix4x3",
+                new Field(Float, "M11"), new Field(Float, "M12"), new Field(Float, "M13"),
+                new Field(Float, "M21"), new Field(Float, "M22"), new Field(Float, "M23"),
+                new Field(Float, "M31"), new Field(Float, "M32"), new Field(Float, "M33"),
+                new Field(Float, "M41"), new Field(Float, "M42"), new Field(Float, "M43"))
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitMatrix4x3(this);
+    }
+}
Index: /OniSplit/Metadata/MetaPadding.cs
===================================================================
--- /OniSplit/Metadata/MetaPadding.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaPadding.cs	(revision 1114)
@@ -0,0 +1,20 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaPadding : MetaArray
+    {
+        private readonly byte fillByte;
+
+        public MetaPadding(int length) : this(length, 0)
+        {
+        }
+
+        public MetaPadding(int length, byte fillByte) : base(MetaType.Byte, length)
+        {
+            this.fillByte = fillByte;
+        }
+
+        public byte FillByte => fillByte;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitPadding(this);
+    }
+}
Index: /OniSplit/Metadata/MetaPlane.cs
===================================================================
--- /OniSplit/Metadata/MetaPlane.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaPlane.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaPlane : MetaStruct
+    {
+        internal MetaPlane() : base("Plane", new Field(Vector3, "Normal"), new Field(Float, "D"))
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitPlane(this);
+    }
+}
Index: /OniSplit/Metadata/MetaPointer.cs
===================================================================
--- /OniSplit/Metadata/MetaPointer.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaPointer.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaPointer : MetaType
+    {
+        private readonly TemplateTag tag;
+
+        internal MetaPointer(TemplateTag tag) : base("Link", 4)
+        {
+            this.tag = tag;
+        }
+
+        protected override bool IsLeafImpl() => false;
+
+        public TemplateTag Tag => tag;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitPointer(this);
+    }
+}
Index: /OniSplit/Metadata/MetaPrimitiveType.cs
===================================================================
--- /OniSplit/Metadata/MetaPrimitiveType.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaPrimitiveType.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal abstract class MetaPrimitiveType : MetaType
+    {
+        protected MetaPrimitiveType(string name, int size) : base(name, size)
+        {
+        }
+
+        protected override bool IsLeafImpl() => true;
+    }
+}
Index: /OniSplit/Metadata/MetaQuaternion.cs
===================================================================
--- /OniSplit/Metadata/MetaQuaternion.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaQuaternion.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaQuaternion : MetaStruct
+    {
+        internal MetaQuaternion()
+            : base("Quaternion", new Field(Float, "X"), new Field(Float, "Y"), new Field(Float, "Z"), new Field(Float, "W"))
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitQuaternion(this);
+    }
+}
Index: /OniSplit/Metadata/MetaRawOffset.cs
===================================================================
--- /OniSplit/Metadata/MetaRawOffset.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaRawOffset.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaRawOffset : MetaType
+    {
+        internal MetaRawOffset() : base("RawOffset", 4)
+        {
+        }
+
+        protected override bool IsLeafImpl() => false;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitRawOffset(this);
+    }
+}
Index: /OniSplit/Metadata/MetaSepOffset.cs
===================================================================
--- /OniSplit/Metadata/MetaSepOffset.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaSepOffset.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaSepOffset : MetaType
+    {
+        internal MetaSepOffset() : base("SepOffset", 4)
+        {
+        }
+
+        protected override bool IsLeafImpl() => false;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitSepOffset(this);
+    }
+}
Index: /OniSplit/Metadata/MetaString.cs
===================================================================
--- /OniSplit/Metadata/MetaString.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaString.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaString : MetaArray
+    {
+        public MetaString(int length) : base(Char, length)
+        {
+            Name = "String";
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitString(this);
+    }
+}
Index: /OniSplit/Metadata/MetaStruct.cs
===================================================================
--- /OniSplit/Metadata/MetaStruct.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaStruct.cs	(revision 1114)
@@ -0,0 +1,62 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Metadata
+{
+    internal class MetaStruct : MetaType
+    {
+        private readonly List<Field> fields = new List<Field>();
+        private readonly MetaStruct baseStruct;
+
+        public MetaStruct(params Field[] declaredFields)
+            : this(null, null, declaredFields)
+        {
+        }
+
+        public MetaStruct(MetaStruct baseStruct, params Field[] declaredFields)
+            : this(null, null, declaredFields)
+        {
+        }
+
+        public MetaStruct(string name, params Field[] declaredFields)
+            : this(name, null, declaredFields)
+        {
+        }
+
+        public MetaStruct(string name, MetaStruct baseStruct, params Field[] declaredFields)
+        {
+            this.baseStruct = baseStruct;
+
+            if (baseStruct != null)
+                fields.AddRange(baseStruct.fields);
+
+            fields.AddRange(declaredFields);
+
+            int size = 0;
+
+            foreach (var field in fields)
+                size += field.Type.Size;
+
+            Name = name;
+            Size = size;
+        }
+
+        public IEnumerable<Field> Fields => fields;
+
+        protected override bool IsLeafImpl()
+        {
+            foreach (var field in fields)
+            {
+                if (!field.Type.IsLeaf)
+                    return false;
+            }
+
+            return true;
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor)
+        {
+            visitor.VisitStruct(this);
+        }
+    }
+}
Index: /OniSplit/Metadata/MetaType.cs
===================================================================
--- /OniSplit/Metadata/MetaType.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaType.cs	(revision 1114)
@@ -0,0 +1,168 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Metadata
+{
+    internal abstract class MetaType
+    {
+        private string name;
+        private int size;
+        private bool isFixedSize;
+        private bool? isLeaf;
+
+        protected MetaType()
+        {
+        }
+
+        protected MetaType(string name, int size)
+        {
+            this.size = size;
+            this.name = name;
+        }
+
+        #region Primitive Types
+
+        public static readonly MetaChar Char = new MetaChar();
+        public static readonly MetaByte Byte = new MetaByte();
+        public static readonly MetaInt16 Int16 = new MetaInt16();
+        public static readonly MetaUInt16 UInt16 = new MetaUInt16();
+        public static readonly MetaInt32 Int32 = new MetaInt32();
+        public static readonly MetaUInt32 UInt32 = new MetaUInt32();
+        public static readonly MetaInt64 Int64 = new MetaInt64();
+        public static readonly MetaUInt64 UInt64 = new MetaUInt64();
+        public static readonly MetaColor Color = new MetaColor();
+        public static readonly MetaFloat Float = new MetaFloat();
+        public static readonly MetaVector2 Vector2 = new MetaVector2();
+        public static readonly MetaVector3 Vector3 = new MetaVector3();
+        public static readonly MetaQuaternion Quaternion = new MetaQuaternion();
+        public static readonly MetaPlane Plane = new MetaPlane();
+        public static readonly MetaBoundingBox BoundingBox = new MetaBoundingBox();
+        public static readonly MetaBoundingSphere BoundingSphere = new MetaBoundingSphere();
+        public static readonly MetaMatrix4x3 Matrix4x3 = new MetaMatrix4x3();
+        public static readonly MetaRawOffset RawOffset = new MetaRawOffset();
+        public static readonly MetaSepOffset SepOffset = new MetaSepOffset();
+        public static readonly MetaString String16 = new MetaString(16);
+        public static readonly MetaString String32 = new MetaString(32);
+        public static readonly MetaString String48 = new MetaString(48);
+        public static readonly MetaString String63 = new MetaString(63);
+        public static readonly MetaString String64 = new MetaString(64);
+        public static readonly MetaString String128 = new MetaString(128);
+        public static readonly MetaString String256 = new MetaString(256);
+
+        public static MetaPadding Padding(int length) => new MetaPadding(length);
+
+        public static MetaPadding Padding(int length, byte fillByte) => new MetaPadding(length, fillByte);
+
+        public static MetaArray Array(int length, MetaType elementType) => new MetaArray(elementType, length);
+
+        public static MetaVarArray ShortVarArray(MetaType elementType) => new MetaVarArray(Int16, elementType);
+
+        public static MetaVarArray VarArray(MetaType elementType) => new MetaVarArray(Int32, elementType);
+
+        public static MetaString String(int length)
+        {
+            switch (length)
+            {
+                case 16:
+                    return String16;
+                case 32:
+                    return String32;
+                case 64:
+                    return String64;
+                case 128:
+                    return String128;
+                case 256:
+                    return String256;
+                default:
+                    return new MetaString(length);
+            }
+        }
+
+        public static MetaPointer Pointer(TemplateTag tag) => new MetaPointer(tag);
+
+        public static MetaEnum Enum<T>()
+        {
+            Type underlyingType = System.Enum.GetUnderlyingType(typeof(T));
+
+            if (underlyingType == typeof(Byte))
+                return new MetaEnum(MetaType.Byte, typeof(T));
+            if (underlyingType == typeof(Int16))
+                return new MetaEnum(MetaType.Int16, typeof(T));
+            if (underlyingType == typeof(UInt16))
+                return new MetaEnum(MetaType.UInt16, typeof(T));
+            if (underlyingType == typeof(Int32))
+                return new MetaEnum(MetaType.Int32, typeof(T));
+            if (underlyingType == typeof(UInt32))
+                return new MetaEnum(MetaType.UInt32, typeof(T));
+            if (underlyingType == typeof(Int64))
+                return new MetaEnum(MetaType.Int64, typeof(T));
+            if (underlyingType == typeof(UInt64))
+                return new MetaEnum(MetaType.UInt64, typeof(T));
+
+            throw new InvalidOperationException(System.String.Format("Unsupported enum type {0}", underlyingType));
+        }
+
+        #endregion
+
+        public string Name
+        {
+            get { return name; }
+            protected set { name = value; }
+        }
+
+        public int Size
+        {
+            get { return size; }
+            protected set { size = value; }
+        }
+
+        public bool IsFixedSize
+        {
+            get { return isFixedSize; }
+            protected set { isFixedSize = value; }
+        }
+
+        public bool IsLeaf
+        {
+            get
+            {
+                if (!isLeaf.HasValue)
+                    isLeaf = IsLeafImpl();
+
+                return isLeaf.Value;
+            }
+        }
+
+        protected abstract bool IsLeafImpl();
+
+        public bool IsBlittable
+        {
+            get
+            {
+                return (this == MetaType.Byte
+                    || this == MetaType.Int16
+                    || this == MetaType.UInt16
+                    || this == MetaType.Int32
+                    || this == MetaType.UInt32
+                    || this == MetaType.Int64
+                    || this == MetaType.UInt64
+                    || this == MetaType.Float
+                    || this == MetaType.Color
+                    || this == MetaType.Matrix4x3
+                    || this == MetaType.Plane
+                    || this == MetaType.Quaternion
+                    || this == MetaType.Vector2
+                    || this == MetaType.Vector3);
+            }
+        }
+
+        public abstract void Accept(IMetaTypeVisitor visitor);
+
+        internal int Copy(BinaryReader input, BinaryWriter output, Action<CopyVisitor> callback)
+        {
+            var copyVisitor = new CopyVisitor(input, output, callback);
+            Accept(copyVisitor);
+            return copyVisitor.Position;
+        }
+    }
+}
Index: /OniSplit/Metadata/MetaTypeVisitor.cs
===================================================================
--- /OniSplit/Metadata/MetaTypeVisitor.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaTypeVisitor.cs	(revision 1114)
@@ -0,0 +1,113 @@
+﻿namespace Oni.Metadata
+{
+    internal abstract class MetaTypeVisitor : IMetaTypeVisitor
+    {
+        public virtual void VisitStruct(MetaStruct type)
+        {
+        }
+
+        public virtual void VisitArray(MetaArray type)
+        {
+        }
+
+        public virtual void VisitVarArray(MetaVarArray type)
+        {
+        }
+
+        public virtual void VisitEnum(MetaEnum type)
+        {
+            type.BaseType.Accept(this);
+        }
+
+        public virtual void VisitByte(MetaByte type)
+        {
+        }
+
+        public virtual void VisitInt16(MetaInt16 type)
+        {
+        }
+
+        public virtual void VisitUInt16(MetaUInt16 type)
+        {
+        }
+
+        public virtual void VisitInt32(MetaInt32 type)
+        {
+        }
+
+        public virtual void VisitUInt32(MetaUInt32 type)
+        {
+        }
+
+        public virtual void VisitInt64(MetaInt64 type)
+        {
+        }
+
+        public virtual void VisitUInt64(MetaUInt64 type)
+        {
+        }
+
+        public virtual void VisitFloat(MetaFloat type)
+        {
+        }
+
+        public virtual void VisitString(MetaString type)
+        {
+        }
+
+        public virtual void VisitColor(MetaColor type)
+        {
+        }
+
+        public virtual void VisitVector2(MetaVector2 type)
+        {
+            VisitStruct(type);
+        }
+
+        public virtual void VisitVector3(MetaVector3 type)
+        {
+            VisitStruct(type);
+        }
+
+        public virtual void VisitQuaternion(MetaQuaternion type)
+        {
+            VisitStruct(type);
+        }
+
+        public virtual void VisitMatrix4x3(MetaMatrix4x3 type)
+        {
+            VisitStruct(type);
+        }
+
+        public virtual void VisitPlane(MetaPlane type)
+        {
+            VisitStruct(type);
+        }
+
+        public virtual void VisitBoundingSphere(MetaBoundingSphere type)
+        {
+            VisitStruct(type);
+        }
+
+        public virtual void VisitBoundingBox(MetaBoundingBox type)
+        {
+            VisitStruct(type);
+        }
+
+        public virtual void VisitPointer(MetaPointer type)
+        {
+        }
+
+        public virtual void VisitRawOffset(MetaRawOffset type)
+        {
+        }
+
+        public virtual void VisitSepOffset(MetaSepOffset type)
+        {
+        }
+
+        public virtual void VisitPadding(MetaPadding type)
+        {
+        }
+    }
+}
Index: /OniSplit/Metadata/MetaUInt16.cs
===================================================================
--- /OniSplit/Metadata/MetaUInt16.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaUInt16.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaUInt16 : MetaPrimitiveType
+    {
+        internal MetaUInt16() : base("UInt16", 2)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitUInt16(this);
+    }
+}
Index: /OniSplit/Metadata/MetaUInt32.cs
===================================================================
--- /OniSplit/Metadata/MetaUInt32.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaUInt32.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaUInt32 : MetaPrimitiveType
+    {
+        internal MetaUInt32() : base("UInt32", 4)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitUInt32(this);
+    }
+}
Index: /OniSplit/Metadata/MetaUInt64.cs
===================================================================
--- /OniSplit/Metadata/MetaUInt64.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaUInt64.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaUInt64 : MetaPrimitiveType
+    {
+        internal MetaUInt64() : base("UInt64", 8)
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitUInt64(this);
+    }
+}
Index: /OniSplit/Metadata/MetaVarArray.cs
===================================================================
--- /OniSplit/Metadata/MetaVarArray.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaVarArray.cs	(revision 1114)
@@ -0,0 +1,30 @@
+﻿using System;
+
+namespace Oni.Metadata
+{
+    internal class MetaVarArray : MetaType
+    {
+        private readonly MetaType elementType;
+        private readonly Field lengthField;
+
+        public MetaVarArray(MetaType lengthType, MetaType elementType)
+        {
+            if (lengthType != MetaType.Int16 && lengthType != MetaType.Int32)
+                throw new ArgumentException("lengthType must be Int16 or Int32", "lengthType");
+
+            this.lengthField = new Field(lengthType, "Length");
+            this.elementType = elementType;
+
+            Name = string.Format("{0}[{1}]", elementType.Name, lengthType.Name);
+            Size = lengthType.Size;
+        }
+
+        public Field CountField => lengthField;
+
+        public MetaType ElementType => elementType;
+
+        protected override bool IsLeafImpl() => elementType.IsLeaf;
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitVarArray(this);
+    }
+}
Index: /OniSplit/Metadata/MetaVector2.cs
===================================================================
--- /OniSplit/Metadata/MetaVector2.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaVector2.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaVector2 : MetaStruct
+    {
+        internal MetaVector2()
+            : base("Vector2", new Field(Float, "X"), new Field(Float, "Y"))
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitVector2(this);
+    }
+}
Index: /OniSplit/Metadata/MetaVector3.cs
===================================================================
--- /OniSplit/Metadata/MetaVector3.cs	(revision 1114)
+++ /OniSplit/Metadata/MetaVector3.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Metadata
+{
+    internal class MetaVector3 : MetaStruct
+    {
+        internal MetaVector3()
+            : base("Vector3", new Field(Float, "X"), new Field(Float, "Y"), new Field(Float, "Z"))
+        {
+        }
+
+        public override void Accept(IMetaTypeVisitor visitor) => visitor.VisitVector3(this);
+    }
+}
Index: /OniSplit/Metadata/ObjectMetadata.cs
===================================================================
--- /OniSplit/Metadata/ObjectMetadata.cs	(revision 1114)
+++ /OniSplit/Metadata/ObjectMetadata.cs	(revision 1114)
@@ -0,0 +1,935 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni;
+using Oni.Metadata;
+
+namespace Oni.Metadata
+{
+    internal class ObjectMetadata
+    {
+        internal enum TypeTag
+        {
+            CHAR = 0x43484152,
+            CMBT = 0x434d4254,
+            CONS = 0x434f4e53,
+            DOOR = 0x444f4f52,
+            FLAG = 0x464c4147,
+            FURN = 0x4655524e,
+            MELE = 0x4d454c45,
+            NEUT = 0x4e455554,
+            PART = 0x50415254,
+            PATR = 0x50415452,
+            PWRU = 0x50575255,
+            SNDG = 0x534e4447,
+            TRGV = 0x54524756,
+            TRIG = 0x54524947,
+            TURR = 0x54555252,
+            WEAP = 0x57454150
+        }
+
+        [Flags]
+        public enum ObjectFlags : uint
+        {
+            None = 0x00,
+            Locked = 0x01,
+            PlacedInGame = 0x02,
+            Temporary = 0x04,
+            Gunk = 0x08
+        }
+
+        public static readonly MetaStruct Header = new MetaStruct("Object",
+            new Field(MetaType.Enum<ObjectFlags>(), "Flags"),
+            new Field(MetaType.Vector3, "Position"),
+            new Field(MetaType.Vector3, "Rotation")
+        );
+
+        //
+        // Character Object
+        //
+
+        [Flags]
+        internal enum CharacterFlags : uint
+        {
+            None = 0x00,
+            IsPlayer = 0x01,
+            RandomCostume = 0x02,
+            NotInitiallyPresent = 0x04,
+            NonCombatant = 0x08,
+            CanSpawnMultiple = 0x10,
+            Spawned = 0x20,
+            Unkillable = 0x40,
+            InfiniteAmmo = 0x80,
+            Omniscient = 0x0100,
+            HasLSI = 0x0200,
+            Boss = 0x0400,
+            UpgradeDifficulty = 0x0800,
+            NoAutoDrop = 0x1000
+        }
+
+        internal enum CharacterTeam : uint
+        {
+            Konoko = 0,
+            TCTF = 1,
+            Syndicate = 2,
+            Neutral = 3,
+            SecurityGuard = 4,
+            RogueKonoko = 5,
+            Switzerland = 6,
+            SyndicateAccessory = 7
+        }
+
+        internal enum CharacterJobType : uint
+        {
+            None = 0,
+            Idle = 1,
+            Guard = 2,
+            Patrol = 3,
+            TeamBatle = 4,
+            Combat = 5,
+            Melee = 6,
+            Alarm = 7,
+            Neutral = 8,
+            Panic = 9
+        }
+
+        internal enum CharacterAlertStatus : uint
+        {
+            Lull = 0,
+            Low = 1,
+            Medium = 2,
+            High = 3,
+            Combat = 4
+        }
+
+        internal enum CharacterPursuitMode : uint
+        {
+            None = 0,
+            Forget = 1,
+            GoTo = 2,
+            Wait = 3,
+            Look = 4,
+            Move = 5,
+            Hunt = 6,
+            Glanc = 7,
+        }
+
+        internal enum CharacterPursuitLostBehavior : uint
+        {
+            ReturnToJob = 0,
+            KeepLooking = 1,
+            FindAlarm = 2,
+        }
+
+        public static readonly MetaStruct Character = new MetaStruct("Character",
+            new Field(MetaType.Enum<CharacterFlags>(), "Flags"),
+            new Field(MetaType.String64, "Class"),
+            new Field(MetaType.String32, "Name"),
+            new Field(MetaType.String64, "Weapon"),
+            new Field(new MetaStruct("CharacterScripts",
+                new Field(MetaType.String32, "Spawn"),
+                new Field(MetaType.String32, "Die"),
+                new Field(MetaType.String32, "Combat"),
+                new Field(MetaType.String32, "Alarm"),
+                new Field(MetaType.String32, "Hurt"),
+                new Field(MetaType.String32, "Defeated"),
+                new Field(MetaType.String32, "OutOfAmmo"),
+                new Field(MetaType.String32, "NoPath")), "Scripts"),
+            new Field(MetaType.Int32, "AdditionalHealth"),
+            new Field(new MetaStruct("CharacterJob",
+                new Field(MetaType.Enum<CharacterJobType>(), "Type"),
+                new Field(MetaType.Int16, "PatrolPathId")), "Job"),
+            new Field(new MetaStruct("CharacterBehaviors",
+                new Field(MetaType.Int16, "CombatId"),
+                new Field(MetaType.Int16, "MeleeId"),
+                new Field(MetaType.Int16, "NeutralId")), "Behaviors"),
+            new Field(new MetaStruct("CharacterInventory",
+                new Field(new MetaStruct("Ammo",
+                    new Field(MetaType.Int16, "Use"),
+                    new Field(MetaType.Int16, "Drop")), "Ammo"),
+                new Field(new MetaStruct("EnergyCell",
+                    new Field(MetaType.Int16, "Use"),
+                    new Field(MetaType.Int16, "Drop")), "EnergyCell"),
+                new Field(new MetaStruct("Hypo",
+                    new Field(MetaType.Int16, "Use"),
+                    new Field(MetaType.Int16, "Drop")), "Hypo"),
+                new Field(new MetaStruct("Shield",
+                    new Field(MetaType.Int16, "Use"),
+                    new Field(MetaType.Int16, "Drop")), "Shield"),
+                new Field(new MetaStruct("Invisibility",
+                    new Field(MetaType.Int16, "Use"),
+                    new Field(MetaType.Int16, "Drop")), "Invisibility"),
+                new Field(MetaType.Padding(4))), "Inventory"),
+            new Field(MetaType.Enum<CharacterTeam>(), "Team"),
+            new Field(MetaType.Int32, "AmmoPercentage"),
+            new Field(new MetaStruct("CharacterAlert",
+                new Field(MetaType.Enum<CharacterAlertStatus>(), "Initial"),
+                new Field(MetaType.Enum<CharacterAlertStatus>(), "Minimal"),
+                new Field(MetaType.Enum<CharacterAlertStatus>(), "JobStart"),
+                new Field(MetaType.Enum<CharacterAlertStatus>(), "Investigate")), "Alert"),
+            new Field(MetaType.Int32, "AlarmGroups"),
+            new Field(new MetaStruct("CharacterPursuit",
+                new Field(MetaType.Enum<CharacterPursuitMode>(), "StrongUnseen"),
+                new Field(MetaType.Enum<CharacterPursuitMode>(), "WeakUnseen"),
+                new Field(MetaType.Enum<CharacterPursuitMode>(), "StrongSeen"),
+                new Field(MetaType.Enum<CharacterPursuitMode>(), "WeakSeen"),
+                new Field(MetaType.Enum<CharacterPursuitLostBehavior>(), "Lost")), "Pursuit")
+        );
+
+        //
+        // Combat Behavior Object
+        //
+
+        internal enum CombatBehaviorType : uint
+        {
+            None = 0,
+            Stare = 1,
+            HoldAndFire = 2,
+            FiringCharge = 3,
+            Melee = 4,
+            BarabasShoot = 5,
+            BarabasAdvance = 6,
+            BarabasMelee = 7,
+            SuperNinjaFireball = 8,
+            SuperNinjaAdvance = 9,
+            SuperNinjaMelee = 10,
+            RunForAlarm = 11,
+            MutantMuroMelee = 12,
+            MuroThunderbolt = 13
+        }
+
+        internal enum CombatMeleeOverride : uint
+        {
+            None = 0,
+            IfPunched = 1,
+            Cancelled = 2,
+            ShortRange = 3,
+            MediumRange = 4,
+            AlwaysMelee = 5
+        }
+
+        internal enum CombatNoGunBehavior : uint
+        {
+            Melee = 0,
+            Retreat = 1,
+            RunForAlarm = 2
+        }
+
+        public static readonly MetaStruct CombatProfile = new MetaStruct("CombatProfile",
+            new Field(MetaType.String64, "Name"),
+            new Field(MetaType.Int32, "CombatId"),
+            new Field(new MetaStruct("CMBTBehaviors",
+                new Field(MetaType.Enum<CombatBehaviorType>(), "LongRange"),
+                new Field(MetaType.Enum<CombatBehaviorType>(), "MediumRange"),
+                new Field(MetaType.Enum<CombatBehaviorType>(), "ShortRange"),
+                new Field(MetaType.Enum<CombatBehaviorType>(), "MediumRetreat"),
+                new Field(MetaType.Enum<CombatBehaviorType>(), "LongRetreat")), "Behaviors"),
+            new Field(new MetaStruct("CMBTCombat",
+                new Field(MetaType.Float, "MediumRange"),
+                new Field(MetaType.Enum<CombatMeleeOverride>(), "MeleeOverride"),
+                new Field(MetaType.Enum<CombatNoGunBehavior>(), "NoGunBehavior"),
+                new Field(MetaType.Float, "ShortRange"),
+                new Field(MetaType.Float, "PursuitDistance")), "Combat"),
+            new Field(new MetaStruct("CMBTPanic",
+                new Field(MetaType.Int32, "Hurt"),
+                new Field(MetaType.Int32, "GunFire"),
+                new Field(MetaType.Int32, "Melee"),
+                new Field(MetaType.Int32, "Sight")), "Panic"),
+            new Field(new MetaStruct("CMBTAlarm",
+                new Field(MetaType.Float, "SearchDistance"),
+                new Field(MetaType.Float, "EnemyIgnoreDistance"),
+                new Field(MetaType.Float, "EnemyAttackDistance"),
+                new Field(MetaType.Int32, "DamageThreshold"),
+                new Field(MetaType.Int32, "FightTimer")), "Alarm")
+        );
+
+        //
+        // Console Object
+        //
+
+        [Flags]
+        internal enum ConsoleFlags : ushort
+        {
+            None = 0x00,
+            InitialActive = 0x08,
+            Punch = 0x20,
+            IsAlarm = 0x40
+        }
+
+        public static readonly MetaStruct Console = new MetaStruct("Console",
+            new Field(MetaType.String63, "Class"),
+            new Field(MetaType.Int16, "ConsoleId"),
+            new Field(MetaType.Enum<ConsoleFlags>(), "Flags"),
+            new Field(MetaType.String63, "InactiveTexture"),
+            new Field(MetaType.String63, "ActiveTexture"),
+            new Field(MetaType.String63, "TriggeredTexture")
+        );
+
+        //
+        // Door Object
+        //
+
+        [Flags]
+        internal enum DoorFlags : ushort
+        {
+            None = 0x00,
+            InitialLocked = 0x01,
+            InDoorFrame = 0x04,
+            Manual = 0x10,
+            DoubleDoor = 0x80,
+            Mirror = 0x0100,
+            OneWay = 0x0200,
+            Reverse = 0x0400,
+            Jammed = 0x800,
+            InitialOpen = 0x1000,
+        }
+
+        public static readonly MetaStruct Door = new MetaStruct("Door",
+            new Field(MetaType.String63, "Class"),
+            new Field(MetaType.Int16, "DoorId"),
+            new Field(MetaType.Int16, "KeyId"),
+            new Field(MetaType.Enum<DoorFlags>(), "Flags"),
+            new Field(MetaType.Vector3, "Center"),
+            new Field(MetaType.Float, "SquaredActivationRadius"),
+            new Field(MetaType.String63, "Texture1"),
+            new Field(MetaType.String63, "Texture2")
+        );
+
+        //
+        // Flag Object
+        //
+
+        public static readonly MetaStruct Flag = new MetaStruct("Flag",
+            new Field(MetaType.Color, "Color"),
+            new Field(MetaType.Int16, "Prefix"),
+            new Field(MetaType.Int16, "FlagId"),
+            new Field(MetaType.String128, "Notes")
+        );
+
+        //
+        // Furniture Object
+        //
+
+        public static readonly MetaStruct Furniture = new MetaStruct("Furniture",
+            new Field(MetaType.String32, "Class"),
+            new Field(MetaType.String48, "Particle")
+        );
+
+        //
+        // Melee Object
+        //
+
+        public static readonly MetaStruct MeleeProfile = new MetaStruct("MeleeProfile",
+            new Field(MetaType.Int32, "MeleeId"),
+            new Field(MetaType.String64, "Name"),
+            new Field(MetaType.String64, "CharacterClass"),
+            new Field(MetaType.Int32, "Notice"),
+            new Field(new MetaStruct("MeleeDodge",
+                new Field(MetaType.Int32, "Base"),
+                new Field(MetaType.Int32, "Extra"),
+                new Field(MetaType.Int32, "ExtraDamageThreshold")), "Dodge"),
+            new Field(new MetaStruct("MeleeBlockSkill",
+                new Field(MetaType.Int32, "Single"),
+                new Field(MetaType.Int32, "Group")), "BlockSkill"),
+            new Field(MetaType.Float, "NotBlocked"),
+            new Field(MetaType.Float, "MustChangeStance"),
+            new Field(MetaType.Float, "BlockedButUnblockable"),
+            new Field(MetaType.Float, "BlockedButHasStagger"),
+            new Field(MetaType.Float, "BlockedButHasBlockstun"),
+            new Field(MetaType.Float, "Blocked"),
+            new Field(MetaType.Float, "ThrowDanger"),
+            new Field(MetaType.Int16, "DazedMinFrames"),
+            new Field(MetaType.Int16, "DazedMaxFrames")
+        );
+
+        [Flags]
+        internal enum MeleeTechniqueFlags : uint
+        {
+            None = 0x00,
+            Interruptible = 0x01,
+            GenerousDir = 0x02,
+            Fearless = 0x04
+        }
+
+        public static readonly MetaStruct MeleeTechnique = new MetaStruct("MeleeTechnique",
+            new Field(MetaType.String64, "Name"),
+            new Field(MetaType.Enum<MeleeTechniqueFlags>(), "Flags"),
+            new Field(MetaType.UInt32, "Weight"),
+            new Field(MetaType.UInt32, "Importance"),
+            new Field(MetaType.UInt32, "RepeatDelay")
+        );
+
+        internal enum MeleeMoveCategory
+        {
+            Attack = 0,
+            Position = 16,
+            Maneuver = 32,
+            Evade = 48,
+            Throw = 64
+        }
+
+        internal enum MeleeMoveAttackType
+        {
+            P,
+            PP,
+            PPP,
+            PPPP,
+            PF,
+            PL,
+            PR,
+            PB,
+            PD,
+            PF_PF,
+            PF_PF_PF,
+            PL_PL,
+            PL_PL_PL,
+            PR_PR,
+            PR_PR_PR,
+            PB_PB,
+            PB_PB_PB,
+            PD_PD,
+            PD_PD_PD,
+            K,
+            KK,
+            KKK,
+            KKKF,
+            KF,
+            KL,
+            KR,
+            KB,
+            KD,
+            KF_KF,
+            KF_KF_KF,
+            KL_KL,
+            KL_KL_KL,
+            KR_KR,
+            KR_KR_KR,
+            KB_KB,
+            KB_KB_KB,
+            KD_KD,
+            KD_KD_KD,
+            PPK,
+            PKK,
+            PKP,
+            KPK,
+            KPP,
+            KKP,
+            PK,
+            KP,
+            PPKK,
+            PPKKK,
+            PPKKKKK,
+            HP,
+            HPF,
+            HK,
+            HKF,
+            CS_P,
+            CS_K,
+            C_P1,
+            C_P2,
+            C_PF,
+            C_K1,
+            C_K2,
+            C_KF,
+            GETUP_KF,
+            GETUP_KB,
+            R_P,
+            R_K,
+            RB_P,
+            RB_K,
+            RL_P,
+            RL_K,
+            RR_P,
+            RR_K,
+            R_SLIDE,
+            J_P,
+            J_K,
+            JF_P,
+            JF_PB,
+            JF_K,
+            JF_KB,
+            JB_P,
+            JB_K,
+            JL_P,
+            JL_K,
+            JR_P,
+            JR_K
+        }
+
+        internal enum MeleeMovePositionType
+        {
+            RunForward,
+            RunLeft,
+            RunRight,
+            RunBack,
+
+            JumpUp,
+            JumpForward,
+            JumpLeft,
+            JumpRight,
+            JumpBack,
+            StartToCrouch,
+            Crouch,
+            Stand,
+
+            CloseForward,
+            CloseLeft,
+            CloseRight,
+            CloseBack,
+
+            RunJumpForward,
+            RunJumpLeft,
+            RunJumpRight,
+            RunJumpBack
+        }
+
+        internal enum MeleeMoveManeuverType
+        {
+            Advance,			// Duration, MinRange, ThresholdRange
+            Retreat,			// Duration, MaxRange, ThresholdRange
+            CircleLeft,			// Duration, MinAngle, MaxAngle
+            CircleRight,		// Duration, MinAngle, MaxAngle
+            Pause,				// Duration
+            Crouch,				// Duration
+            Jump,				// Duration
+            Taunt,				// Duration
+            RandomStop,			// Chance
+            GetUpForward,		// Duration
+            GetUpBackward,		// Duration
+            GetUpRollLeft,		// Duration
+            GetUpRollRight,		// Duration
+            BarabasWave			// MaxRange
+        }
+
+        internal class MeleeMoveTypeInfo
+        {
+            public MeleeMoveManeuverType Type;
+            public string[] ParamNames;
+
+            public MeleeMoveTypeInfo(MeleeMoveManeuverType type, params string[] paramNames)
+            {
+                Type = type;
+                ParamNames = paramNames;
+            }
+        }
+
+        public static readonly MeleeMoveTypeInfo[] MeleeMoveManeuverTypeInfo = new MeleeMoveTypeInfo[]
+        {
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.Advance, "Duration", "MinRange", "ThresholdRange"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.Retreat, "Duration", "MaxRange", "ThresholdRange"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.CircleLeft, "Duration", "MinAngle", "MaxAngle"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.CircleRight, "Duration", "MinAngle", "MaxAngle"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.Pause, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.Crouch, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.Jump, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.Taunt, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.RandomStop, "Chance"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.GetUpForward, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.GetUpBackward, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.GetUpRollLeft, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.GetUpRollRight, "Duration"),
+            new MeleeMoveTypeInfo(MeleeMoveManeuverType.BarabasWave, "MaxRange")
+        };
+
+        internal enum MeleeMoveEvadeType
+        {
+            JumpForward,
+            JumpForward2,
+            JumpBack,
+            JumpBack2,
+            JumpLeft,
+            JumpLeft2,
+            JumpRight,
+            JumpRight2,
+            RunJumpForward,
+            RunJumpForward2,
+            RunJumpBack,
+            RunJumpBack2,
+            RunJumpLeft,
+            RunJumpLeft2,
+            RunJumpRight,
+            RunJumpRight2,
+            RollForward,
+            RollBackward,
+            RollLeft,
+            RollRight,
+            SlideForward,
+            SlideBack,
+            SlideLeft,
+            SlideRight
+        }
+
+        internal enum MeleeMoveThrowType
+        {
+            P_Front,
+            K_Front,
+            P_Behind,
+            K_Behind,
+            RP_Front,
+            RK_Front,
+            RP_Behind,
+            RK_Behind,
+            P_FrontDisarm,
+            K_FrontDisarm,
+            P_BehindDisarm,
+            K_BehindDisarm,
+            RP_FrontDisarm,
+            RK_FrontDisarm,
+            RP_BehindDisarm,
+            RK_BehindDisarm,
+            P_FrontRifDisarm,
+            K_FrontRifDisarm,
+            P_BehindRifDisarm,
+            K_BehindRifDisarm,
+            RP_FrontRifDisarm,
+            RK_FrontRifDisarm,
+            RP_BehindRifDisarm,
+            RK_BehindRifDisarm,
+            Tackle
+        }
+
+        public static readonly MetaStruct MeleeMove = new MetaStruct("MeleeMove",
+            new Field(MetaType.Int32, "Type"),
+            new Field(MetaType.Float, "Param1"),
+            new Field(MetaType.Float, "Param2"),
+            new Field(MetaType.Float, "Param3")
+        );
+
+        //
+        // Neutral Object
+        //
+
+        [Flags]
+        internal enum NeutralFlags : uint
+        {
+            None = 0x00,
+            NoResume = 0x01,
+            NoResumeAfterGive = 0x02,
+            Uninterruptible = 0x04
+        }
+
+        [Flags]
+        public enum NeutralItems : byte
+        {
+            None = 0,
+            Shield = 1,
+            Invisibility = 2,
+            LSI = 4
+        }
+
+        public static readonly MetaStruct NeutralBehavior = new MetaStruct("NeutralBehavior",
+            new Field(MetaType.String32, "Name"),
+            new Field(MetaType.Int16, "NeutralId")
+        );
+
+        public static readonly MetaStruct NeutralBehaviorParams = new MetaStruct("NeutralBehaviorParams",
+            new Field(MetaType.Enum<NeutralFlags>(), "Flags"),
+            new Field(new MetaStruct("NeutralBehaviorRange",
+                new Field(MetaType.Float, "Trigger"),
+                new Field(MetaType.Float, "Talk"),
+                new Field(MetaType.Float, "Follow"),
+                new Field(MetaType.Float, "Enemy")), "Ranges"),
+            new Field(new MetaStruct("NeutralehaviorSpeech",
+                new Field(MetaType.String32, "Trigger"),
+                new Field(MetaType.String32, "Abort"),
+                new Field(MetaType.String32, "Enemy")), "Speech"),
+            new Field(new MetaStruct("NeutralBehaviorScript",
+                new Field(MetaType.String32, "AfterTalk")), "Script"),
+            new Field(new MetaStruct("NeutralBehaviorRewards",
+                new Field(MetaType.String32, "WeaponClass"),
+                new Field(MetaType.Byte, "Ammo"),
+                new Field(MetaType.Byte, "EnergyCell"),
+                new Field(MetaType.Byte, "Hypo"),
+                new Field(MetaType.Enum<NeutralItems>(), "Other")), "Rewards")
+        );
+
+        [Flags]
+        public enum NeutralDialogLineFlags : ushort
+        {
+            None = 0x00,
+            IsPlayer = 0x01,
+            GiveItems = 0x02,
+            AnimOnce = 0x04,
+            OtherAnimOnce = 0x08
+        }
+
+        public static readonly MetaStruct NeutralBehaviorDialogLine = new MetaStruct("DialogLine",
+            new Field(MetaType.Enum<NeutralDialogLineFlags>(), "Flags"),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.Int16, "Anim"),
+            new Field(MetaType.Int16, "OtherAnim"),
+            new Field(MetaType.String32, "SpeechName")
+        );
+
+        //
+        // Particle Object
+        //
+
+        [Flags]
+        internal enum ParticleFlags : ushort
+        {
+            None = 0x00,
+            NotInitiallyCreated = 0x02,
+        }
+
+        public static readonly MetaStruct Particle = new MetaStruct("Particle",
+            new Field(MetaType.String64, "Class"),
+            new Field(MetaType.String48, "Tag"),
+            new Field(MetaType.Enum<ParticleFlags>(), "Flags"),
+            new Field(MetaType.Vector2, "DecalScale")
+        );
+
+        //
+        // Patrol Path Object
+        //
+
+        internal enum PatrolPathPointType
+        {
+            MoveToFlag = 0,
+            Stop = 1,
+            Pause = 2,
+            LookAtFlag = 3,
+            LookAtPoint = 4,
+            MoveAndFaceFlag = 5,
+            Loop = 6,
+            MovementMode = 7,
+            MoveToPoint = 8,
+            LockFacing = 9,
+            MoveThroughFlag = 10,
+            MoveThroughPoint = 11,
+            StopLooking = 12,
+            FreeFacing = 13,
+            GlanceAtFlagFor = 14, // unused ?
+            MoveNearFlag = 15,
+            LoopFrom = 16,
+            Scan = 17,            // unused ?
+            StopScanning = 18,
+            MoveToFlagLookAndWait = 19,
+            CallScript = 20,
+            ForkScript = 21,
+            IgnorePlayer = 22,
+            FaceToFlagAndFire = 23
+        }
+
+        internal enum PatrolPathFacing
+        {
+            Forward = 0,
+            Backward = 1,
+            Left = 1,
+            Right = 2,
+            Stopped = 3
+        }
+
+        internal enum PatrolPathMovementMode
+        {
+            ByAlertLevel = 0,
+            Stop = 1,
+            Crouch = 2,
+            Creep = 3,
+            WalkNoAim = 4,
+            Walk = 5,
+            RunNoAim = 6,
+            Run = 7
+        }
+
+        public static readonly MetaStruct PatrolPath = new MetaStruct("PatrolPath",
+            new Field(MetaType.String32, "Name")
+        );
+
+        public static readonly MetaStruct PatrolPathInfo = new MetaStruct("PatrolPathInfo",
+            new Field(MetaType.Int16, "PatrolId"),
+            new Field(MetaType.Int16, "ReturnToNearest")
+        );
+
+        public static int GetPatrolPathPointSize(PatrolPathPointType pointType)
+        {
+            switch (pointType)
+            {
+                case PatrolPathPointType.IgnorePlayer:
+                    return 1;
+                case PatrolPathPointType.MoveToFlag:
+                case PatrolPathPointType.LookAtFlag:
+                case PatrolPathPointType.MoveAndFaceFlag:
+                case PatrolPathPointType.ForkScript:
+                case PatrolPathPointType.CallScript:
+                    return 2;
+                case PatrolPathPointType.Pause:
+                case PatrolPathPointType.MovementMode:
+                case PatrolPathPointType.LoopFrom:
+                case PatrolPathPointType.LockFacing:
+                    return 4;
+                case PatrolPathPointType.MoveThroughFlag:
+                case PatrolPathPointType.MoveNearFlag:
+                case PatrolPathPointType.GlanceAtFlagFor:
+                case PatrolPathPointType.Scan:
+                    return 6;
+                case PatrolPathPointType.MoveToFlagLookAndWait:
+                case PatrolPathPointType.FaceToFlagAndFire:
+                    return 8;
+                case PatrolPathPointType.LookAtPoint:
+                case PatrolPathPointType.MoveToPoint:
+                    return 12;
+                case PatrolPathPointType.MoveThroughPoint:
+                    return 16;
+                default:
+                    return 0;
+            }
+        }
+
+        //
+        // PowerUp Object
+        //
+
+        internal enum PowerUpClass : uint
+        {
+            Ammo = 0x424D4D41,
+            EnergyCell = 0x454D4D41,
+            Hypo = 0x4F505948,
+            Shield = 0x444C4853,
+            Invisibility = 0x49564E49,
+            LSI = 0x49534C41,
+        }
+
+        public static readonly MetaStruct PowerUp = new MetaStruct("PowerUp",
+            new Field(MetaType.Enum<PowerUpClass>(), "Class")
+        );
+
+        //
+        // Sound Object
+        //
+
+        internal enum SoundVolumeType
+        {
+            Box = 0x564C4D45,
+            Sphere = 0x53504852
+        }
+
+        public static readonly MetaStruct Sound = new MetaStruct("Sound",
+            new Field(MetaType.String32, "Class")
+        );
+
+        public static readonly MetaStruct SoundSphere = new MetaStruct("SoundSphere",
+            new Field(MetaType.Float, "MinRadius"),
+            new Field(MetaType.Float, "MaxRadius")
+        );
+
+        public static readonly MetaStruct SoundParams = new MetaStruct("SoundParams",
+            new Field(MetaType.Float, "Volume"),
+            new Field(MetaType.Float, "Pitch")
+        );
+
+        //
+        // Trigger Volume Object
+        //
+
+        [Flags]
+        public enum TriggerVolumeFlags : uint
+        {
+            None = 0x00,
+            OneTimeEnter = 0x01,
+            OneTimeInside = 0x02,
+            OneTimeExit = 0x04,
+            EnterDisabled = 0x08,
+            InsideDisabled = 0x10,
+            ExitDisabled = 0x20,
+            Disabled = 0x40,
+            PlayerOnly = 0x80
+        }
+
+        public static readonly MetaStruct TriggerVolume = new MetaStruct("TriggerVolume",
+            new Field(MetaType.String63, "Name"),
+            new Field(new MetaStruct("TriggerVolumeScripts",
+                new Field(MetaType.String32, "Entry"),
+                new Field(MetaType.String32, "Inside"),
+                new Field(MetaType.String32, "Exit")), "Scripts"),
+            new Field(MetaType.Byte, "Teams"),
+            new Field(MetaType.Padding(3)),
+            new Field(MetaType.Vector3, "Size"),
+            new Field(MetaType.Int32, "TriggerVolumeId"),
+            new Field(MetaType.Int32, "ParentId"),
+            new Field(MetaType.String128, "Notes"),
+            new Field(MetaType.Enum<TriggerVolumeFlags>(), "Flags")
+        );
+
+        //
+        // Trigger Object
+        //
+
+        [Flags]
+        public enum TriggerFlags : ushort
+        {
+            None = 0,
+            InitialActive = 0x0008,
+            ReverseAnim = 0x0010,
+            PingPong = 0x0020,
+        }
+
+        public static readonly MetaStruct Trigger = new MetaStruct("Trigger",
+            new Field(MetaType.String63, "Class"),
+            new Field(MetaType.Int16, "TriggerId"),
+            new Field(MetaType.Enum<TriggerFlags>(), "Flags"),
+            new Field(MetaType.Color, "LaserColor"),
+            new Field(MetaType.Float, "StartPosition"),
+            new Field(MetaType.Float, "Speed"),
+            new Field(MetaType.Int16, "EmitterCount"),
+            new Field(MetaType.Int16, "TimeOn"),
+            new Field(MetaType.Int16, "TimeOff")
+        );
+
+        //
+        // Turret Object
+        //
+
+        [Flags]
+        internal enum TurretTargetTeams : uint
+        {
+            None = 0x00,
+            Konoko = 0x01,
+            TCTF = 0x02,
+            Syndicate = 0x04,
+            Neutral = 0x08,
+            SecurityGuard = 0x10,
+            RogueKonoko = 0x20,
+            Switzerland = 0x40,
+            SyndicateAccessory = 0x80
+        }
+
+        [Flags]
+        internal enum TurretFlags : ushort
+        {
+            None = 0,
+            InitialActive = 0x0002,
+        }
+
+        public static readonly MetaStruct Turret = new MetaStruct("Turret",
+            new Field(MetaType.String63, "Class"),
+            new Field(MetaType.Int16, "TurretId"),
+            new Field(MetaType.Enum<TurretFlags>(), "Flags"),
+            new Field(MetaType.Padding(36)),
+            new Field(MetaType.Enum<TurretTargetTeams>(), "TargetedTeams")
+        );
+
+        //
+        // Weapon Object
+        //
+
+        public static readonly MetaStruct Weapon = new MetaStruct("Weapon",
+            new Field(MetaType.String32, "Class")
+        );
+
+        internal enum EventType
+        {
+            None,
+            Script,
+            ActivateTurret,
+            DeactivateTurret,
+            ActivateConsole,
+            DeactivateConsole,
+            ActivateAlarm,
+            DeactivateAlaram,
+            ActivateTrigger,
+            DeactivateTrigger,
+            LockDoor,
+            UnlockDoor
+        }
+    }
+}
Index: /OniSplit/Metadata/OniMacMetadata.cs
===================================================================
--- /OniSplit/Metadata/OniMacMetadata.cs	(revision 1114)
+++ /OniSplit/Metadata/OniMacMetadata.cs	(revision 1114)
@@ -0,0 +1,64 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Metadata
+{
+    internal class OniMacMetadata : InstanceMetadata
+    {
+        //
+        // Binary Data template
+        //
+
+        private static MetaStruct bina = new MetaStruct("BINAInstance",
+            new Field(MetaType.Int32, "DataSize"),
+            new BinaryPartField(MetaType.SepOffset, "DataOffset", "DataSize")
+        );
+
+        //
+        // Texture Map template
+        //
+
+        private static MetaStruct txmp = new MetaStruct("TXMPInstance",
+            new Field(MetaType.Padding(128)),
+            new Field(MetaType.Enum<TXMPFlags>(), "Flags"),
+            new Field(MetaType.Int16, "Width"),
+            new Field(MetaType.Int16, "Height"),
+            new Field(MetaType.Enum<TXMPFormat>(), "Format"),
+            new Field(MetaType.Pointer(TemplateTag.TXAN), "Animation"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "EnvMap"),
+            new Field(MetaType.Padding(4)),
+            new BinaryPartField(MetaType.SepOffset, "DataOffset"),
+            new Field(MetaType.Padding(8))
+        );
+
+        //
+        // Oni Sound Binary Data template
+        //
+
+        private static MetaStruct osbd = new MetaStruct("OSBDInstance",
+            new Field(MetaType.Int32, "DataSize"),
+            new BinaryPartField(MetaType.SepOffset, "DataOffset", "DataSize")
+        );
+
+        //
+        // Sound Data template
+        //
+
+        private static MetaStruct sndd = new MetaStruct("SNDDInstance",
+            new Field(MetaType.Int32, "Flags"),
+            new Field(MetaType.Int32, "Duration"),
+            new Field(MetaType.Int32, "DataSize"),
+            new BinaryPartField(MetaType.RawOffset, "DataOffset", "DataSize")
+        );
+
+        protected override void InitializeTemplates(IList<Template> templates)
+        {
+            base.InitializeTemplates(templates);
+
+            templates.Add(new Template(TemplateTag.BINA, bina, 0x15e11, "Binary Data"));
+            templates.Add(new Template(TemplateTag.OSBD, osbd, 0x15e3c, "Oni Sound Binary Data"));
+            templates.Add(new Template(TemplateTag.TXMP, txmp, 0x8911eeb5f, "Texture Map"));
+            templates.Add(new Template(TemplateTag.SNDD, sndd, 0x411eb, "Sound Data"));
+        }
+    }
+}
Index: /OniSplit/Metadata/OniPcMetadata.cs
===================================================================
--- /OniSplit/Metadata/OniPcMetadata.cs	(revision 1114)
+++ /OniSplit/Metadata/OniPcMetadata.cs	(revision 1114)
@@ -0,0 +1,79 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Metadata
+{
+    internal class OniPcMetadata : InstanceMetadata
+    {
+        //
+        // Binary Data template
+        //
+
+        private static MetaStruct bina = new MetaStruct("BINAInstance",
+            new Field(MetaType.Int32, "DataSize"),
+            new BinaryPartField(MetaType.RawOffset, "DataOffset", "DataSize")
+        );
+
+        //
+        // Oni Sound Binary Data template
+        //
+
+        private static MetaStruct osbd = new MetaStruct("OSBDInstance",
+            new Field(MetaType.Int32, "DataSize"),
+            new BinaryPartField(MetaType.RawOffset, "DataOffset", "DataSize")
+        );
+
+        //
+        // Texture Map template
+        //
+
+        private static MetaStruct txmp = new MetaStruct("TXMPInstance",
+            new Field(MetaType.Padding(128)),
+            new Field(MetaType.Enum<TXMPFlags>(), "Flags"),
+            new Field(MetaType.UInt16, "Width"),
+            new Field(MetaType.UInt16, "Height"),
+            new Field(MetaType.Enum<TXMPFormat>(), "Format"),
+            new Field(MetaType.Pointer(TemplateTag.TXAN), "Animation"),
+            new Field(MetaType.Pointer(TemplateTag.TXMP), "EnvMap"),
+            new BinaryPartField(MetaType.RawOffset, "DataOffset"),
+            new Field(MetaType.Padding(12))
+        );
+
+        //
+        // Sound Data template
+        //
+
+        private static MetaStruct sndd = new MetaStruct("SNDDInstance",
+            new Field(MetaType.Int32, "WaveHeaderSize"),
+
+            new Field(MetaType.Int16, "Format"),
+            new Field(MetaType.Int16, "ChannelCount"),
+            new Field(MetaType.Int32, "SamplesPerSecond"),
+            new Field(MetaType.Int32, "BytesPerSecond"),
+            new Field(MetaType.Int16, "BlockAlignment"),
+            new Field(MetaType.Int16, "BitsPerSample"),
+            new Field(MetaType.Int16, "AdpcmHeaderSize"),
+
+            new Field(MetaType.Int16, "SamplesPerBlock"),
+            new Field(MetaType.Int16, "CoefficientCount"),
+            new Field(MetaType.Array(7, new MetaStruct("ADPCMCoefficient",
+                new Field(MetaType.Int16, "Coefficient1"),
+                new Field(MetaType.Int16, "Coefficient2")
+            )), "Coefficients"),
+
+            new Field(MetaType.Int16, "Duration"),
+            new Field(MetaType.Int32, "DataSize"),
+            new BinaryPartField(MetaType.RawOffset, "DataOffset", "DataSize")
+        );
+
+        protected override void InitializeTemplates(IList<Template> templates)
+        {
+            base.InitializeTemplates(templates);
+
+            templates.Add(new Template(TemplateTag.BINA, bina, 0xdb41, "Binary Data"));
+            templates.Add(new Template(TemplateTag.OSBD, osbd, 0xdb6c, "Oni Sound Binary Data"));
+            templates.Add(new Template(TemplateTag.TXMP, txmp, 0x891187581, "Texture Map"));
+            templates.Add(new Template(TemplateTag.SNDD, sndd, 0x370578, "Sound Data"));
+        }
+    }
+}
Index: /OniSplit/Metadata/SoundMetadata.cs
===================================================================
--- /OniSplit/Metadata/SoundMetadata.cs	(revision 1114)
+++ /OniSplit/Metadata/SoundMetadata.cs	(revision 1114)
@@ -0,0 +1,133 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Metadata
+{
+    internal class SoundMetadata
+    {
+        public const int OSAm = 0x4f53416d;
+        public const int OSGr = 0x4f534772;
+        public const int OSIm = 0x4f53496d;
+
+        public enum OSAmPriority : uint
+        {
+            Low,
+            Normal,
+            High,
+            Highest
+        }
+
+        [Flags]
+        public enum OSAmFlags : uint
+        {
+            None = 0x0000,
+            InterruptTracksOnStop = 0x0001,
+            PlayOnce = 0x0002,
+            CanPan = 0x0004
+        }
+
+        public static readonly MetaStruct osam4 = new MetaStruct("OSAm",
+            new Field(MetaType.Enum<OSAmPriority>(), "Priority"),
+            new Field(MetaType.Enum<OSAmFlags>(), "Flags"),
+            new Field(new MetaStruct("OSAmDetailTrackProperties",
+                new Field(MetaType.Float, "SphereRadius"),
+                new Field(new MetaStruct(
+                    new Field(MetaType.Float, "Min"),
+                    new Field(MetaType.Float, "Max")),
+                    "ElapsedTime")),
+                "DetailTrackProperties"),
+            new Field(new MetaStruct(
+                new Field(new MetaStruct(
+                    new Field(MetaType.Float, "Min"),
+                    new Field(MetaType.Float, "Max")),
+                    "Distance")),
+                "Volume"),
+            new Field(MetaType.String32, "DetailTrack"),
+            new Field(MetaType.String32, "BaseTrack1"),
+            new Field(MetaType.String32, "BaseTrack2"),
+            new Field(MetaType.String32, "InSound"),
+            new Field(MetaType.String32, "OutSound"));
+
+        public static readonly MetaStruct osam5 = new MetaStruct("OSAm5", osam4,
+            new Field(MetaType.UInt32, "Treshold"));
+
+        public static readonly MetaStruct osam6 = new MetaStruct("OSAm6", osam5,
+            new Field(MetaType.Float, "MinOcclusion"));
+
+        public enum OSImPriority : uint
+        {
+            Low,
+            Normal,
+            High,
+            Highest
+        }
+
+        public static readonly MetaStruct osim3 = new MetaStruct("OSIm3",
+            new Field(MetaType.String32, "Group"),
+            new Field(MetaType.Enum<OSImPriority>(), "Priority"),
+            new Field(new MetaStruct("OSImVolume",
+                new Field(new MetaStruct("OSImDistance",
+                    new Field(MetaType.Float, "Min"),
+                    new Field(MetaType.Float, "Max")),
+                    "Distance"),
+                new Field(new MetaStruct("OSImAngle",
+                    new Field(MetaType.Float, "Min"),
+                    new Field(MetaType.Float, "Max"),
+                    new Field(MetaType.Float, "MinAttenuation")),
+                    "Angle")),
+                "Volume"));
+
+        public static readonly MetaStruct osim4 = new MetaStruct("OSIm4", osim3,
+            new Field(new MetaStruct("OSImAlternateImpulse",
+                new Field(MetaType.UInt32, "Treshold"),
+                new Field(MetaType.String32, "Impulse")), "AlternateImpulse"));
+
+        public static readonly MetaStruct osim5 = new MetaStruct("OSIm5", osim4,
+            new Field(MetaType.Float, "ImpactVelocity"));
+
+        public static readonly MetaStruct osim6 = new MetaStruct("OSIm6", osim5,
+            new Field(MetaType.Float, "MinOcclusion"));
+
+        [Flags]
+        public enum OSGrFlags : ushort
+        {
+            None = 0x0000,
+            PreventRepeat = 0x0001
+        }
+
+        public static readonly MetaStruct osgrPermutation = new MetaStruct("Permutation",
+                new Field(MetaType.Int32, "Weight"),
+                new Field(new MetaStruct(
+                    new Field(MetaType.Float, "Min"),
+                    new Field(MetaType.Float, "Max")),
+                    "Volume"),
+                new Field(new MetaStruct(
+                    new Field(MetaType.Float, "Min"),
+                    new Field(MetaType.Float, "Max")),
+                    "Pitch"),
+                new Field(MetaType.String32, "Sound"));
+
+        public static readonly MetaStruct osgr1 = new MetaStruct("OSGr",
+            new Field(MetaType.Int32, "NumberOfChannels"),
+            new Field(MetaType.VarArray(osgrPermutation), "Permutations"));
+
+        public static readonly MetaStruct osgr2 = new MetaStruct("OSGr",
+            new Field(MetaType.Float, "Volume"),
+            new Field(MetaType.Int32, "NumberOfChannels"),
+            new Field(MetaType.VarArray(osgrPermutation), "Permutations"));
+
+        public static readonly MetaStruct osgr3 = new MetaStruct("OSGr",
+            new Field(MetaType.Float, "Volume"),
+            new Field(MetaType.Float, "Pitch"),
+            new Field(MetaType.Int32, "NumberOfChannels"),
+            new Field(MetaType.VarArray(osgrPermutation), "Permutations"));
+
+        public static readonly MetaStruct osgr6 = new MetaStruct("OSGr",
+            new Field(MetaType.Float, "Volume"),
+            new Field(MetaType.Float, "Pitch"),
+            new Field(MetaType.Enum<OSGrFlags>(), "Flags"),
+            new Field(MetaType.Padding(2)),
+            new Field(MetaType.Int32, "NumberOfChannels"),
+            new Field(MetaType.VarArray(osgrPermutation), "Permutations"));
+    }
+}
Index: /OniSplit/Metadata/XmlReaderExtensions.cs
===================================================================
--- /OniSplit/Metadata/XmlReaderExtensions.cs	(revision 1114)
+++ /OniSplit/Metadata/XmlReaderExtensions.cs	(revision 1114)
@@ -0,0 +1,17 @@
+﻿using System.Xml;
+
+namespace Oni.Metadata
+{
+    internal static class XmlReaderExtensions
+    {
+        public static T ReadElementContentAsEnum<T>(this XmlReader xml) where T : struct
+        {
+            return Metadata.MetaEnum.Parse<T>(xml.ReadElementContentAsString());
+        }
+
+        public static T ReadElementContentAsEnum<T>(this XmlReader xml, string name) where T : struct
+        {
+            return Metadata.MetaEnum.Parse<T>(xml.ReadElementContentAsString(name, ""));
+        }
+    }
+}
Index: /OniSplit/Motoko/Geometry.cs
===================================================================
--- /OniSplit/Motoko/Geometry.cs	(revision 1114)
+++ /OniSplit/Motoko/Geometry.cs	(revision 1114)
@@ -0,0 +1,32 @@
+﻿using System;
+
+namespace Oni.Motoko
+{
+    internal class Geometry
+    {
+        private InstanceDescriptor texture;
+
+        public string Name;
+        public Vector3[] Points;
+        public Vector3[] Normals;
+        public Vector2[] TexCoords;
+        public int[] Triangles;
+
+        public string TextureName;
+        public bool HasTransform;
+        public Matrix Transform;
+
+        public InstanceDescriptor Texture
+        {
+            get
+            {
+                return texture;
+            }
+            set
+            {
+                texture = value;
+                TextureName = value == null ? null : value.Name;
+            }
+        }
+    }
+}
Index: /OniSplit/Motoko/GeometryDaeReader.cs
===================================================================
--- /OniSplit/Motoko/GeometryDaeReader.cs	(revision 1114)
+++ /OniSplit/Motoko/GeometryDaeReader.cs	(revision 1114)
@@ -0,0 +1,261 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Motoko
+{
+    internal static class GeometryDaeReader
+    {
+        #region private struct Vertex
+
+        private struct Vertex : IEquatable<Vertex>
+        {
+            public readonly int PositionIndex;
+            public readonly int TexcoordIndex;
+            public readonly int NormalIndex;
+
+            public Vertex(int pointIndex, int uvIndex, int normalIndex)
+            {
+                PositionIndex = pointIndex;
+                TexcoordIndex = uvIndex;
+                NormalIndex = normalIndex;
+            }
+
+            public static bool operator ==(Vertex v1, Vertex v2) => v1.Equals(v2);
+
+            public static bool operator !=(Vertex v1, Vertex v2) => !v1.Equals(v2);
+
+            public bool Equals(Vertex v) => PositionIndex == v.PositionIndex && TexcoordIndex == v.TexcoordIndex && NormalIndex == v.NormalIndex;
+
+            public override bool Equals(object obj) => obj is Vertex && Equals((Vertex)obj);
+
+            public override int GetHashCode() => PositionIndex ^ TexcoordIndex ^ NormalIndex;
+        }
+
+        #endregion
+
+        public static Geometry Read(Dae.Geometry daeGeometry)
+        {
+            return Read(daeGeometry, false, false, 0.0f);
+        }
+
+        public static IEnumerable<Geometry> Read(Dae.Node node, TextureImporter3 textureImporter)
+        {
+            Dae.FaceConverter.Triangulate(node);
+
+            foreach (var daeGeometryInstance in node.GeometryInstances)
+            {
+                var daeGeometry = daeGeometryInstance.Target;
+
+                var geometry = Read(daeGeometry, false, false, 0.0f);
+                geometry.Name = node.Name;
+
+                if (textureImporter != null && daeGeometryInstance.Materials.Count > 0)
+                    geometry.TextureName = textureImporter.AddMaterial(daeGeometryInstance.Materials[0].Target);
+
+                yield return geometry;
+            }
+        }
+
+        public static Geometry Read(Dae.Geometry daeGeometry, bool generateNormals, bool flatNormals, float shellOffset)
+        {
+            if (daeGeometry.Primitives.Count > 1)
+                throw new NotSupportedException(string.Format("Geometry {0}: Multiple primitive groups per mesh are not supported", daeGeometry.Name));
+
+            var primitives = daeGeometry.Primitives[0];
+
+            if (primitives.PrimitiveType == Dae.MeshPrimitiveType.Lines || primitives.PrimitiveType == Dae.MeshPrimitiveType.LineStrips)
+                throw new NotSupportedException(string.Format("Geometry {0}: Line primitives are not supported", daeGeometry.Name));
+
+            var positionIndex = new Dictionary<Vector3, int>();
+            var positions = new List<Vector3>();
+            int[] positionIndices = null;
+
+            var normalIndex = new Dictionary<Vector3, int>();
+            var normals = new List<Vector3>();
+            int[] normalIndices = null;
+
+            var texCoordIndex = new Dictionary<Vector2, int>();
+            var texCoords = new List<Vector2>();
+            int[] texCoordIndices = null;
+
+            foreach (var input in primitives.Inputs)
+            {
+                switch (input.Semantic)
+                {
+                    case Dae.Semantic.Position:
+                        positionIndices = RemoveDuplicates(input, positions, positionIndex, Dae.Source.ReadVector3);
+                        break;
+
+                    case Dae.Semantic.Normal:
+                        if (!generateNormals)
+                            normalIndices = RemoveDuplicates(input, normals, normalIndex, Dae.Source.ReadVector3);
+                        break;
+
+                    case Dae.Semantic.TexCoord:
+                        texCoordIndices = RemoveDuplicates(input, texCoords, texCoordIndex, Dae.Source.ReadTexCoord);
+                        break;
+                }
+            }
+
+            if (texCoordIndices == null)
+                Console.WriteLine("Geometry {0} does not have texture coordinates", daeGeometry.Name);
+
+            if (normalIndices == null)
+                generateNormals = true;
+
+            Vector3[] generatedNormals = null;
+
+            if (generateNormals || shellOffset != 0.0f)
+                generatedNormals = GenerateNormals(positions, positionIndices, flatNormals);
+
+            if (generateNormals)
+            {
+                normals = new List<Vector3>(generatedNormals);
+                normalIndices = positionIndices;
+            }
+
+            int[] shellIndices = null;
+
+            if (shellOffset != 0.0f)
+            {
+                var shellNormals = generatedNormals;
+
+                if (flatNormals)
+                    shellNormals = GenerateNormals(positions, positionIndices, false);
+
+                shellIndices = GenerateShell(positions, positionIndices, shellNormals, shellOffset);
+            }
+
+            var triangles = new int[(shellIndices == null) ? positionIndices.Length : positionIndices.Length + shellIndices.Length];
+            var vertices = new List<Vertex>();
+            var vertexIndex = new Dictionary<Vertex, int>();
+
+            for (int i = 0; i < positionIndices.Length; i++)
+            {
+                var vertex = new Vertex(
+                    positionIndices[i],
+                    (texCoordIndices != null) ? texCoordIndices[i] : -1,
+                    (normalIndices != null) ? normalIndices[i] : -1);
+
+                if (!vertexIndex.TryGetValue(vertex, out triangles[i]))
+                {
+                    triangles[i] = vertices.Count;
+                    vertices.Add(vertex);
+                    vertexIndex.Add(vertex, triangles[i]);
+                }
+            }
+
+            if (shellIndices != null)
+            {
+                for (int i = 0; i < shellIndices.Length; i++)
+                {
+                    var vertex = new Vertex(shellIndices[i], -1, -1);
+                    int j = i + positionIndices.Length;
+
+                    if (!vertexIndex.TryGetValue(vertex, out triangles[j]))
+                    {
+                        triangles[j] = vertices.Count;
+                        vertices.Add(vertex);
+                        vertexIndex.Add(vertex, triangles[j]);
+                    }
+                }
+            }
+
+            if (vertices.Count > 2048)
+                Console.Error.WriteLine("Warning: Geometry {0} has too many vertices ({1})", daeGeometry.Name, vertices.Count);
+
+            var geometry = new Geometry
+            {
+                Points = new Vector3[vertices.Count],
+                Normals = new Vector3[vertices.Count],
+                TexCoords = new Vector2[vertices.Count],
+                Triangles = triangles
+            };
+
+            for (int i = 0; i < vertices.Count; i++)
+            {
+                geometry.Points[i] = positions[vertices[i].PositionIndex];
+
+                if (vertices[i].NormalIndex != -1)
+                    geometry.Normals[i] = normals[vertices[i].NormalIndex];
+
+                if (vertices[i].TexcoordIndex != -1)
+                    geometry.TexCoords[i] = texCoords[vertices[i].TexcoordIndex];
+            }
+
+            return geometry;
+        }
+
+        private static int[] RemoveDuplicates<T>(
+            Dae.IndexedInput input,
+            List<T> list,
+            Dictionary<T, int> index,
+            Func<Dae.Source, int, T> elementReader)
+        {
+            var indices = new int[input.Indices.Count];
+
+            for (int i = 0; i < indices.Length; i++)
+            {
+                var v = elementReader(input.Source, input.Indices[i]);
+
+                if (!index.TryGetValue(v, out indices[i]))
+                {
+                    indices[i] = list.Count;
+                    list.Add(v);
+                    index.Add(v, indices[i]);
+                }
+            }
+
+            return indices;
+        }
+
+        private static Vector3[] GenerateNormals(List<Vector3> positions, int[] triangleList, bool flatNormals)
+        {
+            var autoNormals = new Vector3[positions.Count];
+
+            if (!flatNormals)
+            {
+                for (int i = 0; i < triangleList.Length; i += 3)
+                {
+                    Vector3 p0 = positions[triangleList[i + 0]];
+                    Vector3 p1 = positions[triangleList[i + 1]];
+                    Vector3 p2 = positions[triangleList[i + 2]];
+
+                    Vector3 e1 = p1 - p0;
+                    Vector3 e2 = p2 - p0;
+
+                    Vector3 faceNormal = Vector3.Cross(e1, e2);
+                    float weight = FMath.Atan2(faceNormal.Length(), Vector3.Dot(e1, e2));
+                    faceNormal = Vector3.Normalize(faceNormal) * weight;
+
+                    for (int j = 0; j < 3; j++)
+                        autoNormals[triangleList[i + j]] += faceNormal;
+                }
+
+                for (int i = 0; i < autoNormals.Length; i++)
+                    autoNormals[i].Normalize();
+            }
+
+            return autoNormals;
+        }
+
+        private static int[] GenerateShell(List<Vector3> positions, int[] positionIndices, Vector3[] normals, float offset)
+        {
+            int positionCount = positions.Count;
+
+            for (int i = 0; i < positionCount; i++)
+                positions.Add(positions[i] + normals[i] * offset);
+
+            var shellIndices = new int[positionIndices.Length];
+
+            for (int i = 0; i < positionIndices.Length; i += 3)
+            {
+                shellIndices[i + 0] = positionIndices[i + 2] + positionCount;
+                shellIndices[i + 1] = positionIndices[i + 1] + positionCount;
+                shellIndices[i + 2] = positionIndices[i + 0] + positionCount;
+            }
+
+            return shellIndices;
+        }
+    }
+}
Index: /OniSplit/Motoko/GeometryDaeWriter.cs
===================================================================
--- /OniSplit/Motoko/GeometryDaeWriter.cs	(revision 1114)
+++ /OniSplit/Motoko/GeometryDaeWriter.cs	(revision 1114)
@@ -0,0 +1,119 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Motoko
+{
+    internal class GeometryDaeWriter
+    {
+        private readonly TextureDaeWriter textureWriter;
+
+        public GeometryDaeWriter(TextureDaeWriter textureWriter)
+        {
+            this.textureWriter = textureWriter;
+        }
+
+        public Dae.Node WriteNode(Geometry geometry, string name)
+        {
+            var daeGeometryInstance = WriteGeometryInstance(geometry, name);
+
+            return new Dae.Node
+            {
+                Name = name,
+                Instances = { daeGeometryInstance }
+            };
+        }
+
+        public Dae.GeometryInstance WriteGeometryInstance(Geometry geometry, string name)
+        {
+            var daeGeometry = WriteGeometry(geometry, name);
+            var daeGeometryInstance = new Dae.GeometryInstance(daeGeometry);
+
+            if (geometry.Texture != null)
+            {
+                var daeMaterial = textureWriter.WriteMaterial(geometry.Texture);
+
+                daeGeometryInstance.Materials.Add(new Dae.MaterialInstance("default", daeMaterial)
+                {
+                    Bindings = {
+                        new Dae.MaterialBinding(
+                            semantic: "diffuse_TEXCOORD",
+                            input: daeGeometry.Primitives[0].Inputs.Find(i => i.Semantic == Dae.Semantic.TexCoord))
+                    }
+                });
+            }
+
+            return daeGeometryInstance;
+        }
+
+        private Dae.Geometry WriteGeometry(Geometry geometry, string name)
+        {
+            var points = geometry.Points;
+            var normals = geometry.Normals;
+            var texCoords = geometry.TexCoords;
+
+            if (geometry.HasTransform)
+            {
+                points = Vector3.Transform(points, ref geometry.Transform);
+                normals = Vector3.TransformNormal(normals, ref geometry.Transform);
+            }
+
+            int[] pointMap;
+
+            points = WeldPoints(points, out pointMap);
+
+            var positionInput = new Dae.IndexedInput(Dae.Semantic.Position, new Dae.Source(points));
+            var normalInput = new Dae.IndexedInput(Dae.Semantic.Normal, new Dae.Source(normals));
+            var texCoordInput = new Dae.IndexedInput(Dae.Semantic.TexCoord, new Dae.Source(texCoords));
+
+            var primitives = new Dae.MeshPrimitives(Dae.MeshPrimitiveType.Polygons)
+            {
+                MaterialSymbol = "default",
+                Inputs = { positionInput, normalInput, texCoordInput }
+            };
+
+            for (int triangleStart = 0; triangleStart < geometry.Triangles.Length; triangleStart += 3)
+            {
+                primitives.VertexCounts.Add(3);
+
+                for (int i = 0; i < 3; i++)
+                {
+                    int index = geometry.Triangles[triangleStart + i];
+
+                    positionInput.Indices.Add(pointMap[index]);
+                    texCoordInput.Indices.Add(index);
+                    normalInput.Indices.Add(index);
+                }
+            }
+
+            return new Dae.Geometry
+            {
+                Name = name,
+                Vertices = { positionInput },
+                Primitives = { primitives }
+            };
+        }
+
+        private static T[] WeldPoints<T>(T[] list, out int[] map)
+        {
+            var indicesMap = new int[list.Length];
+            var index = new Dictionary<T, int>(list.Length);
+            var result = new List<T>(list.Length);
+
+            for (int i = 0; i < indicesMap.Length; i++)
+            {
+                var v = list[i];
+
+                if (!index.TryGetValue(v, out indicesMap[i]))
+                {
+                    indicesMap[i] = result.Count;
+                    result.Add(v);
+                    index.Add(v, indicesMap[i]);
+                }
+            }
+
+            map = indicesMap;
+
+            return result.ToArray();
+        }
+    }
+}
Index: /OniSplit/Motoko/GeometryDatReader.cs
===================================================================
--- /OniSplit/Motoko/GeometryDatReader.cs	(revision 1114)
+++ /OniSplit/Motoko/GeometryDatReader.cs	(revision 1114)
@@ -0,0 +1,114 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Motoko
+{
+    internal static class GeometryDatReader
+    {
+        public static Geometry Read(InstanceDescriptor m3gm)
+        {
+            if (m3gm.Template.Tag != TemplateTag.M3GM)
+                throw new ArgumentException(string.Format("Invalid instance type {0}", m3gm.Template.Tag), "m3gm");
+
+            InstanceDescriptor pnta;
+            InstanceDescriptor vcra1;
+            InstanceDescriptor vcra2;
+            InstanceDescriptor txca;
+            InstanceDescriptor idxa1;
+            InstanceDescriptor idxa2;
+            InstanceDescriptor txmp;
+
+            using (var reader = m3gm.OpenRead(4))
+            {
+                pnta = reader.ReadInstance();
+                vcra1 = reader.ReadInstance();
+                vcra2 = reader.ReadInstance();
+                txca = reader.ReadInstance();
+                idxa1 = reader.ReadInstance();
+                idxa2 = reader.ReadInstance();
+                txmp = reader.ReadInstance();
+            }
+
+            var geometry = new Geometry {
+                Name = m3gm.FullName,
+                Texture = txmp
+            };
+
+            Vector3[] faceNormals;
+            int[] faceIndices;
+            int[] vertexIndices;
+
+            using (var reader = pnta.OpenRead(52))
+                geometry.Points = reader.ReadVector3Array(reader.ReadInt32());
+
+            using (var reader = vcra1.OpenRead(20))
+                geometry.Normals = reader.ReadVector3Array(reader.ReadInt32());
+
+            using (var reader = vcra2.OpenRead(20))
+                faceNormals = reader.ReadVector3Array(reader.ReadInt32());
+
+            using (var reader = txca.OpenRead(20))
+                geometry.TexCoords = reader.ReadVector2Array(reader.ReadInt32());
+
+            using (var reader = idxa1.OpenRead(20))
+                vertexIndices = reader.ReadInt32Array(reader.ReadInt32());
+
+            using (var reader = idxa2.OpenRead(20))
+                faceIndices = reader.ReadInt32Array(reader.ReadInt32());
+
+            geometry.Triangles = ConvertTriangleStripToTriangleList(geometry.Points, vertexIndices, faceNormals, faceIndices);
+
+            return geometry;
+        }
+
+        private static int[] ConvertTriangleStripToTriangleList(Vector3[] points, int[] vIndices, Vector3[] fNormals, int[] fIndices)
+        {
+            var triangles = new List<int>(vIndices.Length * 2);
+
+            var face = new int[3];
+            int faceIndex = 0;
+            int order = 0;
+
+            for (int i = 0; i < vIndices.Length; i++)
+            {
+                if (vIndices[i] < 0)
+                {
+                    face[0] = vIndices[i++] & int.MaxValue;
+                    face[1] = vIndices[i++];
+                    order = 0;
+                }
+                else
+                {
+                    face[order] = face[2];
+                    order ^= 1;
+                }
+
+                face[2] = vIndices[i];
+
+                var v1 = points[face[0]];
+                var v2 = points[face[1]];
+                var v3 = points[face[2]];
+
+                var faceNormal1 = Vector3.Normalize(fNormals[fIndices[faceIndex]]);
+                var faceNormal2 = Vector3.Normalize(Vector3.Cross(v2 - v1, v3 - v1));
+
+                if (Vector3.Dot(faceNormal1, faceNormal2) < 0.0f)
+                {
+                    triangles.Add(face[2]);
+                    triangles.Add(face[1]);
+                    triangles.Add(face[0]);
+                }
+                else
+                {
+                    triangles.Add(face[0]);
+                    triangles.Add(face[1]);
+                    triangles.Add(face[2]);
+                }
+
+                faceIndex++;
+            }
+
+            return triangles.ToArray();
+        }
+    }
+}
Index: /OniSplit/Motoko/GeometryDatWriter.cs
===================================================================
--- /OniSplit/Motoko/GeometryDatWriter.cs	(revision 1114)
+++ /OniSplit/Motoko/GeometryDatWriter.cs	(revision 1114)
@@ -0,0 +1,75 @@
+﻿namespace Oni.Motoko
+{
+    internal class GeometryDatWriter
+    {
+        private Geometry geometry;
+        private ImporterFile importer;
+
+        public static ImporterDescriptor Write(Geometry geometry, ImporterFile importer)
+        {
+            var writer = new GeometryDatWriter
+            {
+                geometry = geometry,
+                importer = importer
+            };
+
+            return writer.WriteGeometry();
+        }
+
+        private ImporterDescriptor WriteGeometry()
+        {
+            var triangleStrips = Stripify.FromTriangleList(geometry.Triangles);
+            var triangleList = Stripify.ToTriangleList(triangleStrips);
+
+            var faceNormals = new Vector3[triangleList.Length / 3];
+            var faceIndices = new int[faceNormals.Length];
+
+            for (int i = 0; i < triangleList.Length; i += 3)
+            {
+                var p0 = geometry.Points[triangleList[i + 0]];
+                var p1 = geometry.Points[triangleList[i + 1]];
+                var p2 = geometry.Points[triangleList[i + 2]];
+
+                var faceNormal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
+                int faceIndex = i / 3;
+
+                faceNormals[faceIndex] = faceNormal;
+                faceIndices[faceIndex] = faceIndex;
+            }
+
+            var m3gm = importer.CreateInstance(TemplateTag.M3GM, geometry.Name);
+            var pnta = importer.CreateInstance(TemplateTag.PNTA);
+            var vcra1 = importer.CreateInstance(TemplateTag.VCRA);
+            var vcra2 = importer.CreateInstance(TemplateTag.VCRA);
+            var txca = importer.CreateInstance(TemplateTag.TXCA);
+            var idxa1 = importer.CreateInstance(TemplateTag.IDXA);
+            var idxa2 = importer.CreateInstance(TemplateTag.IDXA);
+
+            using (var writer = m3gm.OpenWrite(4))
+            {
+                writer.Write(pnta);
+                writer.Write(vcra1);
+                writer.Write(vcra2);
+                writer.Write(txca);
+                writer.Write(idxa1);
+                writer.Write(idxa2);
+
+                if (geometry.TextureName != null)
+                    writer.Write(importer.CreateInstance(TemplateTag.TXMP, geometry.TextureName));
+                else
+                    writer.Write(0);
+
+                writer.Skip(4);
+            }
+
+            pnta.WritePoints(geometry.Points);
+            vcra1.WriteVectors(geometry.Normals);
+            vcra2.WriteVectors(faceNormals);
+            txca.WriteTexCoords(geometry.TexCoords);
+            idxa1.WriteIndices(triangleStrips);
+            idxa2.WriteIndices(faceIndices);
+
+            return m3gm;
+        }
+    }
+}
Index: /OniSplit/Motoko/GeometryImporter.cs
===================================================================
--- /OniSplit/Motoko/GeometryImporter.cs	(revision 1114)
+++ /OniSplit/Motoko/GeometryImporter.cs	(revision 1114)
@@ -0,0 +1,153 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+
+namespace Oni.Motoko
+{
+    internal class GeometryImporter : Importer
+    {
+        private readonly bool generateNormals;
+        private readonly bool flatNormals;
+        private readonly float shellOffset;
+        private readonly bool overrideTextureName;
+        private string textureName;
+
+        public GeometryImporter(string[] args)
+        {
+            foreach (string arg in args)
+            {
+                if (arg == "-normals")
+                {
+                    generateNormals = true;
+                }
+                else if (arg == "-flat")
+                {
+                    flatNormals = true;
+                }
+                else if (arg == "-cel" || arg.StartsWith("-cel:", StringComparison.Ordinal))
+                {
+                    int i = arg.IndexOf(':');
+
+                    if (i != -1)
+                        shellOffset = float.Parse(arg.Substring(i + 1), CultureInfo.InvariantCulture);
+                    else
+                        shellOffset = 0.07f;
+                }
+                else if (arg.StartsWith("-tex:", StringComparison.Ordinal))
+                {
+                    textureName = arg.Substring(5);
+                    overrideTextureName = true;
+                }
+            }
+        }
+
+        public ImporterDescriptor Import(string filePath, ImporterFile importer)
+        {
+            var scene = Dae.Reader.ReadFile(filePath);
+
+            Dae.FaceConverter.Triangulate(scene);
+
+            var daeInstances = new List<Dae.GeometryInstance>();
+
+            foreach (var node in scene.Nodes)
+            {
+                var daeInstance = node.GeometryInstances.FirstOrDefault();
+
+                if (daeInstance == null || daeInstance.Target == null)
+                    continue;
+
+                daeInstances.Add(daeInstance);
+            }
+
+            foreach (var daeInstance in daeInstances)
+            {
+                var daeGeometry = daeInstance.Target;
+
+                var geometry = GeometryDaeReader.Read(daeGeometry, generateNormals, flatNormals, shellOffset);
+
+                string textureFilePath = null;
+
+                if (!overrideTextureName && daeInstance.Materials.Count > 0)
+                    textureFilePath = ReadMaterial(daeInstance.Materials[0].Target);
+
+                geometry.TextureName = Path.GetFileNameWithoutExtension(textureFilePath);
+
+                return GeometryDatWriter.Write(geometry, importer);
+            }
+
+            return null;
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            var scene = Dae.Reader.ReadFile(filePath);
+
+            Dae.FaceConverter.Triangulate(scene);
+
+            var daeInstances = new List<Dae.GeometryInstance>();
+
+            foreach (var node in scene.Nodes)
+            {
+                var daeInstance = node.GeometryInstances.FirstOrDefault();
+
+                if (daeInstance == null || daeInstance.Target == null)
+                    continue;
+
+                daeInstances.Add(daeInstance);
+            }
+
+            foreach (var daeInstance in daeInstances)
+            {
+                var daeGeometry = daeInstance.Target;
+
+                var geometry = GeometryDaeReader.Read(daeGeometry, generateNormals, flatNormals, shellOffset);
+                geometry.Name = Path.GetFileNameWithoutExtension(filePath);
+
+                if (daeInstances.Count > 1)
+                    geometry.Name += daeGeometry.Name;
+
+                string textureFilePath = null;
+
+                if (!overrideTextureName && daeInstance.Materials.Count > 0)
+                    textureFilePath = ReadMaterial(daeInstance.Materials[0].Target);
+
+                WriteM3GM(geometry, textureFilePath, outputDirPath);
+            }
+        }
+
+        private void WriteM3GM(Geometry geometry, string textureFilePath, string outputDirPath)
+        {
+            geometry.Name = MakeInstanceName(TemplateTag.M3GM, geometry.Name);
+
+            if (string.IsNullOrEmpty(textureFilePath))
+                geometry.TextureName = textureName;
+            else
+                geometry.TextureName = Path.GetFileNameWithoutExtension(textureFilePath);
+
+            BeginImport();
+
+            GeometryDatWriter.Write(geometry, ImporterFile);
+
+            Write(outputDirPath);
+        }
+
+        private string ReadMaterial(Dae.Material material)
+        {
+            if (material == null || material.Effect == null)
+                return null;
+
+            var texture = material.Effect.Textures.FirstOrDefault(t => t.Channel == Dae.EffectTextureChannel.Diffuse);
+
+            if (texture == null)
+                return null;
+
+            var sampler = texture.Sampler;
+
+            if (sampler == null || sampler.Surface == null || sampler.Surface.InitFrom == null || string.IsNullOrEmpty(sampler.Surface.InitFrom.FilePath))
+                return null;
+
+            return sampler.Surface.InitFrom.FilePath;
+        }
+    }
+}
Index: /OniSplit/Motoko/Quadify.cs
===================================================================
--- /OniSplit/Motoko/Quadify.cs	(revision 1114)
+++ /OniSplit/Motoko/Quadify.cs	(revision 1114)
@@ -0,0 +1,299 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Motoko
+{
+    internal class Quadify
+    {
+        private readonly Geometry mesh;
+        private readonly List<Face> faces;
+
+        public Quadify(Geometry mesh)
+        {
+            this.mesh = mesh;
+
+            faces = new List<Face>();
+
+            for (int i = 0; i < mesh.Triangles.Length; i += 3)
+            {
+                var plane = new Plane(
+                    mesh.Points[mesh.Triangles[i + 0]],
+                    mesh.Points[mesh.Triangles[i + 1]],
+                    mesh.Points[mesh.Triangles[i + 2]]);
+
+                faces.Add(new Face(
+                    mesh,
+                    new[] { mesh.Triangles[i + 0], mesh.Triangles[i + 1], mesh.Triangles[i + 2] },
+                    plane.Normal));
+            }
+        }
+
+        public static List<int[]> Do(Geometry mesh)
+        {
+            var quadrangulate = new Quadify(mesh);
+            return quadrangulate.Execute();
+        }
+
+        public List<int[]> Execute()
+        {
+            GenerateAdjacency();
+
+            var candidates = new List<QuadCandidate>();
+            var newFaces = new int[faces.Count][];
+            var quadified = new bool[faces.Count];
+            int quadCount = 0;
+
+            for (int i = 0; i < faces.Count; i++)
+            {
+                Face f1 = faces[i];
+
+                if (quadified[i])
+                    continue;
+
+                candidates.Clear();
+
+                foreach (Edge e1 in f1.edges)
+                {
+                    foreach (Edge e2 in e1.adjacency)
+                    {
+                        if (quadified[faces.IndexOf(e2.face)])
+                            continue;
+
+                        candidates.Add(new QuadCandidate(e1, e2));
+                    }
+                }
+
+                if (candidates.Count > 0)
+                {
+                    candidates.Sort(new QuadCandidateComparer());
+                    newFaces[i] = candidates[0].CreateQuad();
+
+                    int k = faces.IndexOf(candidates[0].e2.face);
+
+                    quadified[i] = true;
+                    quadified[k] = true;
+
+                    quadCount++;
+                }
+            }
+
+            var quadList = new List<int[]>(faces.Count - quadCount);
+
+            for (int i = 0; i < faces.Count; i++)
+            {
+                if (newFaces[i] != null)
+                    quadList.Add(newFaces[i]);
+                else if (!quadified[i])
+                    quadList.Add(faces[i].indices);
+            }
+
+            return quadList;
+        }
+
+        private void GenerateAdjacency()
+        {
+            var points = mesh.Points;
+            var pointUseCount = new int[points.Length];
+            var pointUsage = new int[points.Length][];
+
+            foreach (Face face in faces)
+            {
+                foreach (int i in face.indices)
+                    pointUseCount[i]++;
+            }
+
+            for (int faceIndex = 0; faceIndex < faces.Count; faceIndex++)
+            {
+                foreach (int pointIndex in faces[faceIndex].indices)
+                {
+                    int useCount = pointUseCount[pointIndex];
+                    int[] usage = pointUsage[pointIndex];
+
+                    if (usage == null)
+                    {
+                        usage = new int[useCount];
+                        pointUsage[pointIndex] = usage;
+                    }
+
+                    usage[usage.Length - useCount] = faceIndex;
+                    pointUseCount[pointIndex] = useCount - 1;
+                }
+            }
+
+            var adjacencyBuffer = new List<Edge>();
+
+            foreach (Face f1 in faces)
+            {
+                foreach (Edge e1 in f1.edges)
+                {
+                    int[] usage0 = pointUsage[e1.Point0Index];
+                    int[] usage1 = pointUsage[e1.Point1Index];
+
+                    if (usage0 == null || usage1 == null)
+                        continue;
+
+                    adjacencyBuffer.Clear();
+
+                    foreach (int adjFaceIndex in MatchSortedArrays(usage0, usage1))
+                    {
+                        Face f2 = faces[adjFaceIndex];
+
+                        if (f2 == f1 || (f2.normal - f1.normal).Length() > 0.01f)
+                            continue;
+
+                        foreach (Edge e2 in f2.edges)
+                        {
+                            if (e1.IsShared(e2))
+                                adjacencyBuffer.Add(e2);
+                        }
+                    }
+
+                    e1.adjacency = adjacencyBuffer.ToArray();
+                }
+            }
+        }
+
+        private static IEnumerable<int> MatchSortedArrays(int[] a1, int[] a2)
+        {
+            int l1 = a1.Length;
+            int l2 = a2.Length;
+            int i1 = 0;
+            int i2 = 0;
+
+            while (i1 < l1 && i2 < l2)
+            {
+                int v1 = a1[i1];
+                int v2 = a2[i2];
+
+                if (v1 < v2)
+                {
+                    i1++;
+                }
+                else if (v1 > v2)
+                {
+                    i2++;
+                }
+                else
+                {
+                    i1++;
+                    i2++;
+
+                    yield return v1;
+                }
+            }
+        }
+
+        private class Face
+        {
+            public readonly Geometry mesh;
+            public readonly int[] indices;
+            public readonly Edge[] edges;
+            public readonly Vector3 normal;
+
+            public Face(Geometry mesh, int[] pointIndices, Vector3 normal)
+            {
+                this.mesh = mesh;
+                this.indices = pointIndices;
+                this.normal = normal;
+
+                edges = new Edge[indices.Length];
+
+                for (int i = 0; i < edges.Length; i++)
+                    edges[i] = new Edge(this, i);
+            }
+        }
+
+        private class Edge
+        {
+            private static readonly Edge[] emptyEdges = new Edge[0];
+
+            public readonly Face face;
+            public readonly int i0;
+            public readonly int i1;
+            public Edge[] adjacency;
+
+            public Edge(Face polygon, int index)
+            {
+                this.face = polygon;
+                this.i0 = index;
+                this.i1 = (index + 1) % face.edges.Length;
+                this.adjacency = emptyEdges;
+            }
+
+            public int Point0Index
+            {
+                get { return face.indices[i0]; }
+            }
+
+            public int Point1Index
+            {
+                get { return face.indices[i1]; }
+            }
+
+            public bool IsShared(Edge e)
+            {
+                return (Point0Index == e.Point1Index && Point1Index == e.Point0Index);
+            }
+        }
+
+        private class QuadCandidateComparer : IComparer<QuadCandidate>
+        {
+            public int Compare(QuadCandidate x, QuadCandidate y)
+            {
+                return x.length.CompareTo(y.length);
+            }
+        }
+
+        private class QuadCandidate
+        {
+            public readonly Edge e1;
+            public readonly Edge e2;
+            public readonly float length;
+
+            public QuadCandidate(Edge e1, Edge e2)
+            {
+                this.e1 = e1;
+                this.e2 = e2;
+
+                Vector3[] points = e1.face.mesh.Points;
+                this.length = (points[e1.Point0Index] - points[e1.Point1Index]).LengthSquared();
+            }
+
+            public int[] CreateQuad()
+            {
+                int[] newPoints = new int[4];
+                int l = 0;
+
+                newPoints[l] = e1.face.indices[e1.i1];
+                l++;
+
+                for (int k = 0; k < 3; k++)
+                {
+                    if (k != e1.i0 && k != e1.i1)
+                    {
+                        newPoints[l] = e1.face.indices[k];
+                        l++;
+
+                        break;
+                    }
+                }
+
+                newPoints[l] = e1.face.indices[e1.i0];
+                l++;
+
+                for (int k = 0; k < 3; k++)
+                {
+                    if (k != e2.i0 && k != e2.i1)
+                    {
+                        newPoints[l] = e2.face.indices[k];
+                        l++;
+
+                        break;
+                    }
+                }
+
+                return newPoints;
+            }
+        }
+    }
+}
Index: /OniSplit/Motoko/Stripify.cs
===================================================================
--- /OniSplit/Motoko/Stripify.cs	(revision 1114)
+++ /OniSplit/Motoko/Stripify.cs	(revision 1114)
@@ -0,0 +1,363 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Motoko
+{
+    internal class Stripify
+    {
+        private const int BeginStrip = int.MinValue;
+        private int[] tlist;
+        private int[] adjacency;
+        private int[] degree;
+        private List<int> strips;
+        private bool[] used;
+
+        public static int[] FromTriangleList(int[] triangleList)
+        {
+            var triStrips = new Stripify(triangleList);
+            return triStrips.Run();
+        }
+
+        public static int[] ToTriangleList(int[] triangleStrips)
+        {
+            int triangleCount = 0;
+
+            for (int i = 0; i < triangleStrips.Length; i++)
+            {
+                triangleCount++;
+
+                if (triangleStrips[i] < 0)
+                    triangleCount -= 2;
+            }
+
+            var triangles = new int[triangleCount * 3];
+            int pos = 0;
+            var face = new int[3];
+            int order = 0;
+
+            for (int i = 0; i < triangleStrips.Length; i++)
+            {
+                if (triangleStrips[i] < 0)
+                {
+                    face[0] = triangleStrips[i] & int.MaxValue;
+                    i++;
+                    face[1] = triangleStrips[i];
+                    i++;
+                    order = 0;
+                }
+                else
+                {
+                    face[order] = face[2];
+                    order = (order + 1) % 2;
+                }
+
+                face[2] = triangleStrips[i];
+
+                Array.Copy(face, 0, triangles, pos, 3);
+                pos += 3;
+            }
+
+            return triangles;
+        }
+
+        private Stripify(int[] triangleList)
+        {
+            tlist = triangleList;
+        }
+
+        private int[] Run()
+        {
+            strips = new List<int>();
+
+            GenerateAdjacency();
+
+            while (GenerateStrip())
+                ;
+
+            //
+            // Generate 1 triangle long strips for all triangles that were not included
+            // in triangle strips
+            //
+
+            for (int i = 0; i < degree.Length; i++)
+            {
+                if (!used[i])
+                {
+                    int j = i * 3;
+
+                    strips.Add(tlist[j + 0] | BeginStrip);
+                    strips.Add(tlist[j + 1]);
+                    strips.Add(tlist[j + 2]);
+
+                    used[i] = true;
+                }
+            }
+
+            return strips.ToArray();
+        }
+
+        private bool GenerateStrip()
+        {
+            int current = -1;
+
+            int minDegree = 4;
+            int minAdjacentDegree = 4;
+
+            //
+            // Find a triangle to start with. The triangle with the lowest degree
+            // is picked as a start triangle. If multiple triangles have the same 
+            // degree then the adjacent triangles are checked for lowest degree.
+            //
+
+            for (int t = 0; t < degree.Length; t++)
+            {
+                if (used[t] || degree[t] == 0)
+                    continue;
+
+                if (degree[t] < minDegree)
+                {
+                    minDegree = degree[t];
+                    minAdjacentDegree = 4;
+                    current = t;
+                }
+                else if (degree[t] == minDegree)
+                {
+                    //
+                    // We have 2 candidates for a start triangle with the same degree.
+                    // Check their neighbours for lowest degree to decide which candidate to use.
+                    //
+
+                    for (int k = 0; k < 3; k++)
+                    {
+                        int a = adjacency[t * 3 + k];
+
+                        if (a == -1 || used[a] || degree[a] == 0)
+                            continue;
+
+                        if (degree[a] < minAdjacentDegree)
+                        {
+                            minAdjacentDegree = degree[a];
+                            current = t;
+                        }
+                    }
+                }
+            }
+
+            if (current == -1)
+            {
+                //
+                // A start triangle cannot be found. Either there are no more unused triangles left
+                // or all remaining triangles have degree = 0.
+                //
+
+                return false;
+            }
+
+            UseTriangle(current);
+
+            //
+            // Find a triangle adjacent to the start triangle so we can decide
+            // on a vertex order for the start triangle. If there are multiple
+            // adjacent triangles the one with lowest degree is used.
+            //
+
+            int next = -1;
+            int edge = 0;
+
+            minDegree = 4;
+
+            for (int e = 0; e < 3; e++)
+            {
+                int a = adjacency[current * 3 + e];
+
+                if (a == -1 || used[a])
+                    continue;
+
+                //
+                // NOTE: Don't check for degree = 0. The previous UseTriangle(current) can make 
+                // adjacent triangles have a 0 degree. It works because all we are interested in
+                // is which adjacent triangle has the lowest degree.
+                //
+
+                if (degree[a] < minDegree)
+                {
+                    minDegree = degree[a];
+                    next = a;
+                    edge = e;
+                }
+            }
+
+            //
+            // Begin a new triangle strip
+            //
+
+            var triangle = new int[3];
+
+            triangle[0] = tlist[(current * 3) + (edge + 2) % 3];
+            triangle[1] = tlist[(current * 3) + (edge + 0) % 3];
+            triangle[2] = tlist[(current * 3) + (edge + 1) % 3];
+
+            strips.Add(triangle[0] | BeginStrip);
+            strips.Add(triangle[1]);
+            strips.Add(triangle[2]);
+
+            //
+            // Continue the triangle strip as long as possible
+            //
+
+            int order = 0;
+
+            while (next != -1)
+            {
+                UseTriangle(next);
+
+                triangle[0] = triangle[1 + order];
+
+                //
+                // Search an edge in triangle "next" that matches the "exit" edge of triangle "current"
+                //
+
+                for (int v = 0; v < 3; v++)
+                {
+                    int t = next * 3;
+
+                    if (tlist[t + v] == triangle[(2 + order) % 3] && tlist[t + (v + 1) % 3] == triangle[order])
+                    {
+                        edge = (v + 2 - order) % 3;
+                        triangle[1 + order] = tlist[t + (v + 2) % 3];
+                        break;
+                    }
+                }
+
+                strips.Add(triangle[1 + order]);
+
+                //
+                // Replace "current" with "next" and find a "next" triangle that is adjacent with "current"
+                //
+
+                current = next;
+                next = adjacency[current * 3 + edge];
+
+                if (next == -1 || used[next])
+                    break;
+
+                UseTriangle(next);
+
+                //
+                // Alternate vertex ordering
+                //
+
+                order = (order + 1) % 2;
+            }
+
+            return true;
+        }
+
+        private void UseTriangle(int t)
+        {
+            degree[t] = 0;
+            used[t] = true;
+
+            //
+            // Decrease the degree of all adjacent triangles by 1.
+            //
+
+            for (int e = 0; e < 3; e++)
+            {
+                int a = adjacency[t * 3 + e];
+
+                if (a != -1 && degree[a] > 0)
+                    degree[a]--;
+            }
+        }
+
+        #region private struct Edge
+
+        private struct Edge : IEquatable<Edge>
+        {
+            public readonly int V1;
+            public readonly int V2;
+
+            public Edge(int V1, int V2)
+            {
+                this.V1 = V1;
+                this.V2 = V2;
+            }
+
+            public static bool operator ==(Edge e1, Edge e2) => e1.V1 == e2.V1 && e1.V2 == e2.V2;
+            public static bool operator !=(Edge e1, Edge e2) => e1.V1 != e2.V1 || e1.V2 != e2.V2;
+            public bool Equals(Edge edge) => V1 == edge.V1 && V2 == edge.V2;
+            public override bool Equals(object obj) => obj is Edge && Equals((Edge)obj);
+            public override int GetHashCode() => V1 ^ V2;
+        }
+
+        #endregion
+
+        private void GenerateAdjacency()
+        {
+            adjacency = new int[tlist.Length];
+            degree = new int[tlist.Length / 3];
+            used = new bool[tlist.Length / 3];
+
+            for (int i = 0; i < adjacency.Length; i++)
+                adjacency[i] = -1;
+
+            //
+            // Store all the edges in a dictionary for easier lookup
+            //
+
+            var edges = new Dictionary<Edge, int>();
+
+            for (int t = 0; t < tlist.Length; t += 3)
+            {
+                for (int v = 0; v < 3; v++)
+                {
+                    var edge = new Edge(tlist[t + v], tlist[t + (v + 1) % 3]);
+
+                    edges[edge] = t / 3;
+                }
+            }
+
+            //
+            // Fill the adjacency array
+            //
+
+            for (int t = 0; t < tlist.Length; t += 3)
+            {
+                for (int e = 0; e < 3; e++)
+                {
+                    //
+                    // We already have an adjacent triangle for this edge.
+                    // This means that there are 3 or more triangles that have a 
+                    // common edge but this is not very common and we'll just 
+                    // ignore it.
+                    //
+
+                    if (adjacency[t + e] != -1)
+                        continue;
+
+                    //
+                    // Notice that the edge must be reversed compared to the
+                    // order they were stored in the dictionary to preserve
+                    // trinangle vertex ordering.
+                    //
+
+                    var edge = new Edge(tlist[t + (e + 1) % 3], tlist[t + e]);
+
+                    int k;
+
+                    //
+                    // Note the k != t / 3 check to avoid making degenerate triangles
+                    // adjacent to themselfs.
+                    //
+
+                    if (edges.TryGetValue(edge, out k) && k != t / 3)
+                    {
+                        adjacency[t + e] = k;
+                        degree[t / 3]++;
+                    }
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Motoko/Texture.cs
===================================================================
--- /OniSplit/Motoko/Texture.cs	(revision 1114)
+++ /OniSplit/Motoko/Texture.cs	(revision 1114)
@@ -0,0 +1,58 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Motoko
+{
+    internal class Texture
+    {
+        public readonly List<Surface> Surfaces = new List<Surface>();
+        public int Width;
+        public int Height;
+        public TextureFormat Format;
+        public TextureFlags Flags;
+        public string Name;
+        public Texture EnvMap;
+
+        public void GenerateMipMaps()
+        {
+            if ((Flags & TextureFlags.HasMipMaps) != 0)
+                return;
+
+            var surface = Surfaces[0];
+
+            Surfaces.Clear();
+            Surfaces.Add(surface);
+
+            if (surface.Format == SurfaceFormat.DXT1)
+                surface = surface.Convert(SurfaceFormat.BGRX5551);
+
+            int width = surface.Width;
+            int height = surface.Height;
+            var surfaceFormat = Format.ToSurfaceFormat();
+
+            while (width > 1 || height > 1)
+            {
+                width = Math.Max(width >> 1, 1);
+                height = Math.Max(height >> 1, 1);
+
+                surface = surface.Resize(width, height);
+
+                Surfaces.Add(surface);
+            }
+
+            if (surface.Format != surfaceFormat)
+            {
+                for (int i = 1; i < Surfaces.Count; i++)
+                    Surfaces[i] = Surfaces[i].Convert(surfaceFormat);
+            }
+
+            Flags |= TextureFlags.HasMipMaps;
+        }
+
+        public bool HasAlpha => Surfaces[0].HasAlpha;
+
+        public bool WrapU => (Flags & TextureFlags.NoUWrap) == 0;
+        public bool WrapV => (Flags & TextureFlags.NoVWrap) == 0;
+    }
+}
Index: /OniSplit/Motoko/TextureDaeWriter.cs
===================================================================
--- /OniSplit/Motoko/TextureDaeWriter.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureDaeWriter.cs	(revision 1114)
@@ -0,0 +1,97 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Motoko
+{
+    internal class TextureDaeWriter
+    {
+        private readonly string outputDirPath;
+        private readonly Dictionary<InstanceDescriptor, Dae.Material> materials = new Dictionary<InstanceDescriptor, Dae.Material>();
+
+        public TextureDaeWriter(string outputDirPath)
+        {
+            this.outputDirPath = outputDirPath;
+        }
+
+        public Dae.Material WriteMaterial(InstanceDescriptor txmp)
+        {
+            Dae.Material material;
+
+            if (!materials.TryGetValue(txmp, out material))
+            {
+                material = CreateMaterial(txmp);
+                materials.Add(txmp, material);
+            }
+
+            return material;
+        }
+
+        private Dae.Material CreateMaterial(InstanceDescriptor txmp)
+        {
+            var texture = TextureDatReader.Read(txmp);
+
+            var imageFilePath = Utils.CleanupTextureName(txmp.Name) + ".tga";
+            imageFilePath = Path.Combine("images", imageFilePath);
+            TgaWriter.Write(texture.Surfaces[0], Path.Combine(outputDirPath, imageFilePath));
+
+            string name = TextureNameToId(txmp);
+
+            var image = new Dae.Image
+            {
+                FilePath = "./" + imageFilePath.Replace('\\', '/'),
+                Name = name
+            };
+
+            var effectSurface = new Dae.EffectSurface(image);
+
+            var effectSampler = new Dae.EffectSampler(effectSurface)
+            {
+                WrapS = texture.WrapU ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None,
+                WrapT = texture.WrapV ? Dae.EffectSamplerWrap.Wrap : Dae.EffectSamplerWrap.None
+            };
+
+            var effectTexture = new Dae.EffectTexture(effectSampler, "diffuse_TEXCOORD");
+
+            var effect = new Dae.Effect
+            {
+                Name = name,
+                DiffuseValue = effectTexture,
+                TransparentValue = texture.HasAlpha ? effectTexture : null,
+                Parameters = {
+                    new Dae.EffectParameter("surface", effectSurface),
+                    new Dae.EffectParameter("sampler", effectSampler)
+                }
+            };
+
+            var material = new Dae.Material
+            {
+                Name = name,
+                Effect = effect
+            };
+
+            return material;
+        }
+
+        private static string TextureNameToId(InstanceDescriptor txmp)
+        {
+            string name = Utils.CleanupTextureName(txmp.Name);
+
+            if (name.StartsWith("Iteration", StringComparison.Ordinal))
+            {
+                //
+                // HACK: discard Iteration_NNN_ prefixes to avoid truncation of material
+                // names in Blender.
+                //
+
+                name = name.Substring(9);
+
+                if (char.IsDigit(name[0]) && char.IsDigit(name[1]) && char.IsDigit(name[2]) && name[3] == '_')
+                    name = name.Substring(4);
+            }
+
+            return name;
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureDatReader.cs
===================================================================
--- /OniSplit/Motoko/TextureDatReader.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureDatReader.cs	(revision 1114)
@@ -0,0 +1,81 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Motoko
+{
+    internal static class TextureDatReader
+    {
+        public static Texture ReadInfo(InstanceDescriptor txmp)
+        {
+            var texture = new Texture
+            {
+                Name = txmp.Name
+            };
+
+            using (var reader = txmp.OpenRead(128))
+            {
+                texture.Flags = (TextureFlags)reader.ReadInt32();
+                texture.Width = reader.ReadInt16();
+                texture.Height = reader.ReadInt16();
+                texture.Format = (TextureFormat)reader.ReadInt32();
+                reader.Skip(8);
+
+                if (txmp.IsMacFile)
+                    reader.Skip(4);
+
+                reader.Skip(4);
+            }
+
+            return texture;
+        }
+
+        public static Texture Read(InstanceDescriptor txmp)
+        {
+            var texture = new Texture
+            {
+                Name = txmp.Name
+            };
+
+            int rawOffset;
+
+            using (var reader = txmp.OpenRead(128))
+            {
+                texture.Flags = (TextureFlags)reader.ReadInt32();
+                texture.Width = reader.ReadInt16();
+                texture.Height = reader.ReadInt16();
+                texture.Format = (TextureFormat)reader.ReadInt32();
+                reader.Skip(8);
+
+                if (txmp.IsMacFile)
+                    reader.Skip(4);
+
+                rawOffset = reader.ReadInt32();
+            }
+
+            using (var rawReader = txmp.GetSepReader(rawOffset))
+                ReadSurfaces(texture, rawReader);
+
+            return texture;
+        }
+
+        private static void ReadSurfaces(Texture texture, BinaryReader reader)
+        {
+            var format = texture.Format.ToSurfaceFormat();
+            int width = texture.Width;
+            int height = texture.Height;
+            bool hasMipMaps = (texture.Flags & TextureFlags.HasMipMaps) != 0;
+
+            do
+            {
+                var surface = new Surface(width, height, format);
+                reader.Read(surface.Data, 0, surface.Data.Length);
+                texture.Surfaces.Add(surface);
+
+                width = Math.Max(width >> 1, 1);
+                height = Math.Max(height >> 1, 1);
+
+            } while (hasMipMaps && (width > 1 || height > 1));
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureDatWriter.cs
===================================================================
--- /OniSplit/Motoko/TextureDatWriter.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureDatWriter.cs	(revision 1114)
@@ -0,0 +1,63 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Motoko
+{
+    internal class TextureDatWriter
+    {
+        private readonly Importer importer;
+
+        public static void Write(Texture texture, string outputDirPath)
+        {
+            var writer = new DatWriter();
+            Write(texture, writer);
+            writer.Write(outputDirPath);
+        }
+
+        public static void Write(Texture texture, Importer importer)
+        {
+            var writer = new TextureDatWriter(importer);
+            writer.Write(texture);
+        }
+
+        private TextureDatWriter(Importer importer)
+        {
+            this.importer = importer;
+        }
+
+        private void Write(Texture texture)
+        {
+            var txmp = importer.CreateInstance(TemplateTag.TXMP, texture.Name);
+            int rawOffset = importer.RawWriter.Align32();
+            var flags = texture.Flags;
+            ImporterDescriptor envMapTxmp = null;
+
+            if (texture.EnvMap != null)
+            {
+                envMapTxmp = importer.CreateInstance(TemplateTag.TXMP, texture.EnvMap.Name);
+                flags |= TextureFlags.HasEnvMap;
+            }
+
+            if (texture.Surfaces.Count > 1)
+            {
+                flags |= TextureFlags.HasMipMaps;
+            }
+
+            using (var writer = txmp.OpenWrite(128))
+            {
+                writer.Write((int)flags);
+                writer.WriteInt16(texture.Width);
+                writer.WriteInt16(texture.Height);
+                writer.Write((int)texture.Format);
+                writer.Write(0);
+                writer.Write(envMapTxmp);
+                writer.Write(rawOffset);
+                writer.Skip(12);
+            }
+
+            foreach (var surface in texture.Surfaces)
+                importer.RawWriter.Write(surface.Data);
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureExporter.cs
===================================================================
--- /OniSplit/Motoko/TextureExporter.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureExporter.cs	(revision 1114)
@@ -0,0 +1,51 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Imaging;
+
+namespace Oni.Motoko
+{
+    internal class TextureExporter : Exporter
+    {
+        protected string fileType;
+
+        public TextureExporter(InstanceFileManager fileManager, string outputDirPath, string fileType)
+            : base(fileManager, outputDirPath)
+        {
+            this.fileType = fileType;
+        }
+
+        protected override List<InstanceDescriptor> GetSupportedDescriptors(InstanceFile file)
+        {
+            return file.GetNamedDescriptors(TemplateTag.TXMP);
+        }
+
+        protected override void ExportInstance(InstanceDescriptor descriptor)
+        {
+            var texture = TextureDatReader.Read(descriptor);
+            var filePath = CreateFileName(descriptor, "." + fileType);
+
+            switch (fileType)
+            {
+                case "tga":
+                    TgaWriter.Write(texture.Surfaces[0], filePath);
+                    break;
+
+                case "dds":
+                    DdsWriter.Write(texture.Surfaces, filePath);
+                    break;
+
+                case "png":
+                case "jpg":
+                case "bmp":
+                case "tif":
+#if !NETCORE
+                    SysWriter.Write(texture.Surfaces[0], filePath);
+#endif
+                    break;
+
+                default:
+                    throw new NotSupportedException(string.Format("Extracting textures as '{0}' is not supported", fileType));
+            }
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureFlags.cs
===================================================================
--- /OniSplit/Motoko/TextureFlags.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureFlags.cs	(revision 1114)
@@ -0,0 +1,23 @@
+﻿using System;
+
+namespace Oni.Motoko
+{
+    [Flags]
+    internal enum TextureFlags
+    {
+        None = 0x0000,
+        HasMipMaps = 0x0001,
+        NoUWrap = 0x0004,
+        NoVWrap = 0x0008,
+        AnimPingPong = 0x0040,
+        AnimRandom = 0x0080,
+        AnimGlobalTime = 0x0100,
+        HasEnvMap = 0x0200,
+        AdditiveBlend = 0x0400,
+        SwapBytes = 0x1000,
+        AnimLoop = 0x4000,
+        Shield = 0x8000,
+        Invisibility = 0x10000,
+        Daodan = 0x20000
+    }
+}
Index: /OniSplit/Motoko/TextureFormat.cs
===================================================================
--- /OniSplit/Motoko/TextureFormat.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureFormat.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Motoko
+{
+    internal enum TextureFormat
+    {
+        BGRA4444 = 0,
+        BGR555 = 1,
+        BGRA5551 = 2,
+        RGBA = 7,
+        BGR = 8,
+        DXT1 = 9
+    }
+}
Index: /OniSplit/Motoko/TextureImporter.cs
===================================================================
--- /OniSplit/Motoko/TextureImporter.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureImporter.cs	(revision 1114)
@@ -0,0 +1,264 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Motoko
+{
+    internal class TextureImporter : Importer
+    {
+        private static readonly int[] powersOf2 = new[] { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
+        private static readonly Dictionary<string, TextureFormat> formatNames = new Dictionary<string, TextureFormat>(StringComparer.OrdinalIgnoreCase)
+        {
+            { "bgr32", TextureFormat.BGR },
+            { "bgr", TextureFormat.BGR },
+            { "bgra32", TextureFormat.RGBA },
+            { "rgba", TextureFormat.RGBA },
+            { "bgra4444", TextureFormat.BGRA4444 },
+            { "bgr555", TextureFormat.BGR555 },
+            { "bgra5551", TextureFormat.BGRA5551 },
+            { "dxt1", TextureFormat.DXT1 }
+        };
+
+        private readonly bool allowLargeTextures;
+        private readonly bool noMipMaps;
+        private readonly TextureFlags defaultFlags;
+        private readonly TextureFormat? defaultFormat;
+        private readonly string envmapName;
+
+        public static TextureFormat ParseTextureFormat(string name)
+        {
+            TextureFormat format;
+
+            if (!formatNames.TryGetValue(name, out format))
+                throw new FormatException(string.Format("Invalid texture format '{0}'", name));
+
+            return format;
+        }
+
+        public TextureImporter(TextureImporterOptions options)
+        {
+            if (options != null)
+            {
+                defaultFormat = options.Format;
+                envmapName = options.EnvironmentMap;
+                defaultFlags = options.Flags & ~TextureFlags.HasMipMaps;
+                noMipMaps = (options.Flags & TextureFlags.HasMipMaps) == 0;
+                allowLargeTextures = true;
+            }
+        }
+
+        public TextureImporter(string[] args)
+        {
+            foreach (string arg in args)
+            {
+                if (arg.StartsWith("-format:", StringComparison.Ordinal))
+                {
+                    TextureFormat formatArg;
+                    string formatName = arg.Substring(8);
+
+                    if (!formatNames.TryGetValue(formatName, out formatArg))
+                        throw new NotSupportedException(string.Format("Unknown texture format {0}", formatName));
+
+                    defaultFormat = formatArg;
+                }
+                else if (arg.StartsWith("-envmap:", StringComparison.Ordinal))
+                {
+                    string envmap = arg.Substring(8);
+
+                    if (envmap.Length > 0)
+                        envmapName = envmap;
+                }
+                else if (arg == "-large")
+                {
+                    allowLargeTextures = true;
+                }
+                else if (arg == "-nouwrap")
+                {
+                    defaultFlags |= TextureFlags.NoUWrap;
+                }
+                else if (arg == "-novwrap")
+                {
+                    defaultFlags |= TextureFlags.NoVWrap;
+                }
+                else if (arg == "-nomipmaps")
+                {
+                    noMipMaps = true;
+                }
+            }
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            var texture = new Texture
+            {
+                Name = DecodeFileName(filePath),
+                Flags = defaultFlags
+            };
+
+            LoadImage(texture, filePath);
+
+            if (envmapName != null)
+            {
+                texture.EnvMap = new Texture();
+                texture.EnvMap.Name = envmapName;
+            }
+
+            BeginImport();
+            TextureDatWriter.Write(texture, this);
+            Write(outputDirPath);
+        }
+
+        private void LoadImage(Texture texture, string filePath)
+        {
+            var surfaces = new List<Surface>();
+
+            switch (Path.GetExtension(filePath).ToLowerInvariant())
+            {
+                case ".tga":
+                    surfaces.Add(TgaReader.Read(filePath));
+                    break;
+                case ".dds":
+                    surfaces.AddRange(DdsReader.Read(filePath, noMipMaps));
+                    break;
+                default:
+#if !NETCORE
+                    surfaces.Add(SysReader.Read(filePath));
+#endif
+                    break;
+            }
+
+            if (surfaces.Count == 0)
+                throw new InvalidDataException(string.Format("Could not load image '{0}'", filePath));
+
+            var mainSurface = surfaces[0];
+
+            if (Array.IndexOf(powersOf2, mainSurface.Width) == -1)
+                Console.Error.WriteLine("Warning: Texture '{0}' width is not a power of 2", filePath);
+
+            if (Array.IndexOf(powersOf2, mainSurface.Height) == -1)
+                Console.Error.WriteLine("Warning: Texture '{0}' height is not a power of 2", filePath);
+
+            if (surfaces.Count == 1)
+            {
+                mainSurface.CleanupAlpha();
+            }
+
+            if (mainSurface.Format == SurfaceFormat.DXT1 && defaultFormat != null && defaultFormat != TextureFormat.DXT1)
+            {
+                //
+                // If the source image is DXT1 but the target is not then we can convert here
+                // to a more flexible image format.
+                //
+
+                for (int i = 0; i < surfaces.Count; i++)
+                    surfaces[i] = surfaces[i].Convert(SurfaceFormat.RGBA);
+
+                mainSurface = surfaces[0];
+            }
+
+            if (!allowLargeTextures && (mainSurface.Width > 256 || mainSurface.Height > 256))
+            {
+                if (surfaces.Count == 1)
+                {
+                    int sx = mainSurface.Width / 256;
+                    int sy = mainSurface.Height / 256;
+
+                    int s = Math.Max(sx, sy);
+
+                    mainSurface = mainSurface.Resize(mainSurface.Width / s, mainSurface.Height / s);
+                    surfaces[0] = mainSurface;
+                }
+                else
+                {
+                    while (surfaces.Count > 0 && (surfaces[0].Width > 256 || surfaces[0].Height > 256))
+                        surfaces.RemoveAt(0);
+
+                    mainSurface = surfaces[0];
+                }
+            }
+
+            if (surfaces.Count == 1 && !noMipMaps && mainSurface.Format != SurfaceFormat.DXT1)
+            {
+                var surface = mainSurface;
+
+                while (surface.Width > 1 || surface.Height > 1)
+                {
+                    int width = Math.Max(surface.Width >> 1, 1);
+                    int height = Math.Max(surface.Height >> 1, 1);
+
+                    surface = surface.Resize(width, height);
+
+                    surfaces.Add(surface);
+                }
+            }
+
+            var convertToFormat = mainSurface.Format;
+
+            if (defaultFormat != null)
+            {
+                texture.Format = defaultFormat.Value;
+                convertToFormat = texture.Format.ToSurfaceFormat();
+            }
+            else
+            {
+                switch (mainSurface.Format)
+                {
+                    case SurfaceFormat.BGRA4444:
+                        texture.Format = TextureFormat.BGRA4444;
+                        break;
+
+                    case SurfaceFormat.BGRX5551:
+                        texture.Format = TextureFormat.BGR555;
+                        break;
+
+                    case SurfaceFormat.BGR565:
+                        texture.Format = TextureFormat.BGR555;
+                        break;
+
+                    case SurfaceFormat.BGRA5551:
+                        texture.Format = TextureFormat.BGRA5551;
+                        break;
+
+                    case SurfaceFormat.BGRX:
+                    case SurfaceFormat.RGBX:
+                        texture.Format = TextureFormat.BGR;
+                        convertToFormat = SurfaceFormat.BGRX;
+                        break;
+
+                    case SurfaceFormat.BGRA:
+                    case SurfaceFormat.RGBA:
+                        //
+                        // We don't do RGBA32 unless specifically required because
+                        // Oni needs a patch to support it.
+                        //
+                        texture.Format = TextureFormat.BGRA4444;
+                        convertToFormat = SurfaceFormat.BGRA4444;
+                        break;
+
+                    case SurfaceFormat.DXT1:
+                        texture.Format = TextureFormat.DXT1;
+                        break;
+
+                    default:
+                        throw new NotSupportedException(string.Format("Image format {0} cannot be imported", mainSurface.Format));
+                }
+            }
+
+            if (convertToFormat != mainSurface.Format)
+            {
+                for (int i = 0; i < surfaces.Count; i++)
+                    surfaces[i] = surfaces[i].Convert(convertToFormat);
+
+                mainSurface = surfaces[0];
+            }
+
+            if (texture.Format != TextureFormat.RGBA)
+                texture.Flags |= TextureFlags.SwapBytes;
+
+            texture.Width = mainSurface.Width;
+            texture.Height = mainSurface.Height;
+            texture.Surfaces.AddRange(surfaces);
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureImporter3.cs
===================================================================
--- /OniSplit/Motoko/TextureImporter3.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureImporter3.cs	(revision 1114)
@@ -0,0 +1,361 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace Oni.Motoko
+{
+    using Imaging;
+    using Metadata;
+
+    internal class TextureImporter3
+    {
+        private readonly string outputDirPath;
+        private readonly Dictionary<string, TextureImporterOptions> textures = new Dictionary<string, TextureImporterOptions>(StringComparer.Ordinal);
+        private TextureFormat defaultFormat = TextureFormat.BGR;
+        private TextureFormat defaultSquareFormat = TextureFormat.BGR;
+        private TextureFormat defaultAlphaFormat = TextureFormat.RGBA;
+        private int maxSize = 512;
+
+        public TextureImporter3(string outputDirPath)
+        {
+            this.outputDirPath = outputDirPath;
+        }
+
+        public TextureFormat DefaultFormat
+        {
+            get
+            {
+                return defaultFormat;
+            }
+            set
+            {
+                defaultFormat = value;
+                defaultSquareFormat = value;
+            }
+        }
+
+        public TextureFormat DefaultAlphaFormat
+        {
+            get { return defaultAlphaFormat; }
+            set { defaultAlphaFormat = value; }
+        }
+
+        public int MaxSize
+        {
+            get { return maxSize; }
+            set { maxSize = value; }
+        }
+
+        public string AddMaterial(Dae.Material material)
+        {
+            if (material == null || material.Effect == null)
+                return null;
+
+            var texture = material.Effect.Textures.FirstOrDefault(t => t.Channel == Dae.EffectTextureChannel.Diffuse);
+
+            if (texture == null)
+                return null;
+
+            var sampler = texture.Sampler;
+
+            if (sampler == null || sampler.Surface == null || sampler.Surface.InitFrom == null)
+                return null;
+
+            var filePath = Path.GetFullPath(sampler.Surface.InitFrom.FilePath);
+
+            if (!File.Exists(filePath))
+                return null;
+
+            var options = GetOptions(Path.GetFileNameWithoutExtension(filePath), filePath);
+
+            return options.Name;
+        }
+
+        public TextureImporterOptions AddMaterial(Akira.Material material)
+        {
+            return GetOptions(material.Name, material.ImageFilePath);
+        }
+
+        private TextureImporterOptions GetOptions(string name, string filePath)
+        {
+            TextureImporterOptions options;
+
+            if (!textures.TryGetValue(name, out options))
+            {
+                options = new TextureImporterOptions
+                {
+                    Name = name,
+                    Images = new[] { filePath }
+                };
+
+                textures.Add(name, options);
+            }
+
+            return options;
+        }
+
+        public TextureImporterOptions GetOptions(string name, bool create)
+        {
+            TextureImporterOptions options;
+
+            if (!textures.TryGetValue(name, out options) && create)
+            {
+                options = new TextureImporterOptions
+                {
+                    Name = name
+                };
+
+                textures.Add(name, options);
+            }
+
+            return options;
+        }
+
+        public void ReadOptions(XmlReader xml, string basePath)
+        {
+            var options = GetOptions(xml.GetAttribute("Name"), true);
+            var images = new List<string>();
+
+            xml.ReadStartElement("Texture");
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Width":
+                        options.Width = xml.ReadElementContentAsInt();
+                        break;
+                    case "Height":
+                        options.Height = xml.ReadElementContentAsInt();
+                        break;
+                    case "Format":
+                        options.Format = TextureImporter.ParseTextureFormat(xml.ReadElementContentAsString());
+                        break;
+                    case "Flags":
+                        options.Flags = xml.ReadElementContentAsEnum<TextureFlags>();
+                        break;
+                    case "GunkFlags":
+                        options.GunkFlags = xml.ReadElementContentAsEnum<Akira.GunkFlags>();
+                        break;
+                    case "EnvMap":
+                        options.EnvironmentMap = xml.ReadElementContentAsString();
+                        break;
+                    case "Speed":
+                        options.Speed = xml.ReadElementContentAsInt();
+                        break;
+                    case "Image":
+                        images.Add(Path.Combine(basePath, xml.ReadElementContentAsString()));
+                        break;
+                    default:
+                        Console.Error.WriteLine("Unknown texture option {0}", xml.LocalName);
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+
+            options.Images = images.ToArray();
+        }
+
+        public void Write()
+        {
+            Parallel.ForEach(textures.Values, options =>
+            {
+                if (options.Images.Length > 0)
+                {
+                    var writer = new TexImporter(this, options);
+                    writer.Import();
+                    writer.Write(outputDirPath);
+                }
+            });
+
+            Console.WriteLine("Imported {0} textures", textures.Count);
+        }
+
+        private class TexImporter : Importer
+        {
+            private readonly TextureImporter3 importer;
+            private readonly TextureImporterOptions options;
+
+            public TexImporter(TextureImporter3 importer, TextureImporterOptions options)
+            {
+                this.importer = importer;
+                this.options = options;
+
+                BeginImport();
+            }
+
+            public void Import()
+            {
+                var surfaces = new List<Surface>();
+
+                foreach (string imageFilePath in options.Images)
+                    surfaces.Add(TextureUtils.LoadImage(imageFilePath));
+
+                if (surfaces.Count == 0)
+                    throw new InvalidDataException("No images found. A texture must have at least one image.");
+
+                TextureFormat format;
+
+                if (options.Format != null)
+                {
+                    format = options.Format.Value;
+                }
+                else
+                {
+                    var surface = surfaces[0];
+
+                    if (surface.HasTransparentPixels())
+                        format = importer.defaultAlphaFormat;
+                    else if (surface.Width % 4 == 0 && surface.Height % 4 == 0)
+                        format = importer.defaultSquareFormat;
+                    else
+                        format = importer.defaultFormat;
+                }
+
+                int imageWidth = 0;
+                int imageHeight = 0;
+
+                foreach (var surface in surfaces)
+                {
+                    if (imageWidth == 0)
+                        imageWidth = surface.Width;
+                    else if (imageWidth != surface.Width)
+                        throw new NotSupportedException("All animation frames must have the same size.");
+
+                    if (imageHeight == 0)
+                        imageHeight = surface.Height;
+                    else if (imageHeight != surface.Height)
+                        throw new NotSupportedException("All animation frames must have the same size.");
+                }
+
+                int width = options.Width;
+                int height = options.Height;
+
+                if (width == 0)
+                    width = imageWidth;
+                else if (width > imageWidth)
+                    throw new NotSupportedException("Cannot upscale images.");
+
+                if (height == 0)
+                    height = imageHeight;
+                else if (height > imageHeight)
+                    throw new NotSupportedException("Cannot upscale images.");
+
+                if (width > importer.maxSize || height > importer.maxSize)
+                {
+                    if (width > height)
+                    {
+                        height = importer.maxSize * height / width;
+                        width = importer.maxSize;
+                    }
+                    else
+                    {
+                        width = importer.maxSize * width / height;
+                        height = importer.maxSize;
+                    }
+                }
+
+                width = TextureUtils.RoundToPowerOf2(width);
+                height = TextureUtils.RoundToPowerOf2(height);
+
+                if (width != imageWidth || height != imageHeight)
+                {
+                    for (int i = 0; i < surfaces.Count; i++)
+                        surfaces[i] = surfaces[i].Resize(width, height);
+                }
+
+                var flags = options.Flags | TextureFlags.HasMipMaps;
+                flags &= ~(TextureFlags.HasEnvMap | TextureFlags.SwapBytes);
+                var envMapName = options.EnvironmentMap;
+
+                if (format != TextureFormat.RGBA)
+                    flags |= TextureFlags.SwapBytes;
+
+                if (!string.IsNullOrEmpty(envMapName))
+                    flags |= TextureFlags.HasEnvMap;
+
+                var name = options.Name;
+                int speed = options.Speed;
+
+                for (int i = 0; i < surfaces.Count; i++)
+                {
+                    var descriptor = CreateInstance(TemplateTag.TXMP, i == 0 ? name : null);
+
+                    using (var writer = descriptor.OpenWrite(128))
+                    {
+                        writer.Write((int)flags);
+                        writer.WriteUInt16(width);
+                        writer.WriteUInt16(height);
+                        writer.Write((int)format);
+
+                        if (i == 0 && surfaces.Count > 1)
+                            writer.WriteInstanceId(surfaces.Count);
+                        else
+                            writer.Write(0);
+
+                        if (!string.IsNullOrEmpty(envMapName))
+                            writer.WriteInstanceId(surfaces.Count + ((surfaces.Count > 1) ? 1 : 0));
+                        else
+                            writer.Write(0);
+
+                        writer.Write(RawWriter.Align32());
+                        writer.Skip(12);
+
+                        var mainSurface = surfaces[i];
+
+                        var levels = new List<Surface>(16);
+                        levels.Add(mainSurface);
+
+                        if ((flags & TextureFlags.HasMipMaps) != 0)
+                        {
+                            int mipWidth = width;
+                            int mipHeight = height;
+
+                            var surface = mainSurface;
+
+                            while (mipWidth > 1 || mipHeight > 1)
+                            {
+                                mipWidth = Math.Max(mipWidth >> 1, 1);
+                                mipHeight = Math.Max(mipHeight >> 1, 1);
+
+                                surface = surface.Resize(mipWidth, mipHeight);
+
+                                levels.Add(surface);
+                            }
+                        }
+
+                        foreach (var level in levels)
+                        {
+                            var surface = level.Convert(format.ToSurfaceFormat());
+
+                            RawWriter.Write(surface.Data);
+                        }
+                    }
+                }
+
+                if (surfaces.Count > 1)
+                {
+                    var txan = CreateInstance(TemplateTag.TXAN);
+
+                    using (var writer = txan.OpenWrite(12))
+                    {
+                        writer.WriteInt16(speed);
+                        writer.WriteInt16(speed);
+                        writer.Write(0);
+                        writer.Write(surfaces.Count);
+                        writer.Write(0);
+
+                        for (int i = 1; i < surfaces.Count; i++)
+                            writer.WriteInstanceId(i);
+                    }
+                }
+
+                if (!string.IsNullOrEmpty(envMapName))
+                    CreateInstance(TemplateTag.TXMP, envMapName);
+            }
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureImporterOptions.cs
===================================================================
--- /OniSplit/Motoko/TextureImporterOptions.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureImporterOptions.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿using System;
+using Oni.Akira;
+
+namespace Oni.Motoko
+{
+    internal class TextureImporterOptions
+    {
+        public string Name;
+        public int Width;
+        public int Height;
+        public TextureFormat? Format;
+        public TextureFlags Flags;
+        public GunkFlags GunkFlags;
+        public string EnvironmentMap;
+        public int Speed = 1;
+        public string[] Images;
+    }
+}
Index: /OniSplit/Motoko/TextureUtils.cs
===================================================================
--- /OniSplit/Motoko/TextureUtils.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureUtils.cs	(revision 1114)
@@ -0,0 +1,80 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using Oni.Imaging;
+
+namespace Oni.Motoko
+{
+    internal static class TextureUtils
+    {
+        public static SurfaceFormat ToSurfaceFormat(this TextureFormat format)
+        {
+            switch (format)
+            {
+                case TextureFormat.BGRA4444:
+                    return SurfaceFormat.BGRA4444;
+
+                case TextureFormat.BGR555:
+                    return SurfaceFormat.BGRX5551;
+
+                case TextureFormat.BGRA5551:
+                    return SurfaceFormat.BGRA5551;
+
+                case TextureFormat.RGBA:
+                    return SurfaceFormat.RGBA;
+
+                case TextureFormat.BGR:
+                    return SurfaceFormat.BGRX;
+
+                case TextureFormat.DXT1:
+                    return SurfaceFormat.DXT1;
+
+                default:
+                    throw new NotSupportedException(string.Format("Texture format {0} is not supported", format));
+            }
+        }
+
+        public static Surface LoadImage(string filePath)
+        {
+            var surfaces = new List<Surface>();
+
+            switch (Path.GetExtension(filePath).ToLowerInvariant())
+            {
+                case ".tga":
+                    surfaces.Add(TgaReader.Read(filePath));
+                    break;
+                default:
+#if !NETCORE
+                    surfaces.Add(SysReader.Read(filePath));
+#endif
+                    break;
+            }
+
+            if (surfaces.Count == 0)
+                throw new InvalidDataException(string.Format("Could not load image '{0}'", filePath));
+
+            return surfaces[0];
+        }
+
+        public static int RoundToPowerOf2(int value)
+        {
+            if (value <= 2)
+                return value;
+
+            int hsb = 0;
+
+            for (int x = value; x > 1; hsb++)
+                x >>= 1;
+
+            //
+            // TODO would be nice to round up for cases like 127
+            // But that require to upscale the image and currently the image
+            // resizing code doesn't handle upscaling.
+            //
+
+            //return 1 << (hsb + ((value >> (hsb - 1)) & 1));
+
+            return 1 << hsb;
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureXmlExporter.cs
===================================================================
--- /OniSplit/Motoko/TextureXmlExporter.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureXmlExporter.cs	(revision 1114)
@@ -0,0 +1,113 @@
+﻿using System;
+using System.IO;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Motoko
+{
+    internal sealed class TextureXmlExporter : RawXmlExporter
+    {
+        private InstanceDescriptor txmp;
+        private string outputDirPath;
+        private string baseFileName;
+
+        private TextureXmlExporter(BinaryReader reader, XmlWriter writer)
+            : base(reader, writer)
+        {
+        }
+
+        public static void Export(InstanceDescriptor txmp, XmlWriter writer, string outputDirPath, string baseFileName)
+        {
+            using (var reader = txmp.OpenRead(128))
+            {
+                var exporter = new TextureXmlExporter(reader, writer) {
+                    txmp = txmp,
+                    outputDirPath = outputDirPath,
+                    baseFileName = baseFileName
+                };
+
+                exporter.Export();
+            }
+        }
+
+        private void Export()
+        {
+            var flags = (InstanceMetadata.TXMPFlags)Reader.ReadInt32();
+            int width = Reader.ReadInt16();
+            int height = Reader.ReadInt16();
+            var format = (InstanceMetadata.TXMPFormat)Reader.ReadInt32();
+            var txan = Reader.ReadInstance();
+            var envmap = Reader.ReadInstance();
+            int dataOffset = Reader.ReadInt32();
+
+            //
+            // Cleanup unwanted/unneeded flags.
+            //
+
+            flags &= ~(InstanceMetadata.TXMPFlags.Unknown0010
+                | InstanceMetadata.TXMPFlags.SwapBytes
+                | InstanceMetadata.TXMPFlags.HasEnvMap);
+
+            Xml.WriteStartElement("Texture");
+
+            string name = txmp.FullName;
+
+            if (name.StartsWith("TXMP", StringComparison.Ordinal))
+                name = name.Substring(4);
+
+            Xml.WriteElementString("Flags", flags.ToString().Replace(",", " "));
+            Xml.WriteElementString("Format", format.ToString());
+
+            if (envmap != null)
+                Xml.WriteElementString("EnvMap", envmap.FullName);
+
+            if (txan == null)
+            {
+                string fileName = baseFileName + ".tga";
+                TgaWriter.Write(TextureDatReader.Read(txmp).Surfaces[0], Path.Combine(outputDirPath, fileName));
+
+                Xml.WriteElementString("Image", fileName);
+            }
+            else
+            {
+                WriteAnimationFrames2(txan);
+            }
+
+            Xml.WriteEndElement();
+        }
+
+        private void WriteAnimationFrames2(InstanceDescriptor txan)
+        {
+            using (var txanReader = txan.OpenRead(12))
+            {
+                int speed = txanReader.ReadInt16();
+                txanReader.Skip(6);
+                int count = txanReader.ReadInt32();
+
+                Xml.WriteElementString("Speed", XmlConvert.ToString(speed));
+
+                for (int i = 0; i < count; i++)
+                {
+                    InstanceDescriptor frame;
+
+                    if (i == 0)
+                    {
+                        txanReader.Skip(4);
+                        frame = txmp;
+                    }
+                    else
+                    {
+                        frame = txanReader.ReadInstance();
+                    }
+
+                    string fileName = string.Format("{0}_{1:d3}.tga", baseFileName, i);
+                    TgaWriter.Write(TextureDatReader.Read(frame).Surfaces[0], Path.Combine(outputDirPath, fileName));
+
+                    Xml.WriteElementString("Image", fileName);
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Motoko/TextureXmlImporter.cs
===================================================================
--- /OniSplit/Motoko/TextureXmlImporter.cs	(revision 1114)
+++ /OniSplit/Motoko/TextureXmlImporter.cs	(revision 1114)
@@ -0,0 +1,227 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Motoko
+{
+    internal class TextureXmlImporter
+    {
+        private readonly XmlImporter importer;
+        private readonly XmlReader xml;
+        private readonly string filePath;
+
+        public TextureXmlImporter(XmlImporter importer, XmlReader xml, string filePath)
+        {
+            this.importer = importer;
+            this.xml = xml;
+            this.filePath = filePath;
+        }
+
+        public void Import()
+        {
+            xml.ReadStartElement();
+
+            var name = Importer.DecodeFileName(Path.GetFileNameWithoutExtension(filePath));
+            var flags = MetaEnum.Parse<InstanceMetadata.TXMPFlags>(xml.ReadElementContentAsString("Flags", ""));
+            var format = MetaEnum.Parse<InstanceMetadata.TXMPFormat>(xml.ReadElementContentAsString("Format", ""));
+
+            int width = 0;
+            int height = 0;
+            int speed = 1;
+
+            if (xml.IsStartElement("Width"))
+                width = xml.ReadElementContentAsInt();
+
+            if (xml.IsStartElement("Height"))
+                height = xml.ReadElementContentAsInt();
+
+            string envMapName = null;
+
+            if (xml.IsStartElement("EnvMap"))
+            {
+                envMapName = xml.ReadElementContentAsString();
+
+                if (envMapName != null && envMapName.Length == 0)
+                    envMapName = null;
+            }
+
+            if (xml.IsStartElement("Speed"))
+                speed = xml.ReadElementContentAsInt();
+
+            var imageFilePaths = new List<string>();
+            var inputDirPath = Path.GetDirectoryName(filePath);
+
+            while (xml.IsStartElement("Image"))
+            {
+                string imageFilePath = xml.ReadElementContentAsString();
+
+                if (!Path.IsPathRooted(imageFilePath))
+                    imageFilePath = Path.Combine(inputDirPath, imageFilePath);
+
+                if (!File.Exists(imageFilePath))
+                    throw new IOException(string.Format("Could not find image file '{0}'", imageFilePath));
+
+                imageFilePaths.Add(imageFilePath);
+            }
+
+            xml.ReadEndElement();
+
+            var surfaces = new List<Surface>();
+
+            foreach (string imageFilePath in imageFilePaths)
+                surfaces.Add(TgaReader.Read(imageFilePath));
+
+            if (surfaces.Count == 0)
+                throw new InvalidDataException("No images found. A texture must have at least one image.");
+
+            int imageWidth = 0;
+            int imageHeight = 0;
+
+            foreach (Surface surface in surfaces)
+            {
+                if (imageWidth == 0)
+                    imageWidth = surface.Width;
+                else if (imageWidth != surface.Width)
+                    throw new NotSupportedException("All animation frames must have the same size.");
+
+                if (imageHeight == 0)
+                    imageHeight = surface.Height;
+                else if (imageHeight != surface.Height)
+                    throw new NotSupportedException("All animation frames must have the same size.");
+            }
+
+            if (width == 0)
+                width = imageWidth;
+            else if (width > imageWidth)
+                throw new NotSupportedException("Cannot upscale images.");
+
+            if (height == 0)
+                height = imageHeight;
+            else if (height > imageHeight)
+                throw new NotSupportedException("Cannot upscale images.");
+
+            //if (envMapName != null && surfaces.Count > 1)
+            //    throw new NotSupportedException("Animated textures cannot have an environment map.");
+
+            if (width != imageWidth || height != imageHeight)
+            {
+                for (int i = 0; i < surfaces.Count; i++)
+                    surfaces[i] = surfaces[i].Resize(width, height);
+            }
+
+            flags |= InstanceMetadata.TXMPFlags.SwapBytes;
+
+            if (envMapName != null)
+                flags |= InstanceMetadata.TXMPFlags.HasEnvMap;
+
+            for (int i = 0; i < surfaces.Count; i++)
+            {
+                BinaryWriter writer;
+
+                if (i == 0)
+                    writer = importer.BeginXmlInstance(TemplateTag.TXMP, name, i.ToString(CultureInfo.InvariantCulture));
+                else
+                    writer = importer.BeginXmlInstance(TemplateTag.TXMP, null, i.ToString(CultureInfo.InvariantCulture));
+
+                writer.Skip(128);
+                writer.Write((int)flags);
+                writer.WriteUInt16(width);
+                writer.WriteUInt16(height);
+                writer.Write((int)format);
+
+                if (i == 0 && surfaces.Count > 1)
+                    writer.WriteInstanceId(surfaces.Count);
+                else
+                    writer.Write(0);
+
+                if (envMapName != null)
+                    writer.WriteInstanceId(surfaces.Count + ((surfaces.Count > 1) ? 1 : 0));
+                else
+                    writer.Write(0);
+
+                writer.Write(importer.RawWriter.Align32());
+                writer.Skip(12);
+
+                var mainSurface = surfaces[i];
+
+                var levels = new List<Surface> { mainSurface };
+
+                if ((flags & InstanceMetadata.TXMPFlags.HasMipMaps) != 0)
+                {
+                    int mipWidth = width;
+                    int mipHeight = height;
+
+                    while (mipWidth > 1 || mipHeight > 1)
+                    {
+                        mipWidth = Math.Max(mipWidth >> 1, 1);
+                        mipHeight = Math.Max(mipHeight >> 1, 1);
+
+                        levels.Add(mainSurface.Resize(mipWidth, mipHeight));
+                    }
+                }
+
+                foreach (var level in levels)
+                {
+                    var surface = level.Convert(TextureFormatToSurfaceFormat(format));
+                    importer.RawWriter.Write(surface.Data);
+                }
+
+                importer.EndXmlInstance();
+            }
+
+            if (surfaces.Count > 1)
+            {
+                var txan = importer.CreateInstance(TemplateTag.TXAN);
+
+                using (var writer = txan.OpenWrite(12))
+                {
+                    writer.WriteInt16(speed);
+                    writer.WriteInt16(speed);
+                    writer.Write(0);
+                    writer.Write(surfaces.Count);
+                    writer.Write(0);
+
+                    for (int i = 1; i < surfaces.Count; i++)
+                        writer.WriteInstanceId(i);
+                }
+            }
+
+            if (envMapName != null)
+            {
+                importer.CreateInstance(TemplateTag.TXMP, envMapName);
+            }
+        }
+
+        private static SurfaceFormat TextureFormatToSurfaceFormat(InstanceMetadata.TXMPFormat format)
+        {
+            switch (format)
+            {
+                case InstanceMetadata.TXMPFormat.BGRA4444:
+                    return SurfaceFormat.BGRA4444;
+
+                case InstanceMetadata.TXMPFormat.BGR555:
+                    return SurfaceFormat.BGRX5551;
+
+                case InstanceMetadata.TXMPFormat.BGRA5551:
+                    return SurfaceFormat.BGRA5551;
+
+                case InstanceMetadata.TXMPFormat.RGBA:
+                    return SurfaceFormat.RGBA;
+
+                case InstanceMetadata.TXMPFormat.BGR:
+                    return SurfaceFormat.BGRX;
+
+                case InstanceMetadata.TXMPFormat.DXT1:
+                    return SurfaceFormat.DXT1;
+
+                default:
+                    throw new NotSupportedException(string.Format("Texture format {0} is not supported", format));
+            }
+        }
+    }
+}
Index: /OniSplit/Objects/Character.cs
===================================================================
--- /OniSplit/Objects/Character.cs	(revision 1114)
+++ /OniSplit/Objects/Character.cs	(revision 1114)
@@ -0,0 +1,251 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Objects
+{
+    internal class Character : ObjectBase
+    {
+        public CharacterFlags Flags;
+
+        public string ClassName;
+        public string Name;
+        public string WeaponClassName;
+
+        public string OnSpawn;
+        public string OnDeath;
+        public string OnSeenEnemy;
+        public string OnAlarmed;
+        public string OnHurt;
+        public string OnDefeated;
+        public string OnOutOfAmmo;
+        public string OnNoPath;
+
+        public int AdditionalHealth;
+        public CharacterJobType Job;
+        public int PatrolPathId;
+        public int CombatId;
+        public int MeleeId;
+        public int NeutralId;
+
+        public int MaxAmmoUsed;
+        public int MaxAmmoDropped;
+        public int MaxCellsUsed;
+        public int MaxCellsDropped;
+        public int MaxHyposUsed;
+        public int MaxHyposDropped;
+        public int MaxShieldsUsed;
+        public int MaxShieldsDropped;
+        public int MaxCloakUsed;
+        public int MaxCloakDropped;
+
+        public CharacterTeam Team;
+        public int AmmoPercent;
+        public CharacterAlertStatus InitialAlertLevel;
+        public CharacterAlertStatus MinimalAlertLevel;
+        public CharacterAlertStatus JobStartingAlertLevel;
+        public CharacterAlertStatus InvestigatingAlertLevel;
+        public int AlarmGroups;
+
+        public CharacterPursuitMode PursuitStrongUnseen;
+        public CharacterPursuitMode PursuitWeakUnseen;
+        public CharacterPursuitMode PursuitStrongSeen;
+        public CharacterPursuitMode PursuitWeakSeen;
+        public CharacterPursuitLostBehavior PursuitLost;
+
+        public Character()
+        {
+            TypeId = ObjectType.Character;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write((int)Flags);
+
+            writer.Write(ClassName, 64);
+            writer.Write(Name, 32);
+            writer.Write(WeaponClassName, 64);
+
+            writer.Write(OnSpawn, 32);
+            writer.Write(OnDeath, 32);
+            writer.Write(OnSeenEnemy, 32);
+            writer.Write(OnAlarmed, 32);
+            writer.Write(OnHurt, 32);
+            writer.Write(OnDefeated, 32);
+            writer.Write(OnOutOfAmmo, 32);
+            writer.Write(OnNoPath, 32);
+
+            writer.Write(AdditionalHealth);
+            writer.Write((int)Job);
+
+            writer.WriteInt16(PatrolPathId);
+            writer.WriteInt16(CombatId);
+            writer.WriteInt16(MeleeId);
+            writer.WriteInt16(NeutralId);
+
+            writer.WriteInt16(MaxAmmoUsed);
+            writer.WriteInt16(MaxAmmoDropped);
+            writer.WriteInt16(MaxCellsUsed);
+            writer.WriteInt16(MaxCellsDropped);
+            writer.WriteInt16(MaxHyposUsed);
+            writer.WriteInt16(MaxHyposDropped);
+            writer.WriteInt16(MaxShieldsUsed);
+            writer.WriteInt16(MaxShieldsDropped);
+            writer.WriteInt16(MaxCloakUsed);
+            writer.WriteInt16(MaxCloakDropped);
+
+            writer.Skip(4);
+
+            writer.Write((int)Team);
+            writer.Write(AmmoPercent);
+
+            writer.Write((int)InitialAlertLevel);
+            writer.Write((int)MinimalAlertLevel);
+            writer.Write((int)JobStartingAlertLevel);
+            writer.Write((int)InvestigatingAlertLevel);
+
+            writer.Write(AlarmGroups);
+
+            writer.Write((int)PursuitStrongUnseen);
+            writer.Write((int)PursuitWeakUnseen);
+            writer.Write((int)PursuitStrongSeen);
+            writer.Write((int)PursuitWeakSeen);
+            writer.Write((int)PursuitLost);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            Flags = (CharacterFlags)reader.ReadInt32();
+
+            ClassName = reader.ReadString(64);
+            Name = reader.ReadString(32);
+            WeaponClassName = reader.ReadString(64);
+
+            OnSpawn = reader.ReadString(32);
+            OnDeath = reader.ReadString(32);
+            OnSeenEnemy = reader.ReadString(32);
+            OnAlarmed = reader.ReadString(32);
+            OnHurt = reader.ReadString(32);
+            OnDefeated = reader.ReadString(32);
+            OnOutOfAmmo = reader.ReadString(32);
+            OnNoPath = reader.ReadString(32);
+
+            AdditionalHealth = reader.ReadInt32();
+            Job = (CharacterJobType)reader.ReadInt32();
+
+            PatrolPathId = reader.ReadInt16();
+            CombatId = reader.ReadInt16();
+            MeleeId = reader.ReadInt16();
+            NeutralId = reader.ReadInt16();
+
+            MaxAmmoUsed = reader.ReadInt16();
+            MaxAmmoDropped = reader.ReadInt16();
+            MaxCellsUsed = reader.ReadInt16();
+            MaxCellsDropped = reader.ReadInt16();
+            MaxHyposUsed = reader.ReadInt16();
+            MaxHyposDropped = reader.ReadInt16();
+            MaxShieldsUsed = reader.ReadInt16();
+            MaxShieldsDropped = reader.ReadInt16();
+            MaxCloakUsed = reader.ReadInt16();
+            MaxCloakDropped = reader.ReadInt16();
+
+            reader.Skip(4);
+
+            Team = (CharacterTeam)reader.ReadInt32();
+            AmmoPercent = reader.ReadInt32();
+
+            InitialAlertLevel = (CharacterAlertStatus)reader.ReadInt32();
+            MinimalAlertLevel = (CharacterAlertStatus)reader.ReadInt32();
+            JobStartingAlertLevel = (CharacterAlertStatus)reader.ReadInt32();
+            InvestigatingAlertLevel = (CharacterAlertStatus)reader.ReadInt32();
+
+            AlarmGroups = reader.ReadInt32();
+
+            PursuitStrongUnseen = (CharacterPursuitMode)reader.ReadInt32();
+            PursuitWeakUnseen = (CharacterPursuitMode)reader.ReadInt32();
+            PursuitStrongSeen = (CharacterPursuitMode)reader.ReadInt32();
+            PursuitWeakSeen = (CharacterPursuitMode)reader.ReadInt32();
+            PursuitLost = (CharacterPursuitLostBehavior)reader.ReadInt32();
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            Flags = xml.ReadElementContentAsEnum<CharacterFlags>("Flags");
+            ClassName = xml.ReadElementContentAsString("Class", "");
+            Name = xml.ReadElementContentAsString("Name", "");
+            WeaponClassName = xml.ReadElementContentAsString("Weapon", "");
+            
+            xml.ReadStartElement("Scripts");
+            OnSpawn = xml.ReadElementContentAsString("Spawn", "");
+            OnDeath = xml.ReadElementContentAsString("Die", "");
+            OnSeenEnemy = xml.ReadElementContentAsString("Combat", "");
+            OnAlarmed = xml.ReadElementContentAsString("Alarm", "");
+            OnHurt = xml.ReadElementContentAsString("Hurt", "");
+            OnDefeated = xml.ReadElementContentAsString("Defeated", "");
+            OnOutOfAmmo = xml.ReadElementContentAsString("OutOfAmmo", "");
+            OnNoPath = xml.ReadElementContentAsString("NoPath", "");
+            xml.ReadEndElement();
+
+            AdditionalHealth = xml.ReadElementContentAsInt("AdditionalHealth", "");
+
+            xml.ReadStartElement("Job");
+            Job = xml.ReadElementContentAsEnum<CharacterJobType>("Type");
+            PatrolPathId = xml.ReadElementContentAsInt("PatrolPathId", "");
+            xml.ReadEndElement();
+
+            xml.ReadStartElement("Behaviors");
+            CombatId = xml.ReadElementContentAsInt("CombatId", "");
+            MeleeId = xml.ReadElementContentAsInt("MeleeId", "");
+            NeutralId = xml.ReadElementContentAsInt("NeutralId", "");
+            xml.ReadEndElement();
+
+            xml.ReadStartElement("Inventory");
+            xml.ReadStartElement("Ammo");
+            MaxAmmoUsed = xml.ReadElementContentAsInt("Use", "");
+            MaxAmmoDropped = xml.ReadElementContentAsInt("Drop", "");
+            xml.ReadEndElement();
+            xml.ReadStartElement("EnergyCell");
+            MaxCellsUsed = xml.ReadElementContentAsInt("Use", "");
+            MaxCellsDropped = xml.ReadElementContentAsInt("Drop", "");
+            xml.ReadEndElement();
+            xml.ReadStartElement("Hypo");
+            MaxHyposUsed = xml.ReadElementContentAsInt("Use", "");
+            MaxHyposDropped = xml.ReadElementContentAsInt("Drop", "");
+            xml.ReadEndElement();
+            xml.ReadStartElement("Shield");
+            MaxShieldsUsed = xml.ReadElementContentAsInt("Use", "");
+            MaxShieldsDropped = xml.ReadElementContentAsInt("Drop", "");
+            xml.ReadEndElement();
+            xml.ReadStartElement("Invisibility");
+            MaxCloakUsed = xml.ReadElementContentAsInt("Use", "");
+            MaxCloakDropped = xml.ReadElementContentAsInt("Drop", "");
+            xml.ReadEndElement();
+            xml.ReadEndElement();
+
+            Team = xml.ReadElementContentAsEnum<CharacterTeam>("Team");
+            AmmoPercent = xml.ReadElementContentAsInt("AmmoPercentage", "");
+
+            xml.ReadStartElement("Alert");
+            InitialAlertLevel = xml.ReadElementContentAsEnum<CharacterAlertStatus>("Initial");
+            MinimalAlertLevel = xml.ReadElementContentAsEnum<CharacterAlertStatus>("Minimal");
+            JobStartingAlertLevel = xml.ReadElementContentAsEnum<CharacterAlertStatus>("JobStart");
+            InvestigatingAlertLevel = xml.ReadElementContentAsEnum<CharacterAlertStatus>("Investigate");
+            xml.ReadEndElement();
+
+            AlarmGroups = xml.ReadElementContentAsInt("AlarmGroups", "");
+
+            xml.ReadStartElement("Pursuit");
+            PursuitStrongUnseen = xml.ReadElementContentAsEnum<CharacterPursuitMode>("StrongUnseen");
+            PursuitWeakUnseen = xml.ReadElementContentAsEnum<CharacterPursuitMode>("WeakUnseen");
+            PursuitStrongSeen = xml.ReadElementContentAsEnum<CharacterPursuitMode>("StrongSeen");
+            PursuitWeakSeen = xml.ReadElementContentAsEnum<CharacterPursuitMode>("WeakSeen");
+            PursuitLost = xml.ReadElementContentAsEnum<CharacterPursuitLostBehavior>("Lost");
+            xml.ReadEndElement();
+        }
+    }
+}
Index: /OniSplit/Objects/CharacterAlertStatus.cs
===================================================================
--- /OniSplit/Objects/CharacterAlertStatus.cs	(revision 1114)
+++ /OniSplit/Objects/CharacterAlertStatus.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Objects
+{
+    internal enum CharacterAlertStatus : uint
+    {
+        Lull = 0,
+        Low = 1,
+        Medium = 2,
+        High = 3,
+        Combat = 4
+    }
+}
Index: /OniSplit/Objects/CharacterFlags.cs
===================================================================
--- /OniSplit/Objects/CharacterFlags.cs	(revision 1114)
+++ /OniSplit/Objects/CharacterFlags.cs	(revision 1114)
@@ -0,0 +1,23 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum CharacterFlags : uint
+    {
+        None = 0x00,
+        IsPlayer = 0x01,
+        RandomCostume = 0x02,
+        NotInitiallyPresent = 0x04,
+        NonCombatant = 0x08,
+        CanSpawnMultiple = 0x10,
+        Spawned = 0x20,
+        Unkillable = 0x40,
+        InfiniteAmmo = 0x80,
+        Omniscient = 0x0100,
+        HasLSI = 0x0200,
+        Boss = 0x0400,
+        UpgradeDifficulty = 0x0800,
+        NoAutoDrop = 0x1000
+    }
+}
Index: /OniSplit/Objects/CharacterJobType.cs
===================================================================
--- /OniSplit/Objects/CharacterJobType.cs	(revision 1114)
+++ /OniSplit/Objects/CharacterJobType.cs	(revision 1114)
@@ -0,0 +1,16 @@
+﻿namespace Oni.Objects
+{
+    internal enum CharacterJobType : uint
+    {
+        None = 0,
+        Idle = 1,
+        Guard = 2,
+        Patrol = 3,
+        TeamBatle = 4,
+        Combat = 5,
+        Melee = 6,
+        Alarm = 7,
+        Neutral = 8,
+        Panic = 9
+    }
+}
Index: /OniSplit/Objects/CharacterPursuitLostBehavior.cs
===================================================================
--- /OniSplit/Objects/CharacterPursuitLostBehavior.cs	(revision 1114)
+++ /OniSplit/Objects/CharacterPursuitLostBehavior.cs	(revision 1114)
@@ -0,0 +1,9 @@
+﻿namespace Oni.Objects
+{
+    internal enum CharacterPursuitLostBehavior : uint
+    {
+        ReturnToJob = 0,
+        KeepLooking = 1,
+        FindAlarm = 2,
+    }
+}
Index: /OniSplit/Objects/CharacterPursuitMode.cs
===================================================================
--- /OniSplit/Objects/CharacterPursuitMode.cs	(revision 1114)
+++ /OniSplit/Objects/CharacterPursuitMode.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Objects
+{
+    internal enum CharacterPursuitMode : uint
+    {
+        None = 0,
+        Forget = 1,
+        GoTo = 2,
+        Wait = 3,
+        Look = 4,
+        Move = 5,
+        Hunt = 6,
+        Glanc = 7,
+    }
+}
Index: /OniSplit/Objects/CharacterTeam.cs
===================================================================
--- /OniSplit/Objects/CharacterTeam.cs	(revision 1114)
+++ /OniSplit/Objects/CharacterTeam.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Objects
+{
+    internal enum CharacterTeam : uint
+    {
+        Konoko = 0,
+        TCTF = 1,
+        Syndicate = 2,
+        Neutral = 3,
+        SecurityGuard = 4,
+        RogueKonoko = 5,
+        Switzerland = 6,
+        SyndicateAccessory = 7
+    }
+}
Index: /OniSplit/Objects/Console.cs
===================================================================
--- /OniSplit/Objects/Console.cs	(revision 1114)
+++ /OniSplit/Objects/Console.cs	(revision 1114)
@@ -0,0 +1,89 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Objects
+{
+    internal class Console : GunkObject
+    {
+        public int ScriptId;
+        public ConsoleFlags Flags;
+        public string InactiveTexture;
+        public string ActiveTexture;
+        public string TriggeredTexture;
+        public ObjectEvent[] Events;
+
+        public Console()
+        {
+            TypeId = ObjectType.Console;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 63);
+            writer.WriteUInt16(ScriptId);
+            writer.WriteUInt16((ushort)Flags);
+            writer.Write(InactiveTexture, 63);
+            writer.Write(ActiveTexture, 63);
+            writer.Write(TriggeredTexture, 63);
+            ObjectEvent.WriteEventList(writer, Events);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(63);
+            ScriptId = reader.ReadUInt16();
+            Flags = (ConsoleFlags)reader.ReadInt16();
+            InactiveTexture = reader.ReadString(63);
+            ActiveTexture = reader.ReadString(63);
+            TriggeredTexture = reader.ReadString(63);
+            Events = ObjectEvent.ReadEventList(reader);
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            string className = null;
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Class":
+                        className = xml.ReadElementContentAsString();
+                        break;
+                    case "ConsoleId":
+                        ScriptId = xml.ReadElementContentAsInt();
+                        break;
+                    case "Flags":
+                        Flags = xml.ReadElementContentAsEnum<ConsoleFlags>();
+                        break;
+                    case "DisabledTexture":
+                    case "InactiveTexture":
+                        InactiveTexture = xml.ReadElementContentAsString();
+                        break;
+                    case "EnabledTexture":
+                    case "ActiveTexture":
+                        ActiveTexture = xml.ReadElementContentAsString();
+                        break;
+                    case "UsedTexture":
+                    case "TrigerredTexture":
+                        TriggeredTexture = xml.ReadElementContentAsString();
+                        break;
+                    case "Events":
+                        Events = ObjectEvent.ReadEventList(xml);
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            GunkClass = context.GetClass(TemplateTag.CONS, className, ConsoleClass.Read);
+        }
+    }
+}
Index: /OniSplit/Objects/ConsoleClass.cs
===================================================================
--- /OniSplit/Objects/ConsoleClass.cs	(revision 1114)
+++ /OniSplit/Objects/ConsoleClass.cs	(revision 1114)
@@ -0,0 +1,87 @@
+﻿using System.Xml;
+using Oni.Akira;
+using Oni.Metadata;
+using Oni.Motoko;
+using Oni.Physics;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class ConsoleClass : GunkObjectClass
+    {
+        public ConsoleClassFlags Flags;
+        public Vector3 ActionPoint;
+        public Vector3 ActionOrientation;
+        public ObjectNode Geometry;
+        public Geometry ScreenGeometry;
+        public GunkFlags ScreenGunkFlags;
+        public string InactiveTexture;
+        public string ActiveTexture;
+        public string TriggeredTexture;
+
+        public static ConsoleClass Read(InstanceDescriptor cons)
+        {
+            var klass = new ConsoleClass();
+
+            InstanceDescriptor geometryDescriptor;
+            InstanceDescriptor screenGeometryDescriptor;
+
+            using (var reader = cons.OpenRead())
+            {
+                klass.Flags = (ConsoleClassFlags)reader.ReadUInt32();
+                klass.ActionPoint = reader.ReadVector3();
+                klass.ActionOrientation = reader.ReadVector3();
+                geometryDescriptor = reader.ReadInstance();
+                screenGeometryDescriptor = reader.ReadInstance();
+                klass.ScreenGunkFlags = (GunkFlags)reader.ReadUInt32();
+                klass.InactiveTexture = reader.ReadString(32);
+                klass.ActiveTexture = reader.ReadString(32);
+                klass.TriggeredTexture = reader.ReadString(32);
+            }
+
+            if (geometryDescriptor != null)
+                klass.Geometry = ObjectDatReader.ReadObjectGeometry(geometryDescriptor);
+
+            if (screenGeometryDescriptor != null)
+                klass.ScreenGeometry = GeometryDatReader.Read(screenGeometryDescriptor);
+
+            return klass;
+        }
+
+        public static ConsoleClass Read(XmlReader xml)
+        {
+            var klass = new ConsoleClass();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Flags":
+                        klass.Flags = xml.ReadElementContentAsEnum<ConsoleClassFlags>();
+                        break;
+                    case "ActionPoint":
+                        klass.ActionPoint = xml.ReadElementContentAsVector3();
+                        break;
+                    case "ActionOrientation":
+                        klass.ActionOrientation = xml.ReadElementContentAsVector3();
+                        break;
+                    case "ConsoleGeometry":
+                        break;
+                    case "InactiveTexture":
+                        klass.InactiveTexture = xml.ReadElementContentAsString();
+                        break;
+                    case "ActiveTexture":
+                        klass.ActiveTexture = xml.ReadElementContentAsString();
+                        break;
+                    case "TriggeredTexture":
+                        klass.TriggeredTexture = xml.ReadElementContentAsString();
+                        break;
+                }
+            }
+
+            return klass;
+        }
+
+        public override ObjectGeometry[] GunkNodes => Geometry.Geometries;
+    }
+}
Index: /OniSplit/Objects/ConsoleClassFlags.cs
===================================================================
--- /OniSplit/Objects/ConsoleClassFlags.cs	(revision 1114)
+++ /OniSplit/Objects/ConsoleClassFlags.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum ConsoleClassFlags : uint
+    {
+        None = 0x00,
+        AlarmConsole = 0x01
+    }
+}
Index: /OniSplit/Objects/ConsoleFlags.cs
===================================================================
--- /OniSplit/Objects/ConsoleFlags.cs	(revision 1114)
+++ /OniSplit/Objects/ConsoleFlags.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum ConsoleFlags : ushort
+    {
+        None = 0x00,
+        InitialActive = 0x08,
+        Punch = 0x20,
+        IsAlarm = 0x40
+    }
+}
Index: /OniSplit/Objects/Door.cs
===================================================================
--- /OniSplit/Objects/Door.cs	(revision 1114)
+++ /OniSplit/Objects/Door.cs	(revision 1114)
@@ -0,0 +1,105 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class Door : GunkObject
+    {
+        public DoorClass Class;
+        public int ScriptId;
+        public int KeyId;
+        public DoorFlags Flags;
+        public Vector3 Center;
+        public float ActivationRadius = 30.0f;
+        public readonly string[] Textures = new string[2];
+        public ObjectEvent[] Events;
+
+        public Door()
+        {
+            TypeId = ObjectType.Door;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 63);
+            writer.WriteUInt16(ScriptId);
+            writer.WriteUInt16(KeyId);
+            writer.WriteUInt16((ushort)Flags);
+            writer.Write(Center);
+            writer.Write(ActivationRadius * ActivationRadius);
+            writer.Write(Textures[0], 63);
+            writer.Write(Textures[1], 63);
+            ObjectEvent.WriteEventList(writer, Events);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(63);
+            ScriptId = reader.ReadUInt16();
+            KeyId = reader.ReadUInt16();
+            Flags = (DoorFlags)reader.ReadInt16();
+            Center = reader.ReadVector3();
+            ActivationRadius = FMath.Sqrt(reader.ReadSingle());
+            Textures[0] = reader.ReadString(63).ToUpperInvariant();
+            Textures[1] = reader.ReadString(63).ToUpperInvariant();
+            Events = ObjectEvent.ReadEventList(reader);
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            string className = null;
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Class":
+                        className = xml.ReadElementContentAsString();
+                        break;
+                    case "DoorId":
+                        ScriptId = xml.ReadElementContentAsInt();
+                        break;
+                    case "KeyId":
+                        KeyId = xml.ReadElementContentAsInt();
+                        break;
+                    case "Flags":
+                        Flags = xml.ReadElementContentAsEnum<DoorFlags>();
+                        break;
+                    case "Center":
+                        Center = xml.ReadElementContentAsVector3();
+                        break;
+                    case "SquaredActivationRadius":
+                        ActivationRadius = FMath.Sqrt(xml.ReadElementContentAsFloat());
+                        break;
+                    case "ActivationRadius":
+                        ActivationRadius = xml.ReadElementContentAsFloat();
+                        break;
+                    case "Texture":
+                    case "Texture1":
+                        Textures[0] = xml.ReadElementContentAsString().ToUpperInvariant();
+                        break;
+                    case "Texture2":
+                        Textures[1] = xml.ReadElementContentAsString().ToUpperInvariant();
+                        break;
+                    case "Events":
+                        Events = ObjectEvent.ReadEventList(xml);
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+
+            }
+
+            Class = context.GetClass(TemplateTag.DOOR, className, DoorClass.Read);
+            GunkClass = Class;
+        }
+    }
+}
Index: /OniSplit/Objects/DoorClass.cs
===================================================================
--- /OniSplit/Objects/DoorClass.cs	(revision 1114)
+++ /OniSplit/Objects/DoorClass.cs	(revision 1114)
@@ -0,0 +1,53 @@
+﻿using Oni.Physics;
+
+namespace Oni.Objects
+{
+    internal class DoorClass : GunkObjectClass
+    {
+        public ObjectNode Geometry;
+        public string AnimationName;
+        public ObjectAnimation Animation;
+        public float SoundAttenuation;
+        public int AllowedSounds;
+        public int SoundType;
+        public float SoundVolume;
+        public string OpenSound;
+        public string CloseSound;
+
+        public static DoorClass Read(InstanceDescriptor door)
+        {
+            var klass = new DoorClass();
+
+            InstanceDescriptor geometryDescriptor;
+            InstanceDescriptor animationDescriptor;
+
+            using (var reader = door.OpenRead())
+            {
+                geometryDescriptor = reader.ReadInstance();
+                reader.Skip(4);
+                animationDescriptor = reader.ReadInstance();
+                klass.SoundAttenuation = reader.ReadSingle();
+                klass.AllowedSounds = reader.ReadInt32();
+                klass.SoundType = reader.ReadInt32();
+                klass.SoundVolume = reader.ReadSingle();
+                klass.OpenSound = reader.ReadString(32);
+                klass.CloseSound = reader.ReadString(32);
+            }
+
+            if (geometryDescriptor != null)
+            {
+                klass.Geometry = ObjectDatReader.ReadObjectGeometry(geometryDescriptor);
+            }
+
+            if (animationDescriptor != null)
+            {
+                klass.AnimationName = animationDescriptor.Name;
+                klass.Animation = ObjectDatReader.ReadAnimation(animationDescriptor);
+            }
+
+            return klass;
+        }
+
+        public override ObjectGeometry[] GunkNodes => Geometry.Geometries;
+    }
+}
Index: /OniSplit/Objects/DoorFlags.cs
===================================================================
--- /OniSplit/Objects/DoorFlags.cs	(revision 1114)
+++ /OniSplit/Objects/DoorFlags.cs	(revision 1114)
@@ -0,0 +1,19 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum DoorFlags : ushort
+    {
+        None = 0x00,
+        InitialLocked = 0x01,
+        InDoorFrame = 0x04,
+        Manual = 0x10,
+        DoubleDoor = 0x80,
+        Mirror = 0x0100,
+        OneWay = 0x0200,
+        Reverse = 0x0400,
+        Jammed = 0x800,
+        InitialOpen = 0x1000,
+    }
+}
Index: /OniSplit/Objects/Flag.cs
===================================================================
--- /OniSplit/Objects/Flag.cs	(revision 1114)
+++ /OniSplit/Objects/Flag.cs	(revision 1114)
@@ -0,0 +1,81 @@
+﻿using System;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class Flag : ObjectBase
+    {
+        public Color Color;
+        public string Prefix;
+        public int ScriptId;
+        public string Notes;
+
+        public Flag()
+        {
+            TypeId = ObjectType.Flag;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(Color);
+            writer.Write(Prefix, 2);
+            writer.WriteInt16(ScriptId);
+            writer.Write(Notes, 128);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            Color = reader.ReadColor();
+            Prefix = reader.ReadString(2);
+            ScriptId = reader.ReadInt16();
+            Notes = reader.ReadString(128);
+            Prefix = new string(new char[] { Prefix[1], Prefix[0] });
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Color":
+                        byte[] values = xml.ReadElementContentAsArray<byte>(XmlConvert.ToByte);
+
+                        if (values.Length > 3)
+                            Color = new Color(values[0], values[1], values[2], values[3]);
+                        else
+                            Color = new Color(values[0], values[1], values[2]);
+
+                        break;
+                    case "Prefix":
+                        string prefix = xml.ReadElementContentAsString();
+
+                        if (prefix.Length > 2)
+                        {
+                            int prefixId = int.Parse(prefix);
+                            prefix = new string(new char[2] { (char)((prefixId >> 8) & 0xff), (char)(prefixId & 0xff) });
+                        }
+
+                        Prefix = prefix;
+                        break;
+                    case "FlagId":
+                        ScriptId = xml.ReadElementContentAsInt();
+                        break;
+                    case "Note":
+                        Notes = xml.ReadElementContentAsString();
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Objects/Furniture.cs
===================================================================
--- /OniSplit/Objects/Furniture.cs	(revision 1114)
+++ /OniSplit/Objects/Furniture.cs	(revision 1114)
@@ -0,0 +1,58 @@
+﻿using System.Xml;
+
+namespace Oni.Objects
+{
+    internal class Furniture : GunkObject
+    {
+        public FurnitureClass Class;
+        public string ParticleTag;
+
+        public Furniture()
+        {
+            TypeId = ObjectType.Furniture;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 32);
+            writer.Write(ParticleTag, 48);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(32);
+            ParticleTag = reader.ReadString(48);
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            xml.WriteElementString("Class", ClassName);
+            xml.WriteElementString("Particle", ParticleTag);
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            string className = null;
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Class":
+                        className = xml.ReadElementContentAsString();
+                        break;
+                    case "Particle":
+                        ParticleTag = xml.ReadElementContentAsString();
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+
+            Class = context.GetClass(TemplateTag.OFGA, className, FurnitureClass.Read);
+            GunkClass = Class;
+        }
+    }
+}
Index: /OniSplit/Objects/FurnitureClass.cs
===================================================================
--- /OniSplit/Objects/FurnitureClass.cs	(revision 1114)
+++ /OniSplit/Objects/FurnitureClass.cs	(revision 1114)
@@ -0,0 +1,22 @@
+﻿using Oni.Physics;
+
+namespace Oni.Objects
+{
+    internal class FurnitureClass : GunkObjectClass
+    {
+        public ObjectNode Geometry;
+
+        public static FurnitureClass Read(InstanceDescriptor ofga)
+        {
+            if (ofga == null)
+                return null;
+
+            return new FurnitureClass
+            {
+                Geometry = ObjectDatReader.ReadObjectGeometry(ofga)
+            };
+        }
+
+        public override ObjectGeometry[] GunkNodes => Geometry.Geometries;
+    }
+}
Index: /OniSplit/Objects/GunkObject.cs
===================================================================
--- /OniSplit/Objects/GunkObject.cs	(revision 1114)
+++ /OniSplit/Objects/GunkObject.cs	(revision 1114)
@@ -0,0 +1,33 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    internal abstract class GunkObject : ObjectBase
+    {
+        private GunkObjectClass gunkClass;
+        private string className;
+
+        public GunkObjectClass GunkClass
+        {
+            get
+            {
+                return gunkClass;
+            }
+            protected set
+            {
+                gunkClass = value;
+
+                if (value != null)
+                    className = value.Name;
+            }
+        }
+
+        public string ClassName
+        {
+            get { return className; }
+            protected set { className = value; }
+        }
+
+        public int GunkId => ((int)TypeId << 24) | ObjectId;
+    }
+}
Index: /OniSplit/Objects/GunkObjectClass.cs
===================================================================
--- /OniSplit/Objects/GunkObjectClass.cs	(revision 1114)
+++ /OniSplit/Objects/GunkObjectClass.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+using Oni.Physics;
+
+namespace Oni.Objects
+{
+    internal abstract class GunkObjectClass : ObjectClass
+    {
+        public abstract ObjectGeometry[] GunkNodes
+        {
+            get;
+        }
+    }
+}
Index: /OniSplit/Objects/Neutral.cs
===================================================================
--- /OniSplit/Objects/Neutral.cs	(revision 1114)
+++ /OniSplit/Objects/Neutral.cs	(revision 1114)
@@ -0,0 +1,149 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Objects
+{
+    internal class Neutral : ObjectBase
+    {
+        public string Name;
+        public int Id;
+        public NeutralFlags Flags;
+        public float TriggerRange;
+        public float TalkRange;
+        public float FollowRange;
+        public float AbortEnemyRange;
+        public string TriggerSpeech;
+        public string AbortSpeech;
+        public string EnemySpeech;
+        public string EndScript;
+        public string WeaponClass;
+        public int AmmoAmount;
+        public int CellAmount;
+        public int HypoAmount;
+        public NeutralItems OtherItems;
+        public NeutralDialogLine[] Lines;
+
+        public Neutral()
+        {
+            TypeId = ObjectType.Neutral;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(Name, 32);
+            writer.WriteUInt16(Id);
+            writer.WriteUInt16(Lines.Length);
+            writer.Write((int)Flags);
+            writer.Write(TriggerRange);
+            writer.Write(TalkRange);
+            writer.Write(FollowRange);
+            writer.Write(AbortEnemyRange);
+            writer.Write(TriggerSpeech, 32);
+            writer.Write(AbortSpeech, 32);
+            writer.Write(EnemySpeech, 32);
+            writer.Write(EndScript, 32);
+            writer.Write(WeaponClass, 32);
+            writer.WriteByte(AmmoAmount);
+            writer.WriteByte(CellAmount);
+            writer.WriteByte(HypoAmount);
+            writer.WriteByte((byte)OtherItems);
+
+            foreach (var line in Lines)
+            {
+                writer.Write((int)line.Flags);
+                writer.WriteUInt16((ushort)line.AnimType);
+                writer.WriteUInt16((ushort)line.OtherAnimationType);
+                writer.Write(line.Speech, 32);
+            }
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            Name = reader.ReadString(32);
+            Id = reader.ReadInt16();
+            Lines = new NeutralDialogLine[reader.ReadInt16()];
+            Flags = (NeutralFlags)reader.ReadInt32();
+            TriggerRange = reader.ReadSingle();
+            TalkRange = reader.ReadSingle();
+            FollowRange = reader.ReadSingle();
+            AbortEnemyRange = reader.ReadSingle();
+            TriggerSpeech = reader.ReadString(32);
+            AbortSpeech = reader.ReadString(32);
+            EnemySpeech = reader.ReadString(32);
+            EndScript = reader.ReadString(32);
+            WeaponClass = reader.ReadString(32);
+            AmmoAmount = reader.ReadByte();
+            CellAmount = reader.ReadByte();
+            HypoAmount = reader.ReadByte();
+            OtherItems = (NeutralItems)reader.ReadByte();
+
+            for (int i = 0; i < Lines.Length; i++)
+            {
+                Lines[i] = new NeutralDialogLine
+                {
+                    Flags = (NeutralDialogLineFlags)reader.ReadInt32(),
+                    AnimType = (Totoro.AnimationType)reader.ReadUInt16(),
+                    OtherAnimationType = (Totoro.AnimationType)reader.ReadUInt16(),
+                    Speech = reader.ReadString(32)
+                };
+            }
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            Name = xml.ReadElementContentAsString("Name", "");
+            Id = xml.ReadElementContentAsInt("NeutralId", "");
+            Flags = xml.ReadElementContentAsEnum<NeutralFlags>("Flags");
+            xml.ReadStartElement("Ranges");
+            TriggerRange = xml.ReadElementContentAsFloat("Trigger", "");
+            TalkRange = xml.ReadElementContentAsFloat("Talk", "");
+            FollowRange = xml.ReadElementContentAsFloat("Follow", "");
+            AbortEnemyRange = xml.ReadElementContentAsFloat("Enemy", "");
+            xml.ReadEndElement();
+            xml.ReadStartElement("Speech");
+            TriggerSpeech = xml.ReadElementContentAsString("Trigger", "");
+            AbortSpeech = xml.ReadElementContentAsString("Abort", "");
+            EnemySpeech = xml.ReadElementContentAsString("Enemy", "");
+            xml.ReadEndElement();
+            xml.ReadStartElement("Script");
+            EndScript = xml.ReadElementContentAsString("AfterTalk", "");
+            xml.ReadEndElement();
+            xml.ReadStartElement("Rewards");
+            WeaponClass = xml.ReadElementContentAsString("WeaponClass", "");
+            AmmoAmount = xml.ReadElementContentAsInt("Ammo", "");
+            CellAmount = xml.ReadElementContentAsInt("EnergyCell", "");
+            HypoAmount = xml.ReadElementContentAsInt("Hypo", "");
+            OtherItems = xml.ReadElementContentAsEnum<NeutralItems>("Other");
+            xml.ReadEndElement();
+            xml.ReadStartElement("DialogLines");
+
+            var lineList = new List<NeutralDialogLine>();
+
+            while (xml.IsStartElement())
+            {
+                xml.ReadStartElement("DialogLine");
+
+                lineList.Add(new NeutralDialogLine
+                {
+                    Flags = xml.ReadElementContentAsEnum<NeutralDialogLineFlags>("Flags"),
+                    AnimType = xml.ReadElementContentAsEnum<Totoro.AnimationType>("Anim"),
+                    OtherAnimationType = xml.ReadElementContentAsEnum<Totoro.AnimationType>("OtherAnim"),
+                    Speech = xml.ReadElementContentAsString("SpeechName", "")
+                });
+
+                xml.ReadEndElement();
+            }
+
+            Lines = lineList.ToArray();
+
+            xml.ReadEndElement();
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}
Index: /OniSplit/Objects/NeutralDialogLine.cs
===================================================================
--- /OniSplit/Objects/NeutralDialogLine.cs	(revision 1114)
+++ /OniSplit/Objects/NeutralDialogLine.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+using Oni.Totoro;
+
+namespace Oni.Objects
+{
+    internal class NeutralDialogLine
+    {
+        public NeutralDialogLineFlags Flags;
+        public AnimationType AnimType;
+        public AnimationType OtherAnimationType;
+        public string Speech;
+    }
+}
Index: /OniSplit/Objects/NeutralDialogLineFlags.cs
===================================================================
--- /OniSplit/Objects/NeutralDialogLineFlags.cs	(revision 1114)
+++ /OniSplit/Objects/NeutralDialogLineFlags.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum NeutralDialogLineFlags : ushort
+    {
+        None = 0x00,
+        IsPlayer = 0x01,
+        GiveItems = 0x02,
+        AnimOnce = 0x04,
+        OtherAnimOnce = 0x08
+    }
+}
Index: /OniSplit/Objects/NeutralFlags.cs
===================================================================
--- /OniSplit/Objects/NeutralFlags.cs	(revision 1114)
+++ /OniSplit/Objects/NeutralFlags.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum NeutralFlags : uint
+    {
+        None = 0x00,
+        NoResume = 0x01,
+        NoResumeAfterGive = 0x02,
+        Uninterruptible = 0x04
+    }
+}
Index: /OniSplit/Objects/NeutralItems.cs
===================================================================
--- /OniSplit/Objects/NeutralItems.cs	(revision 1114)
+++ /OniSplit/Objects/NeutralItems.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum NeutralItems : byte
+    {
+        None = 0,
+        Shield = 1,
+        Invisibility = 2,
+        LSI = 4
+    }
+}
Index: /OniSplit/Objects/ObjcDatWriter.cs
===================================================================
--- /OniSplit/Objects/ObjcDatWriter.cs	(revision 1114)
+++ /OniSplit/Objects/ObjcDatWriter.cs	(revision 1114)
@@ -0,0 +1,141 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Metadata;
+
+namespace Oni.Objects
+{
+    internal class ObjcDatWriter : Importer
+    {
+        private readonly TypeTag tag;
+        private readonly string name;
+        private readonly List<Objects.ObjectBase> objects;
+        private readonly Type type;
+
+        private ObjcDatWriter(TypeTag tag, string name, List<Objects.ObjectBase> objects)
+        {
+            this.tag = tag;
+            this.name = name;
+            this.objects = objects;
+
+            if (tag == TypeTag.CHAR)
+                type = typeof(Character);
+            else if (tag == TypeTag.WEAP)
+                type = typeof(Weapon);
+            else if (tag == TypeTag.PART)
+                type = typeof(Particle);
+            else if (tag == TypeTag.PWRU)
+                type = typeof(PowerUp);
+            else if (tag == TypeTag.FLAG)
+                type = typeof(Flag);
+            else if (tag == TypeTag.DOOR)
+                type = typeof(Door);
+            else if (tag == TypeTag.CONS)
+                type = typeof(Objects.Console);
+            else if (tag == TypeTag.FURN)
+                type = typeof(Furniture);
+            else if (tag == TypeTag.TRIG)
+                type = typeof(Trigger);
+            else if (tag == TypeTag.TRGV)
+                type = typeof(TriggerVolume);
+            else if (tag == TypeTag.SNDG)
+                type = typeof(Objects.Sound);
+            else if (tag == TypeTag.TURR)
+                type = typeof(Turret);
+            else if (tag == TypeTag.NEUT)
+                type = typeof(Neutral);
+            else if (tag == TypeTag.PATR)
+                type = typeof(PatrolPath);
+        }
+
+        private enum TypeTag
+        {
+            CHAR = 0x43484152,
+            CMBT = 0x434d4254,
+            CONS = 0x434f4e53,
+            DOOR = 0x444f4f52,
+            FLAG = 0x464c4147,
+            FURN = 0x4655524e,
+            MELE = 0x4d454c45,
+            NEUT = 0x4e455554,
+            PART = 0x50415254,
+            PATR = 0x50415452,
+            PWRU = 0x50575255,
+            SNDG = 0x534e4447,
+            TRGV = 0x54524756,
+            TRIG = 0x54524947,
+            TURR = 0x54555252,
+            WEAP = 0x57454150
+        }
+
+        public static void Write(List<Objects.ObjectBase> objects, string outputDirPath)
+        {
+            System.Console.Error.WriteLine("Writing {0} objects...", objects.Count);
+
+            Write(TypeTag.CHAR, "Character", objects, outputDirPath);
+            Write(TypeTag.CONS, "Console", objects, outputDirPath);
+            Write(TypeTag.DOOR, "Door", objects, outputDirPath);
+            Write(TypeTag.FLAG, "Flag", objects, outputDirPath);
+            Write(TypeTag.FURN, "Furniture", objects, outputDirPath);
+            Write(TypeTag.NEUT, "Neutral", objects, outputDirPath);
+            Write(TypeTag.PART, "Particle", objects, outputDirPath);
+            Write(TypeTag.PATR, "Patrol Path", objects, outputDirPath);
+            Write(TypeTag.PWRU, "PowerUp", objects, outputDirPath);
+            Write(TypeTag.SNDG, "Sound", objects, outputDirPath);
+            Write(TypeTag.TRIG, "Trigger", objects, outputDirPath);
+            Write(TypeTag.TRGV, "Trigger Volume", objects, outputDirPath);
+            Write(TypeTag.TURR, "Turret", objects, outputDirPath);
+            Write(TypeTag.WEAP, "Weapon", objects, outputDirPath);
+        }
+
+        private static void Write(TypeTag tag, string name, List<Objects.ObjectBase> objects, string outputDirPath)
+        {
+            var writer = new ObjcDatWriter(tag, name, objects);
+            writer.Import(null, outputDirPath);
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            BeginImport();
+
+            var bina = CreateInstance(TemplateTag.BINA, "CJBO" + name);
+
+            int offset = RawWriter.Align32();
+            int size = WriteCollection(RawWriter);
+
+            using (var writer = bina.OpenWrite())
+            {
+                writer.Write(size);
+                writer.Write(offset);
+            }
+
+            Write(outputDirPath);
+        }
+
+        private int WriteCollection(BinaryWriter raw)
+        {
+            int start = raw.Position;
+
+            raw.Write((int)BinaryTag.OBJC);
+            raw.Write(0);
+            raw.Write(39);
+
+            foreach (var obj in objects.Where(o => o.GetType() == type))
+            {
+                int objectStartPosition = raw.Position;
+                raw.Write(0);
+                raw.Write((int)tag);
+
+                obj.Write(raw);
+
+                raw.Position = Utils.Align4(raw.Position);
+                int objectSize = raw.Position - objectStartPosition - 4;
+                raw.WriteAt(objectStartPosition, objectSize);
+            }
+
+            raw.Write(0);
+            int size = raw.Position - start;
+            raw.WriteAt(start + 4, size - 8);
+            return size;
+        }
+    }
+}
Index: /OniSplit/Objects/ObjectBase.cs
===================================================================
--- /OniSplit/Objects/ObjectBase.cs	(revision 1114)
+++ /OniSplit/Objects/ObjectBase.cs	(revision 1114)
@@ -0,0 +1,112 @@
+﻿using System;
+using System.Xml;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal abstract class ObjectBase
+    {
+        private ObjectType typeId;
+        private int objectId;
+        private ObjectFlags objectFlags;
+        private Vector3 position;
+        private Vector3 rotation;
+        private Matrix? transform;
+
+        public ObjectType TypeId
+        {
+            get { return typeId; }
+            protected set { typeId = value; }
+        }
+
+        public int ObjectId
+        {
+            get { return objectId; }
+            set { objectId = value; }
+        }
+
+        public Matrix Transform
+        {
+            get
+            {
+                if (transform == null)
+                {
+                    transform = Matrix.CreateRotationX(MathHelper.ToRadians(rotation.X))
+                        * Matrix.CreateRotationY(MathHelper.ToRadians(rotation.Y))
+                        * Matrix.CreateRotationZ(MathHelper.ToRadians(rotation.Z))
+                        * Matrix.CreateTranslation(position);
+                }
+
+                return transform.Value;
+            }
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write(objectId);
+            writer.Write((int)objectFlags);
+            writer.Write(position);
+            writer.Write(rotation);
+            WriteOsd(writer);
+        }
+
+        protected abstract void WriteOsd(BinaryWriter writer);
+
+        public void Read(BinaryReader reader)
+        {
+            objectId = reader.ReadInt32();
+            objectFlags = (ObjectFlags)reader.ReadInt32();
+            position = reader.ReadVector3();
+            rotation = reader.ReadVector3();
+            ReadOsd(reader);
+        }
+
+        protected abstract void ReadOsd(BinaryReader reader);
+
+        public void Write(XmlWriter xml)
+        {
+            xml.WriteStartElement("Header");
+            xml.WriteEndElement();
+            xml.WriteStartElement("OSD");
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+        }
+
+        protected abstract void WriteOsd(XmlWriter xml);
+
+        public void Read(XmlReader xml, ObjectLoadContext context)
+        {
+            xml.ReadStartElement();
+            xml.ReadStartElement("Header");
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Flags":
+                        objectFlags = Oni.Metadata.MetaEnum.Parse<ObjectFlags>(xml.ReadElementContentAsString("Flags", ""));
+                        break;
+                    case "Position":
+                        position = xml.ReadElementContentAsVector3("Position");
+                        break;
+                    case "Rotation":
+                        rotation = xml.ReadElementContentAsVector3("Rotation");
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+
+            xml.ReadStartElement("OSD");
+            ReadOsd(xml, context);
+            xml.ReadEndElement();
+
+            xml.ReadEndElement();
+        }
+
+        protected abstract void ReadOsd(XmlReader xml, ObjectLoadContext context);
+    }
+}
Index: /OniSplit/Objects/ObjectClass.cs
===================================================================
--- /OniSplit/Objects/ObjectClass.cs	(revision 1114)
+++ /OniSplit/Objects/ObjectClass.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    internal abstract class ObjectClass
+    {
+        public string Name
+        {
+            get;
+            set;
+        }
+    }
+}
Index: /OniSplit/Objects/ObjectEvent.cs
===================================================================
--- /OniSplit/Objects/ObjectEvent.cs	(revision 1114)
+++ /OniSplit/Objects/ObjectEvent.cs	(revision 1114)
@@ -0,0 +1,101 @@
+﻿using System.Collections.Generic;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Objects
+{
+    internal class ObjectEvent
+    {
+        private ObjectEventType action;
+        private int targetId;
+        private string script;
+
+        public ObjectEvent()
+        {
+        }
+
+        private ObjectEvent(BinaryReader reader)
+        {
+            action = (ObjectEventType)reader.ReadInt16();
+
+            if (action == ObjectEventType.Script)
+                script = reader.ReadString(32);
+            else if (action != ObjectEventType.None)
+                targetId = reader.ReadUInt16();
+        }
+
+        public ObjectEventType Action => action;
+        public int TargetId => targetId;
+        public string Script => script;
+
+        public static ObjectEvent[] ReadEventList(BinaryReader reader)
+        {
+            var events = new ObjectEvent[reader.ReadUInt16()];
+
+            for (int i = 0; i < events.Length; i++)
+                events[i] = new ObjectEvent(reader);
+
+            return events;
+        }
+
+        public static void WriteEventList(BinaryWriter writer, ObjectEvent[] events)
+        {
+            if (events == null)
+            {
+                writer.WriteUInt16(0);
+                return;
+            }
+
+            writer.WriteUInt16(events.Length);
+
+            for (int i = 0; i < events.Length; i++)
+            {
+                writer.WriteUInt16((ushort)events[i].action);
+
+                if (events[i].action == ObjectEventType.Script)
+                    writer.Write(events[i].script, 32);
+                else if (events[i].action != ObjectEventType.None)
+                    writer.WriteUInt16(events[i].targetId);
+            }
+        }
+
+        public static ObjectEvent[] ReadEventList(XmlReader xml)
+        {
+            var events = new List<ObjectEvent>();
+
+            if (xml.IsStartElement("Events") && xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+                return events.ToArray();
+            }
+
+            xml.ReadStartElement("Events");
+
+            while (xml.IsStartElement())
+            {
+                var evt = new ObjectEvent();
+
+                evt.action = MetaEnum.Parse<ObjectEventType>(xml.LocalName);
+
+                switch (evt.action)
+                {
+                    case ObjectEventType.None:
+                        break;
+                    case ObjectEventType.Script:
+                        evt.script = xml.GetAttribute("Function");
+                        break;
+                    default:
+                        evt.targetId = XmlConvert.ToInt16(xml.GetAttribute("TargetId"));
+                        break;
+                }
+
+                events.Add(evt);
+                xml.Skip();
+            }
+
+            xml.ReadEndElement();
+
+            return events.ToArray();
+        }
+    }
+}
Index: /OniSplit/Objects/ObjectEventType.cs
===================================================================
--- /OniSplit/Objects/ObjectEventType.cs	(revision 1114)
+++ /OniSplit/Objects/ObjectEventType.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿namespace Oni.Objects
+{
+    internal enum ObjectEventType
+    {
+        None,
+        Script,
+        ActivateTurret,
+        DeactivateTurret,
+        ActivateConsole,
+        DeactivateConsole,
+        ActivateAlarm,
+        DeactivateAlaram,
+        ActivateTrigger,
+        DeactivateTrigger,
+        LockDoor,
+        UnlockDoor
+    }
+}
Index: /OniSplit/Objects/ObjectFlags.cs
===================================================================
--- /OniSplit/Objects/ObjectFlags.cs	(revision 1114)
+++ /OniSplit/Objects/ObjectFlags.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum ObjectFlags
+    {
+        None = 0x0000,
+        Locked = 0x0001,
+        PlacedInGame = 0x0002,
+        Temporary = 0x0004,
+        Gunk = 0x0008
+    }
+}
Index: /OniSplit/Objects/ObjectLoadContext.cs
===================================================================
--- /OniSplit/Objects/ObjectLoadContext.cs	(revision 1114)
+++ /OniSplit/Objects/ObjectLoadContext.cs	(revision 1114)
@@ -0,0 +1,59 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Objects
+{
+    internal class ObjectLoadContext
+    {
+        private readonly Func<TemplateTag, string, ObjectLoadContext, InstanceDescriptor> getDescriptor;
+        private readonly TextWriter info;
+        private readonly Dictionary<string, ObjectClass> classCache;
+        private string basePath;
+        private string filePath;
+
+        public ObjectLoadContext(Func<TemplateTag, string, ObjectLoadContext, InstanceDescriptor> getDescriptor, TextWriter info)
+        {
+            this.getDescriptor = getDescriptor;
+            this.info = info;
+            this.classCache = new Dictionary<string, ObjectClass>(StringComparer.Ordinal);
+        }
+
+        public T GetClass<T>(TemplateTag tag, string name, Func<InstanceDescriptor, T> reader)
+            where T : ObjectClass, new()
+        {
+            ObjectClass klass;
+
+            string fullName = tag.ToString() + name;
+
+            if (!classCache.TryGetValue(fullName, out klass))
+            {
+                var descriptor = getDescriptor(tag, name, this);
+
+                if (descriptor != null)
+                {
+                    info.WriteLine("Using {0} object class '{1}' from '{2}'", tag, descriptor.Name, descriptor.FilePath);
+
+                    klass = reader(descriptor);
+                    klass.Name = descriptor.Name;
+                }
+
+                classCache.Add(fullName, klass);
+            }
+
+            return (T)klass;
+        }
+
+        public string BasePath
+        {
+            get { return basePath; }
+            set { basePath = value; }
+        }
+
+        public string FilePath
+        {
+            get { return filePath; }
+            set { filePath = value; }
+        }
+    }
+}
Index: /OniSplit/Objects/ObjectType.cs
===================================================================
--- /OniSplit/Objects/ObjectType.cs	(revision 1114)
+++ /OniSplit/Objects/ObjectType.cs	(revision 1114)
@@ -0,0 +1,25 @@
+﻿namespace Oni.Objects
+{
+    internal enum ObjectType
+    {
+        None = 0,
+        Character = 1,
+        PatrolPath = 2,
+        Door = 3,
+        Flag = 4,
+        Furniture = 5,
+        Machine = 6,
+        LSI = 7,
+        Particle = 8,
+        PowerUp = 9,
+        Sound = 10,
+        TriggerVolume = 11,
+        Weapon = 12,
+        Trigger = 13,
+        Turret = 14,
+        Console = 15,
+        Combat = 16,
+        Melee = 17,
+        Neutral = 18
+    }
+}
Index: /OniSplit/Objects/Particle.cs
===================================================================
--- /OniSplit/Objects/Particle.cs	(revision 1114)
+++ /OniSplit/Objects/Particle.cs	(revision 1114)
@@ -0,0 +1,69 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class Particle : ObjectBase
+    {
+        public string ClassName;
+        public string Tag;
+        public ParticleFlags Flags;
+        public Vector2 DecalScale = new Vector2(1.0f, 1.0f);
+
+        public Particle()
+        {
+            TypeId = ObjectType.Particle;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 64);
+            writer.Write(Tag, 48);
+            writer.Write((ushort)Flags);
+            writer.Write(DecalScale);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(64);
+            Tag = reader.ReadString(48);
+            Flags = (ParticleFlags)reader.ReadUInt16();
+            DecalScale = reader.ReadVector2();
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            xml.WriteElementString("Class", ClassName);
+            xml.WriteElementString("Tag", Tag);
+            xml.WriteElementString("Flags", MetaEnum.ToString<ParticleFlags>(Flags));
+            xml.WriteElementString("DecalScale", XmlConvert.ToString(DecalScale.X) + " " + XmlConvert.ToString(DecalScale.Y));
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Class":
+                        ClassName = xml.ReadElementContentAsString();
+                        break;
+                    case "Tag":
+                        Tag = xml.ReadElementContentAsString();
+                        break;
+                    case "Flags":
+                        Flags = xml.ReadElementContentAsEnum<ParticleFlags>() & ParticleFlags.NotInitiallyCreated;
+                        break;
+                    case "DecalScale":
+                        DecalScale = xml.ReadElementContentAsVector2();
+                        break;
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Objects/ParticleFlags.cs
===================================================================
--- /OniSplit/Objects/ParticleFlags.cs	(revision 1114)
+++ /OniSplit/Objects/ParticleFlags.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum ParticleFlags : ushort
+    {
+        None = 0x00,
+        NotInitiallyCreated = 0x02,
+    }
+}
Index: /OniSplit/Objects/PatrolPath.cs
===================================================================
--- /OniSplit/Objects/PatrolPath.cs	(revision 1114)
+++ /OniSplit/Objects/PatrolPath.cs	(revision 1114)
@@ -0,0 +1,268 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Objects
+{
+    internal class PatrolPath : ObjectBase
+    {
+        private string name;
+        private PatrolPathPoint[] points;
+        private int patrolId;
+        private int returnToNearest;
+
+        public PatrolPath()
+        {
+            TypeId = ObjectType.PatrolPath;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(name, 32);
+            writer.Write(points.Length);
+            writer.WriteUInt16(patrolId);
+            writer.WriteUInt16(returnToNearest);
+
+            foreach (var point in points)
+            {
+                writer.Write((int)point.Type);
+
+                switch (point.Type)
+                {
+                    case PatrolPathPointType.Loop:
+                    case PatrolPathPointType.Stop:
+                    case PatrolPathPointType.StopLooking:
+                    case PatrolPathPointType.StopScanning:
+                    case PatrolPathPointType.FreeFacing:
+                        break;
+
+                    case PatrolPathPointType.LoopFrom:
+                        writer.Write((int)point.Attributes["From"]);
+                        break;
+
+                    case PatrolPathPointType.IgnorePlayer:
+                        writer.WriteByte((byte)point.Attributes["Value"]);
+                        break;
+
+                    case PatrolPathPointType.ForkScript:
+                    case PatrolPathPointType.CallScript:
+                        writer.Write((short)point.Attributes["ScriptId"]);
+                        break;
+
+                    case PatrolPathPointType.MoveToFlag:
+                    case PatrolPathPointType.LookAtFlag:
+                    case PatrolPathPointType.MoveAndFaceFlag:
+                        writer.Write((short)point.Attributes["FlagId"]);
+                        break;
+
+                    case PatrolPathPointType.MovementMode:
+                        writer.Write((int)(PatrolPathMovementMode)point.Attributes["Mode"]);
+                        break;
+
+                    case PatrolPathPointType.LockFacing:
+                        writer.Write((int)(PatrolPathFacing)point.Attributes["Facing"]);
+                        break;
+
+                    case PatrolPathPointType.Pause:
+                        writer.Write((int)point.Attributes["Frames"]);
+                        break;
+
+                    case PatrolPathPointType.GlanceAtFlagFor:
+                        writer.Write((short)point.Attributes["FlagId"]);
+                        writer.Write((int)point.Attributes["Frames"]);
+                        break;
+
+                    case PatrolPathPointType.Scan:
+                        writer.Write((short)point.Attributes["Frames"]);
+                        writer.Write((float)point.Attributes["Rotation"]);
+                        break;
+
+                    case PatrolPathPointType.MoveThroughFlag:
+                    case PatrolPathPointType.MoveNearFlag:
+                        writer.Write((short)point.Attributes["FlagId"]);
+                        writer.Write((float)point.Attributes["Distance"]);
+                        break;
+
+                    case PatrolPathPointType.MoveToFlagLookAndWait:
+                        writer.Write((short)point.Attributes["Frames"]);
+                        writer.Write((short)point.Attributes["FlagId"]);
+                        writer.Write((float)point.Attributes["Rotation"]);
+                        break;
+
+                    case PatrolPathPointType.FaceToFlagAndFire:
+                        writer.Write((short)point.Attributes["FlagId"]);
+                        writer.Write((short)point.Attributes["Frames"]);
+                        writer.Write((float)point.Attributes["Spread"]);
+                        break;
+
+                    case PatrolPathPointType.LookAtPoint:
+                    case PatrolPathPointType.MoveToPoint:
+                        writer.Write((Vector3)point.Attributes["Point"]);
+                        break;
+
+                    case PatrolPathPointType.MoveThroughPoint:
+                        writer.Write((Vector3)point.Attributes["Point"]);
+                        writer.Write((float)point.Attributes["Distance"]);
+                        break;
+
+                    default:
+                        throw new NotSupportedException(string.Format("Unsupported path point type {0}", point.Type));
+                }
+            }
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            name = xml.ReadElementContentAsString("Name", "");
+            patrolId = xml.ReadElementContentAsInt("PatrolId", "");
+            returnToNearest = xml.ReadElementContentAsInt("ReturnToNearest", "");
+            bool isEmpty = xml.IsEmptyElement;
+            xml.ReadStartElement("Points");
+
+            if (isEmpty)
+            {
+                points = new PatrolPathPoint[0];
+                return;
+            }
+
+            var pointList = new List<PatrolPathPoint>();
+            int loopStart = -1;
+            bool inLoop = false;
+
+            for (int index = 0; xml.IsStartElement() || inLoop; index++)
+            {
+                if (!xml.IsStartElement())
+                {
+                    xml.ReadEndElement();
+                    inLoop = false;
+                    continue;
+                }
+
+                if (xml.LocalName == "Loop")
+                {
+                    if (xml.SkipEmpty())
+                        continue;
+
+                    inLoop = true;
+                    loopStart = index;
+                    xml.ReadStartElement();
+                    continue;
+                }
+
+                var point = new PatrolPathPoint(MetaEnum.Parse<PatrolPathPointType>(xml.LocalName));
+
+                switch (point.Type)
+                {
+                    case PatrolPathPointType.Stop:
+                    case PatrolPathPointType.StopLooking:
+                    case PatrolPathPointType.StopScanning:
+                    case PatrolPathPointType.FreeFacing:
+                        break;
+
+                    case PatrolPathPointType.IgnorePlayer:
+                        point.Attributes["Value"] = (byte)(xml.GetAttribute("Value") == "Yes" ? 1 : 0);
+                        break;
+
+                    case PatrolPathPointType.ForkScript:
+                    case PatrolPathPointType.CallScript:
+                        point.Attributes["ScriptId"] = XmlConvert.ToInt16(xml.GetAttribute("ScriptId"));
+                        break;
+
+                    case PatrolPathPointType.MoveToFlag:
+                    case PatrolPathPointType.LookAtFlag:
+                    case PatrolPathPointType.MoveAndFaceFlag:
+                        point.Attributes["FlagId"] = XmlConvert.ToInt16(xml.GetAttribute("FlagId"));
+                        break;
+
+                    case PatrolPathPointType.MovementMode:
+                        point.Attributes["Mode"] = Convert.ToInt32(MetaEnum.Parse<PatrolPathMovementMode>(xml.GetAttribute("Mode")));
+                        break;
+
+                    case PatrolPathPointType.LockFacing:
+                        point.Attributes["Facing"] = Convert.ToInt32(MetaEnum.Parse<PatrolPathFacing>(xml.GetAttribute("Facing")));
+                        break;
+
+                    case PatrolPathPointType.Pause:
+                        point.Attributes["Frames"] = XmlConvert.ToInt32(xml.GetAttribute("Frames"));
+                        break;
+
+                    case PatrolPathPointType.GlanceAtFlagFor:
+                        point.Attributes["FlagId"] = XmlConvert.ToInt16(xml.GetAttribute("FlagId"));
+                        point.Attributes["Frames"] = XmlConvert.ToInt32(xml.GetAttribute("Frames"));
+                        break;
+
+                    case PatrolPathPointType.Scan:
+                        point.Attributes["Frames"] = XmlConvert.ToInt16(xml.GetAttribute("Frames"));
+                        point.Attributes["Rotation"] = XmlConvert.ToSingle(xml.GetAttribute("Rotation"));
+                        break;
+
+                    case PatrolPathPointType.MoveThroughFlag:
+                    case PatrolPathPointType.MoveNearFlag:
+                        point.Attributes["FlagId"] = XmlConvert.ToInt16(xml.GetAttribute("FlagId"));
+                        point.Attributes["Distance"] = XmlConvert.ToSingle(xml.GetAttribute("Distance"));
+                        break;
+
+                    case PatrolPathPointType.MoveToFlagLookAndWait:
+                        point.Attributes["Frames"] = XmlConvert.ToInt16(xml.GetAttribute("Frames"));
+                        point.Attributes["FlagId"] = XmlConvert.ToInt16(xml.GetAttribute("FlagId"));
+                        point.Attributes["Rotation"] = XmlConvert.ToSingle(xml.GetAttribute("Rotation"));
+                        break;
+
+                    case PatrolPathPointType.FaceToFlagAndFire:
+                        point.Attributes["FlagId"] = XmlConvert.ToInt16(xml.GetAttribute("FlagId"));
+                        point.Attributes["Frames"] = XmlConvert.ToInt16(xml.GetAttribute("Frames"));
+                        point.Attributes["Spread"] = XmlConvert.ToSingle(xml.GetAttribute("Spread"));
+                        break;
+
+                    case PatrolPathPointType.LookAtPoint:
+                    case PatrolPathPointType.MoveToPoint:
+                        point.Attributes["Point"] = new Vector3(
+                            XmlConvert.ToSingle(xml.GetAttribute("X")),
+                            XmlConvert.ToSingle(xml.GetAttribute("Y")),
+                            XmlConvert.ToSingle(xml.GetAttribute("Z")));
+                        break;
+
+                    case PatrolPathPointType.MoveThroughPoint:
+                        point.Attributes["Point"] = new Vector3(
+                            XmlConvert.ToSingle(xml.GetAttribute("X")),
+                            XmlConvert.ToSingle(xml.GetAttribute("Y")),
+                            XmlConvert.ToSingle(xml.GetAttribute("Z")));
+                        point.Attributes["Distance"] = XmlConvert.ToSingle(xml.GetAttribute("Distance"));
+                        break;
+
+                    default:
+                        throw new NotSupportedException(string.Format("Unsupported path point type {0}", point.Type));
+                }
+
+                xml.Skip();
+                pointList.Add(point);
+            }
+
+            xml.ReadEndElement();
+
+            if (loopStart == 0)
+            {
+                pointList.Add(new PatrolPathPoint(PatrolPathPointType.Loop));
+            }
+            else if (loopStart > 0)
+            {
+                var point = new PatrolPathPoint(PatrolPathPointType.LoopFrom);
+                point.Attributes["From"] = loopStart;
+                pointList.Add(point);
+            }
+
+            points = pointList.ToArray();
+        }
+    }
+}
Index: /OniSplit/Objects/PatrolPathFacing.cs
===================================================================
--- /OniSplit/Objects/PatrolPathFacing.cs	(revision 1114)
+++ /OniSplit/Objects/PatrolPathFacing.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Objects
+{
+    internal enum PatrolPathFacing
+    {
+        Forward = 0,
+        Backward = 1,
+        Left = 1,
+        Right = 2,
+        Stopped = 3
+    }
+}
Index: /OniSplit/Objects/PatrolPathMovementMode.cs
===================================================================
--- /OniSplit/Objects/PatrolPathMovementMode.cs	(revision 1114)
+++ /OniSplit/Objects/PatrolPathMovementMode.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Objects
+{
+    internal enum PatrolPathMovementMode
+    {
+        ByAlertLevel = 0,
+        Stop = 1,
+        Crouch = 2,
+        Creep = 3,
+        WalkNoAim = 4,
+        Walk = 5,
+        RunNoAim = 6,
+        Run = 7
+    }
+}
Index: /OniSplit/Objects/PatrolPathPoint.cs
===================================================================
--- /OniSplit/Objects/PatrolPathPoint.cs	(revision 1114)
+++ /OniSplit/Objects/PatrolPathPoint.cs	(revision 1114)
@@ -0,0 +1,16 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Objects
+{
+    internal class PatrolPathPoint
+    {
+        public PatrolPathPointType Type;
+        public readonly Dictionary<string, object> Attributes = new Dictionary<string, object>();
+
+        public PatrolPathPoint(PatrolPathPointType type)
+        {
+            this.Type = type;
+        }
+    }
+}
Index: /OniSplit/Objects/PatrolPathPointType.cs
===================================================================
--- /OniSplit/Objects/PatrolPathPointType.cs	(revision 1114)
+++ /OniSplit/Objects/PatrolPathPointType.cs	(revision 1114)
@@ -0,0 +1,30 @@
+﻿namespace Oni.Objects
+{
+    internal enum PatrolPathPointType
+    {
+        MoveToFlag = 0,
+        Stop = 1,
+        Pause = 2,
+        LookAtFlag = 3,
+        LookAtPoint = 4,
+        MoveAndFaceFlag = 5,
+        Loop = 6,
+        MovementMode = 7,
+        MoveToPoint = 8,
+        LockFacing = 9,
+        MoveThroughFlag = 10,
+        MoveThroughPoint = 11,
+        StopLooking = 12,
+        FreeFacing = 13,
+        GlanceAtFlagFor = 14, // unused ?
+        MoveNearFlag = 15,
+        LoopFrom = 16,
+        Scan = 17,            // unused ?
+        StopScanning = 18,
+        MoveToFlagLookAndWait = 19,
+        CallScript = 20,
+        ForkScript = 21,
+        IgnorePlayer = 22,
+        FaceToFlagAndFire = 23
+    }
+}
Index: /OniSplit/Objects/PowerUp.cs
===================================================================
--- /OniSplit/Objects/PowerUp.cs	(revision 1114)
+++ /OniSplit/Objects/PowerUp.cs	(revision 1114)
@@ -0,0 +1,37 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class PowerUp : ObjectBase
+    {
+        public PowerUpClass Type;
+
+        public PowerUp()
+        {
+            TypeId = ObjectType.PowerUp;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write((uint)Type);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            Type = (PowerUpClass)reader.ReadUInt32();
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            xml.WriteElementString("Class", MetaEnum.ToString<PowerUpClass>(Type));
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            Type = xml.ReadElementContentAsEnum<PowerUpClass>("Class");
+        }
+    }
+}
Index: /OniSplit/Objects/PowerUpClass.cs
===================================================================
--- /OniSplit/Objects/PowerUpClass.cs	(revision 1114)
+++ /OniSplit/Objects/PowerUpClass.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Objects
+{
+    internal enum PowerUpClass : uint
+    {
+        Ammo = 0x424D4D41,
+        EnergyCell = 0x454D4D41,
+        Hypo = 0x4F505948,
+        Shield = 0x444C4853,
+        Invisibility = 0x49564E49,
+        LSI = 0x49534C41,
+    }
+}
Index: /OniSplit/Objects/Sound.cs
===================================================================
--- /OniSplit/Objects/Sound.cs	(revision 1114)
+++ /OniSplit/Objects/Sound.cs	(revision 1114)
@@ -0,0 +1,115 @@
+﻿using System;
+using System.Xml;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class Sound : ObjectBase
+    {
+        public string ClassName;
+        public float Volume = 1.0f;
+        public float Pitch = 1.0f;
+        public SoundVolumeType Type;
+        public BoundingBox Box;
+        public float MinDistance;
+        public float MaxDistance;
+
+        public Sound()
+        {
+            TypeId = ObjectType.Sound;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 32);
+            writer.Write((uint)Type);
+
+            if (Type == SoundVolumeType.Box)
+            {
+                writer.Write(Box);
+            }
+            else
+            {
+                writer.Write(MinDistance);
+                writer.Write(MaxDistance);
+            }
+
+            writer.Write(Volume);
+            writer.Write(Pitch);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(32);
+            Type = (SoundVolumeType)reader.ReadInt32();
+
+            if (Type == SoundVolumeType.Box)
+            {
+                Box = reader.ReadBoundingBox();
+            }
+            else if (Type == SoundVolumeType.Sphere)
+            {
+                MinDistance = reader.ReadSingle();
+                MaxDistance = reader.ReadSingle();
+            }
+
+            Volume = reader.ReadSingle();
+            Pitch = reader.ReadSingle();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            ClassName = xml.ReadElementContentAsString("Class", "");
+
+            if (xml.IsStartElement("Volume"))
+            {
+                Volume = xml.ReadElementContentAsFloat("Volume", "");
+                Pitch = xml.ReadElementContentAsFloat("Pitch", "");
+
+                if (xml.IsStartElement("Box", ""))
+                {
+                    Type = SoundVolumeType.Box;
+                    xml.ReadStartElement();
+                    Box.Min = xml.ReadElementContentAsVector3("Min");
+                    Box.Max = xml.ReadElementContentAsVector3("Max");
+                    xml.ReadEndElement();
+                }
+                else
+                {
+                    Type = SoundVolumeType.Sphere;
+                    xml.ReadStartElement("Spheres");
+                    MinDistance = xml.ReadElementContentAsFloat("Min", "");
+                    MaxDistance = xml.ReadElementContentAsFloat("Max", "");
+                    xml.ReadEndElement();
+                }
+            }
+            else
+            {
+                if (xml.IsStartElement("Box", ""))
+                {
+                    Type = SoundVolumeType.Box;
+                    xml.ReadStartElement();
+                    Box.Min = xml.ReadElementContentAsVector3("Min");
+                    Box.Max = xml.ReadElementContentAsVector3("Max");
+                    xml.ReadEndElement();
+                }
+                else
+                {
+                    Type = SoundVolumeType.Sphere;
+                    xml.ReadStartElement("Sphere");
+                    MinDistance = xml.ReadElementContentAsFloat("MinRadius", "");
+                    MaxDistance = xml.ReadElementContentAsFloat("MaxRadius", "");
+                    xml.ReadEndElement();
+                }
+
+                Volume = xml.ReadElementContentAsFloat("Volume", "");
+                Pitch = xml.ReadElementContentAsFloat("Pitch", "");
+            }
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}
Index: /OniSplit/Objects/SoundVolumeType.cs
===================================================================
--- /OniSplit/Objects/SoundVolumeType.cs	(revision 1114)
+++ /OniSplit/Objects/SoundVolumeType.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    internal enum SoundVolumeType
+    {
+        Box = 0x564C4D45,
+        Sphere = 0x53504852
+    }
+}
Index: /OniSplit/Objects/Trigger.cs
===================================================================
--- /OniSplit/Objects/Trigger.cs	(revision 1114)
+++ /OniSplit/Objects/Trigger.cs	(revision 1114)
@@ -0,0 +1,92 @@
+﻿using System;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class Trigger : GunkObject
+    {
+        public int ScriptId;
+        public TriggerFlags Flags;
+        public Color LaserColor;
+        public float StartPosition;
+        public float Speed;
+        public int EmitterCount;
+        public int TimeOn;
+        public int TimeOff;
+        public ObjectEvent[] Events;
+
+        public Trigger()
+        {
+            TypeId = ObjectType.Trigger;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 63);
+            writer.WriteUInt16(ScriptId);
+            writer.WriteUInt16((ushort)Flags);
+            writer.Write(LaserColor);
+            writer.Write(StartPosition);
+            writer.Write(Speed);
+            writer.WriteUInt16(EmitterCount);
+            writer.WriteUInt16(TimeOn);
+            writer.WriteUInt16(TimeOff);
+            ObjectEvent.WriteEventList(writer, Events);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(63);
+            ScriptId = reader.ReadUInt16();
+            Flags = (TriggerFlags)(reader.ReadUInt16() & ~0x84);
+            LaserColor = reader.ReadColor();
+            StartPosition = reader.ReadSingle();
+            Speed = reader.ReadSingle();
+            EmitterCount = reader.ReadUInt16();
+            TimeOn = reader.ReadUInt16();
+            TimeOff = reader.ReadUInt16();
+            Events = ObjectEvent.ReadEventList(reader);
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            string className = xml.ReadElementContentAsString("Class", "");
+
+            ScriptId = xml.ReadElementContentAsInt("TriggerId", "");
+            Flags = xml.ReadElementContentAsEnum<TriggerFlags>("Flags");
+
+            byte[] values = xml.ReadElementContentAsArray<byte>(XmlConvert.ToByte, "LaserColor");
+
+            if (values.Length > 3)
+                LaserColor = new Color(values[0], values[1], values[2], values[3]);
+            else
+                LaserColor = new Color(values[0], values[1], values[2]);
+
+            StartPosition = xml.ReadElementContentAsFloat("StartPosition", "");
+            Speed = xml.ReadElementContentAsFloat("Speed", "");
+            EmitterCount = xml.ReadElementContentAsInt("EmitterCount", "");
+
+            if (xml.IsStartElement("Offset_0075"))
+                TimeOn = xml.ReadElementContentAsInt();
+            else
+                TimeOn = xml.ReadElementContentAsInt("TimeOn", "");
+
+            if (xml.IsStartElement("Offset_0077"))
+                TimeOff = xml.ReadElementContentAsInt();
+            else
+                TimeOff = xml.ReadElementContentAsInt("TimeOff", "");
+
+            Events = ObjectEvent.ReadEventList(xml);
+
+            GunkClass = context.GetClass(TemplateTag.TRIG, className, TriggerClass.Read);
+        }
+    }
+}
Index: /OniSplit/Objects/TriggerClass.cs
===================================================================
--- /OniSplit/Objects/TriggerClass.cs	(revision 1114)
+++ /OniSplit/Objects/TriggerClass.cs	(revision 1114)
@@ -0,0 +1,68 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Akira;
+using Oni.Imaging;
+using Oni.Motoko;
+using Oni.Physics;
+
+namespace Oni.Objects
+{
+    internal class TriggerClass : GunkObjectClass
+    {
+        public Color Color;
+        public int TimeOn;
+        public int TimeOff;
+        public float StartOffset;
+        public float AnimScale;
+        public Geometry RailGeometry;
+        public GunkFlags RailGunkFlags;
+        public string ActiveSoundName;
+        public string HitSoundName;
+
+        public static TriggerClass Read(InstanceDescriptor trig)
+        {
+            var klass = new TriggerClass();
+
+            InstanceDescriptor railGeometryDescriptor;
+
+            using (var reader = trig.OpenRead())
+            {
+                klass.Color = reader.ReadColor();
+                klass.TimeOn = reader.ReadUInt16();
+                klass.TimeOff = reader.ReadUInt16();
+                klass.StartOffset = reader.ReadSingle();
+                klass.AnimScale = reader.ReadSingle();
+                railGeometryDescriptor = reader.ReadInstance();
+                reader.Skip(4);
+                klass.RailGunkFlags = (GunkFlags)reader.ReadInt32();
+
+                // we do not need the emitter and animation for now
+                reader.Skip(8);
+                //trge = reader.ReadInstanceLink<TRGEInstance>();
+                //oban = reader.ReadInstanceLink<OBANInstance>();
+
+                klass.ActiveSoundName = reader.ReadString(32) + ".amb";
+                klass.HitSoundName = reader.ReadString(32) + ".imp";
+                reader.Skip(8);
+            }
+
+            if (railGeometryDescriptor != null)
+                klass.RailGeometry = GeometryDatReader.Read(railGeometryDescriptor);
+
+            return klass;
+        }
+
+        public override ObjectGeometry[] GunkNodes
+        {
+            get
+            {
+                return new[] {
+                    new ObjectGeometry {
+                        Geometry = RailGeometry,
+                        Flags = RailGunkFlags
+                    }
+                };
+            }
+        }
+    }
+}
Index: /OniSplit/Objects/TriggerFlags.cs
===================================================================
--- /OniSplit/Objects/TriggerFlags.cs	(revision 1114)
+++ /OniSplit/Objects/TriggerFlags.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum TriggerFlags : ushort
+    {
+        None = 0,
+        InitialActive = 0x0008,
+        ReverseAnim = 0x0010,
+        PingPong = 0x0020,
+    }
+}
Index: /OniSplit/Objects/TriggerVolume.cs
===================================================================
--- /OniSplit/Objects/TriggerVolume.cs	(revision 1114)
+++ /OniSplit/Objects/TriggerVolume.cs	(revision 1114)
@@ -0,0 +1,102 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class TriggerVolume : ObjectBase
+    {
+        public string Name;
+        public string EntryScript;
+        public string InsideScript;
+        public string ExitScript;
+        public TurretTargetTeams Teams;
+        public Vector3 Size;
+        public int ScriptId;
+        public int ParentId;
+        public string Notes;
+        public TriggerVolumeFlags Flags;
+
+        public TriggerVolume()
+        {
+            TypeId = ObjectType.TriggerVolume;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(Name, 63);
+            writer.Write(EntryScript, 32);
+            writer.Write(InsideScript, 32);
+            writer.Write(ExitScript, 32);
+            writer.Write((uint)Teams);
+            writer.Write(Size);
+            writer.Write(ScriptId);
+            writer.Write(ParentId);
+            writer.Write(Notes, 128);
+            writer.Write((uint)Flags);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            Name = reader.ReadString(63);
+            EntryScript = reader.ReadString(32);
+            InsideScript = reader.ReadString(32);
+            ExitScript = reader.ReadString(32);
+            Teams = (TurretTargetTeams)(reader.ReadUInt32() & 0xff);
+            Size = reader.ReadVector3();
+            ScriptId = reader.ReadInt32();
+            ParentId = reader.ReadInt32();
+            Notes = reader.ReadString(128);
+            Flags = (TriggerVolumeFlags)(reader.ReadUInt32() & 0xff);
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            throw new NotImplementedException();
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Name":
+                        Name = xml.ReadElementContentAsString();
+                        break;
+                    case "Scripts":
+                        xml.ReadStartElement();
+                        EntryScript = xml.ReadElementContentAsString("Entry", "");
+                        InsideScript = xml.ReadElementContentAsString("Inside", "");
+                        ExitScript = xml.ReadElementContentAsString("Exit", "");
+                        xml.ReadEndElement();
+                        break;
+                    case "Teams":
+                        Teams = xml.ReadElementContentAsEnum<TurretTargetTeams>();
+                        break;
+                    case "Size":
+                        Size = xml.ReadElementContentAsVector3();
+                        break;
+                    case "TriggerVolumeId":
+                    case "ScriptId":
+                        ScriptId = xml.ReadElementContentAsInt();
+                        break;
+                    case "ParentId":
+                        ParentId = xml.ReadElementContentAsInt();
+                        break;
+                    case "Notes":
+                        Notes = xml.ReadElementContentAsString();
+                        break;
+                    case "Flags":
+                        Flags = xml.ReadElementContentAsEnum<TriggerVolumeFlags>();
+                        break;
+
+                    default:
+                        xml.Skip();
+                        break;
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Objects/TriggerVolumeFlags.cs
===================================================================
--- /OniSplit/Objects/TriggerVolumeFlags.cs	(revision 1114)
+++ /OniSplit/Objects/TriggerVolumeFlags.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum TriggerVolumeFlags : uint
+    {
+        None = 0x00,
+        OneTimeEnter = 0x01,
+        OneTimeInside = 0x02,
+        OneTimeExit = 0x04,
+        EnterDisabled = 0x08,
+        InsideDisabled = 0x10,
+        ExitDisabled = 0x20,
+        Disabled = 0x40,
+        PlayerOnly = 0x80
+    }
+}
Index: /OniSplit/Objects/Turret.cs
===================================================================
--- /OniSplit/Objects/Turret.cs	(revision 1114)
+++ /OniSplit/Objects/Turret.cs	(revision 1114)
@@ -0,0 +1,56 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Objects
+{
+    internal class Turret : GunkObject
+    {
+        public int ScriptId;
+        public TurretFlags Flags;
+        public TurretTargetTeams TargetTeams;
+
+        public Turret()
+        {
+            TypeId = ObjectType.Turret;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 63);
+            writer.WriteUInt16(ScriptId);
+            writer.WriteUInt16((ushort)Flags);
+            writer.Skip(36);
+            writer.Write((uint)TargetTeams);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(63);
+            ScriptId = reader.ReadUInt16();
+            Flags = (TurretFlags)reader.ReadUInt16();
+            reader.Skip(36);
+            TargetTeams = (TurretTargetTeams)reader.ReadInt32();
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            xml.WriteElementString("Class", ClassName);
+            xml.WriteElementString("TurretId", XmlConvert.ToString(ScriptId));
+            xml.WriteElementString("Flags", MetaEnum.ToString<TurretFlags>(Flags));
+            xml.WriteElementString("TargetedTeams", MetaEnum.ToString<TurretTargetTeams>(TargetTeams));
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            string className = xml.ReadElementContentAsString("Class", "");
+
+            ScriptId = xml.ReadElementContentAsInt("TurretId", "");
+            Flags = xml.ReadElementContentAsEnum<TurretFlags>("Flags");
+            TargetTeams = xml.ReadElementContentAsEnum<TurretTargetTeams>("TargetedTeams");
+
+            GunkClass = context.GetClass(TemplateTag.TURR, className, TurretClass.Read);
+        }
+    }
+}
Index: /OniSplit/Objects/TurretClass.cs
===================================================================
--- /OniSplit/Objects/TurretClass.cs	(revision 1114)
+++ /OniSplit/Objects/TurretClass.cs	(revision 1114)
@@ -0,0 +1,90 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Akira;
+using Oni.Motoko;
+using Oni.Physics;
+
+namespace Oni.Objects
+{
+    internal class TurretClass : GunkObjectClass
+    {
+        public string BaseName;
+        public int Flags;
+        public int FreeTime;
+        public int ReloadTime;
+        public int BarrelCount;
+        public int RecoilAnimType;
+        public int ReloadAnimType;
+        public int MaxAmmo;
+        public int AttachmentCount;
+        public int ShooterCount;
+        public float AimingSpeed;
+        public Geometry BaseGeometry;
+        public GunkFlags BaseGunkFlags;
+        public Geometry TurretGeometry;
+        public GunkFlags TurretGunkFlags;
+        public Geometry BarrelGeometry;
+        public GunkFlags BarrelGunkFlags;
+        public Vector3 TurretPosition;
+        public Vector3 BarrelPosition;
+
+        public static TurretClass Read(InstanceDescriptor turr)
+        {
+            var klass = new TurretClass();
+
+            InstanceDescriptor baseGeometryDescriptor;
+            InstanceDescriptor turretGeometryDescriptor;
+            InstanceDescriptor barrelGeometryDescriptor;
+
+            using (var reader = turr.OpenRead())
+            {
+                klass.Name = reader.ReadString(32);
+                klass.BaseName = reader.ReadString(32);
+                klass.Flags = reader.ReadUInt16();
+                klass.FreeTime = reader.ReadUInt16();
+                klass.ReloadTime = reader.ReadUInt16();
+                klass.BarrelCount = reader.ReadUInt16();
+                klass.RecoilAnimType = reader.ReadUInt16();
+                klass.ReloadAnimType = reader.ReadUInt16();
+                klass.MaxAmmo = reader.ReadUInt16();
+                klass.AttachmentCount = reader.ReadUInt16();
+                klass.ShooterCount = reader.ReadUInt16();
+                reader.Skip(2);
+                klass.AimingSpeed = reader.ReadSingle();
+                baseGeometryDescriptor = reader.ReadInstance();
+                reader.Skip(4);
+                klass.BaseGunkFlags = (GunkFlags)reader.ReadInt32();
+                turretGeometryDescriptor = reader.ReadInstance();
+                klass.TurretGunkFlags = (GunkFlags)reader.ReadInt32();
+                barrelGeometryDescriptor = reader.ReadInstance();
+                klass.BarrelGunkFlags = (GunkFlags)reader.ReadInt32();
+                klass.TurretPosition = reader.ReadVector3();
+                klass.BarrelPosition = reader.ReadVector3();
+            }
+
+            if (baseGeometryDescriptor != null)
+                klass.BaseGeometry = GeometryDatReader.Read(baseGeometryDescriptor);
+
+            if (barrelGeometryDescriptor != null)
+                klass.BarrelGeometry = GeometryDatReader.Read(barrelGeometryDescriptor);
+
+            if (turretGeometryDescriptor != null)
+                klass.TurretGeometry = GeometryDatReader.Read(turretGeometryDescriptor);
+
+            return klass;
+        }
+
+        public override ObjectGeometry[] GunkNodes
+        {
+            get
+            {
+                return new[] {
+                    new ObjectGeometry {
+                        Geometry = BaseGeometry,
+                        Flags = BaseGunkFlags
+                    }
+                };
+            }
+        }
+    }
+}
Index: /OniSplit/Objects/TurretFlags.cs
===================================================================
--- /OniSplit/Objects/TurretFlags.cs	(revision 1114)
+++ /OniSplit/Objects/TurretFlags.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum TurretFlags : ushort
+    {
+        None = 0,
+        InitialActive = 0x0002,
+    }
+}
Index: /OniSplit/Objects/TurretTargetTeams.cs
===================================================================
--- /OniSplit/Objects/TurretTargetTeams.cs	(revision 1114)
+++ /OniSplit/Objects/TurretTargetTeams.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿using System;
+
+namespace Oni.Objects
+{
+    [Flags]
+    internal enum TurretTargetTeams : uint
+    {
+        None = 0x00,
+        Konoko = 0x01,
+        TCTF = 0x02,
+        Syndicate = 0x04,
+        Neutral = 0x08,
+        SecurityGuard = 0x10,
+        RogueKonoko = 0x20,
+        Switzerland = 0x40,
+        SyndicateAccessory = 0x80
+    }
+}
Index: /OniSplit/Objects/Weapon.cs
===================================================================
--- /OniSplit/Objects/Weapon.cs	(revision 1114)
+++ /OniSplit/Objects/Weapon.cs	(revision 1114)
@@ -0,0 +1,35 @@
+﻿using System;
+using System.Xml;
+
+namespace Oni.Objects
+{
+    internal class Weapon : ObjectBase
+    {
+        public string ClassName;
+
+        public Weapon()
+        {
+            TypeId = ObjectType.Weapon;
+        }
+
+        protected override void WriteOsd(BinaryWriter writer)
+        {
+            writer.Write(ClassName, 32);
+        }
+
+        protected override void ReadOsd(BinaryReader reader)
+        {
+            ClassName = reader.ReadString(32);
+        }
+
+        protected override void WriteOsd(XmlWriter xml)
+        {
+            xml.WriteElementString("Class", ClassName);
+        }
+
+        protected override void ReadOsd(XmlReader xml, ObjectLoadContext context)
+        {
+            ClassName = xml.ReadElementContentAsString("Class", "");
+        }
+    }
+}
Index: /OniSplit/Oni.xsd
===================================================================
--- /OniSplit/Oni.xsd	(revision 1114)
+++ /OniSplit/Oni.xsd	(revision 1114)
@@ -0,0 +1,44 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<xs:schema targetNamespace="Oni" attributeFormDefault="unqualified" elementFormDefault="unqualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:complexType id="ObjectHeader" name="ObjectHeader">
+        <xs:sequence>
+            <xs:element name="Flags" type="xs:string"/>
+            <xs:element name="Position" type="xs:string"/>
+            <xs:element name="Rotation" type="xs:string"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType id="DoorOsd" name="DoorOsd">
+        <xs:sequence>
+            <xs:element name="Class" type="xs:string"/>
+            <xs:element name="DoorId" type="xs:unsignedByte"/>
+            <xs:element name="KeyId" type="xs:unsignedByte"/>
+            <xs:element name="Flags" type="xs:string"/>
+            <xs:element name="Center" type="xs:string"/>
+            <xs:element minOccurs="0" name="SquaredActivationRadius" type="xs:unsignedShort"/>
+            <xs:element minOccurs="0" name="ActivationRadius" type="xs:decimal"/>
+            <xs:element name="Texture1" type="xs:string"/>
+            <xs:element name="Texture2" type="xs:string"/>
+            <xs:element name="Events"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType id="Door" name="Door">
+        <xs:sequence>
+            <xs:element name="Header" type="ObjectHeader"/>
+            <xs:element name="OSD" type="DoorOsd"/>
+        </xs:sequence>
+        <xs:attribute name="Id" type="xs:unsignedShort" use="required"/>
+    </xs:complexType>
+    <xs:element name="Oni">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="Objects">
+                    <xs:complexType>
+                        <xs:sequence>
+                            <xs:element maxOccurs="unbounded" name="DOOR" type="Door"/>
+                        </xs:sequence>
+                    </xs:complexType>
+                </xs:element>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+</xs:schema>
Index: /OniSplit/OniSplit.csproj
===================================================================
--- /OniSplit/OniSplit.csproj	(revision 1114)
+++ /OniSplit/OniSplit.csproj	(revision 1114)
@@ -0,0 +1,434 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{7337BAAA-A08B-4E98-97D6-F5B095D74BFF}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>Oni</RootNamespace>
+    <AssemblyName>OniSplit</AssemblyName>
+    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <WarningLevel>4</WarningLevel>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
+    <Prefer32Bit>false</Prefer32Bit>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <WarningLevel>4</WarningLevel>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
+    <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
+    <Prefer32Bit>false</Prefer32Bit>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="Akira\AkiraDaeNodeProperties.cs" />
+    <Compile Include="Akira\AkiraDaeReader.cs" />
+    <Compile Include="Akira\AkiraDaeWriter.cs" />
+    <Compile Include="Akira\AkiraDatReader.cs" />
+    <Compile Include="Akira\AkiraDatWriter.cs" />
+    <Compile Include="Akira\AkiraImporter.cs" />
+    <Compile Include="Akira\BspNode.cs" />
+    <Compile Include="Akira\RoomDaeWriter.cs" />
+    <Compile Include="Akira\RoomDaeReader.cs" />
+    <Compile Include="Akira\Polygon2Clipper.cs" />
+    <Compile Include="Akira\Material.cs" />
+    <Compile Include="Akira\MaterialLibrary.cs" />
+    <Compile Include="Akira\PolygonQuadrangulate.cs" />
+    <Compile Include="Akira\PolygonUtils.cs" />
+    <Compile Include="Akira\RoomExtractor.cs" />
+    <Compile Include="Akira\RoomGridRasterizer.cs" />
+    <Compile Include="Akira\Room.cs" />
+    <Compile Include="Akira\RoomAdjacency.cs" />
+    <Compile Include="Akira\RoomBspNode.cs" />
+    <Compile Include="Akira\RoomBuilder.cs" />
+    <Compile Include="Akira\RoomFlags.cs" />
+    <Compile Include="Akira\RoomGrid.cs" />
+    <Compile Include="Akira\RoomGridBuilder.cs" />
+    <Compile Include="Akira\RoomGridWeight.cs" />
+    <Compile Include="BinImporter.cs" />
+    <Compile Include="Game\WeaponClass.cs" />
+    <Compile Include="ImporterFile.cs" />
+    <Compile Include="Dae\Camera.cs" />
+    <Compile Include="Dae\CameraInstance.cs" />
+    <Compile Include="Dae\CameraType.cs" />
+    <Compile Include="DatWriter.cs" />
+    <Compile Include="Dae\Converters\FaceConverter.cs" />
+    <Compile Include="Dae\Converters\UnitConverter.cs" />
+    <Compile Include="Dae\EffectSamplerFilter.cs" />
+    <Compile Include="Dae\EffectSamplerWrap.cs" />
+    <Compile Include="Dae\IndexedInput.cs" />
+    <Compile Include="Dae\Input.cs" />
+    <Compile Include="Dae\IO\ObjReader.cs" />
+    <Compile Include="Dae\IO\ObjWriter.cs" />
+    <Compile Include="Dae\Reader.cs" />
+    <Compile Include="Dae\Writer.cs" />
+    <Compile Include="Dae\LightInstance.cs" />
+    <Compile Include="Dae\Light.cs" />
+    <Compile Include="Dae\LightType.cs" />
+    <Compile Include="Dae\Converters\AxisConverter.cs" />
+    <Compile Include="Dae\MaterialBinding.cs" />
+    <Compile Include="Dae\MeshPrimitives.cs" />
+    <Compile Include="Dae\MeshPrimitiveType.cs" />
+    <Compile Include="Dae\NodeInstance.cs" />
+    <Compile Include="Dae\Sampler.cs" />
+    <Compile Include="Dae\Source.cs" />
+    <Compile Include="Dae\TransformCollection.cs" />
+    <Compile Include="Dae\Visitor.cs" />
+    <Compile Include="ImporterDescriptorExtensions.cs" />
+    <Compile Include="Level\CameraImporter.cs" />
+    <Compile Include="Level\CharacterImporter.cs" />
+    <Compile Include="Level\Corpse.cs" />
+    <Compile Include="Level\FilmImporter.cs" />
+    <Compile Include="Level\ModelImporter.cs" />
+    <Compile Include="Level\ObjectImporter.cs" />
+    <Compile Include="Level\ParticleImporter.cs" />
+    <Compile Include="Level\PhysicsImporter.cs" />
+    <Compile Include="Level\SkyImporter.cs" />
+    <Compile Include="Level\ScriptCharacter.cs" />
+    <Compile Include="Level\TextureImporter.cs" />
+    <Compile Include="Math\FMath.cs" />
+    <Compile Include="Metadata\DumpVisitor.cs" />
+    <Compile Include="Motoko\TextureDaeWriter.cs" />
+    <Compile Include="Motoko\TextureImporter3.cs" />
+    <Compile Include="Objects\CharacterAlertStatus.cs" />
+    <Compile Include="Objects\CharacterFlags.cs" />
+    <Compile Include="Objects\CharacterJobType.cs" />
+    <Compile Include="Objects\CharacterPursuitLostBehavior.cs" />
+    <Compile Include="Objects\CharacterPursuitMode.cs" />
+    <Compile Include="Objects\CharacterTeam.cs" />
+    <Compile Include="Objects\ConsoleClassFlags.cs" />
+    <Compile Include="Objects\ConsoleFlags.cs" />
+    <Compile Include="Objects\DoorFlags.cs" />
+    <Compile Include="Objects\ObjectEventType.cs" />
+    <Compile Include="Objects\NeutralDialogLineFlags.cs" />
+    <Compile Include="Objects\NeutralFlags.cs" />
+    <Compile Include="Objects\NeutralItems.cs" />
+    <Compile Include="Objects\ObjectLoadContext.cs" />
+    <Compile Include="Objects\FurnitureClass.cs" />
+    <Compile Include="Objects\GunkObject.cs" />
+    <Compile Include="Objects\GunkObjectClass.cs" />
+    <Compile Include="Objects\ObjectClass.cs" />
+    <Compile Include="Objects\ParticleFlags.cs" />
+    <Compile Include="Objects\PatrolPathFacing.cs" />
+    <Compile Include="Objects\PatrolPathMovementMode.cs" />
+    <Compile Include="Objects\PatrolPathPointType.cs" />
+    <Compile Include="Objects\PowerUpClass.cs" />
+    <Compile Include="Objects\SoundVolumeType.cs" />
+    <Compile Include="Objects\TriggerFlags.cs" />
+    <Compile Include="Objects\TriggerVolumeFlags.cs" />
+    <Compile Include="Objects\TurretFlags.cs" />
+    <Compile Include="Objects\TurretTargetTeams.cs" />
+    <Compile Include="Physics\ObjectAnimationImporter.cs" />
+    <Compile Include="Physics\ObjectAnimationClip.cs" />
+    <Compile Include="Physics\ObjectDatReader.cs" />
+    <Compile Include="Physics\ObjectDaeImporter.cs" />
+    <Compile Include="Metadata\MetaPrimitiveType.cs" />
+    <Compile Include="Metadata\XmlReaderExtensions.cs" />
+    <Compile Include="Motoko\Quadify.cs" />
+    <Compile Include="Imaging\DdsWriter.cs" />
+    <Compile Include="Imaging\Point.cs" />
+    <Compile Include="Imaging\SysWriter.cs" />
+    <Compile Include="Imaging\TgaWriter.cs" />
+    <Compile Include="ImporterDescriptor.cs" />
+    <Compile Include="Level\LevelDatWriter.cs" />
+    <Compile Include="Level\LevelImporter.cs" />
+    <Compile Include="Motoko\TextureImporterOptions.cs" />
+    <Compile Include="Motoko\TextureUtils.cs" />
+    <Compile Include="Objects\ConsoleClass.cs" />
+    <Compile Include="Objects\DoorClass.cs" />
+    <Compile Include="Physics\ObjectAnimation.cs" />
+    <Compile Include="Physics\ObjectAnimationFlags.cs" />
+    <Compile Include="Physics\ObjectAnimationKey.cs" />
+    <Compile Include="Physics\ObjectDatWriter.cs" />
+    <Compile Include="Physics\ObjectGeometry.cs" />
+    <Compile Include="Physics\ObjectNode.cs" />
+    <Compile Include="Physics\ObjectDaeNodeProperties.cs" />
+    <Compile Include="Physics\ObjectParticle.cs" />
+    <Compile Include="Objects\ObjectType.cs" />
+    <Compile Include="Objects\NeutralDialogLine.cs" />
+    <Compile Include="Objects\ObjcDatWriter.cs" />
+    <Compile Include="Objects\Character.cs" />
+    <Compile Include="Objects\Console.cs" />
+    <Compile Include="Objects\Door.cs" />
+    <Compile Include="Objects\Flag.cs" />
+    <Compile Include="Objects\Furniture.cs" />
+    <Compile Include="Objects\ObjectFlags.cs" />
+    <Compile Include="Objects\ObjectBase.cs" />
+    <Compile Include="Objects\ObjectEvent.cs" />
+    <Compile Include="Math\Polygon2.cs" />
+    <Compile Include="Math\Polygon3.cs" />
+    <Compile Include="Metadata\BinaryTag.cs" />
+    <Compile Include="Metadata\MetaUInt32.cs" />
+    <Compile Include="Metadata\MetaUInt64.cs" />
+    <Compile Include="Metadata\SoundMetadata.cs" />
+    <Compile Include="Motoko\GeometryDaeReader.cs" />
+    <Compile Include="Motoko\GeometryDaeWriter.cs" />
+    <Compile Include="Motoko\GeometryDatReader.cs" />
+    <Compile Include="Motoko\GeometryDatWriter.cs" />
+    <Compile Include="Motoko\TextureDatReader.cs" />
+    <Compile Include="Collections\Set.cs" />
+    <Compile Include="Motoko\TextureDatWriter.cs" />
+    <Compile Include="Motoko\TextureFormat.cs" />
+    <Compile Include="Motoko\TextureXmlImporter.cs" />
+    <Compile Include="Objects\Neutral.cs" />
+    <Compile Include="Objects\Particle.cs" />
+    <Compile Include="Objects\PatrolPath.cs" />
+    <Compile Include="Objects\PatrolPathPoint.cs" />
+    <Compile Include="Objects\PowerUp.cs" />
+    <Compile Include="Objects\Sound.cs" />
+    <Compile Include="Objects\Trigger.cs" />
+    <Compile Include="Objects\TriggerClass.cs" />
+    <Compile Include="Objects\TriggerVolume.cs" />
+    <Compile Include="Objects\Turret.cs" />
+    <Compile Include="Objects\TurretClass.cs" />
+    <Compile Include="Objects\Weapon.cs" />
+    <Compile Include="Parallel.cs" />
+    <Compile Include="Physics\ObjectPhysicsType.cs" />
+    <Compile Include="Physics\ObjectSetup.cs" />
+    <Compile Include="Physics\ObjectSetupFlags.cs" />
+    <Compile Include="Physics\ObjectXmlReader.cs" />
+    <Compile Include="SceneExporter.cs" />
+    <Compile Include="Totoro\Direction.cs" />
+    <Compile Include="Totoro\AnimationFlags.cs" />
+    <Compile Include="Totoro\AnimationState.cs" />
+    <Compile Include="Totoro\AnimationType.cs" />
+    <Compile Include="Totoro\AnimationVarient.cs" />
+    <Compile Include="Totoro\AttackFlags.cs" />
+    <Compile Include="Totoro\Body.cs" />
+    <Compile Include="Dae\IO\DaeWriter.cs" />
+    <Compile Include="Totoro\Animation.cs" />
+    <Compile Include="Totoro\AnimationDaeWriter.cs" />
+    <Compile Include="Totoro\AnimationDaeReader.cs" />
+    <Compile Include="Totoro\Attack.cs" />
+    <Compile Include="Totoro\BodyDaeImporter.cs" />
+    <Compile Include="Totoro\BodyDaeReader.cs" />
+    <Compile Include="Totoro\BodyDatWriter.cs" />
+    <Compile Include="Totoro\BodyNode.cs" />
+    <Compile Include="Totoro\Bone.cs" />
+    <Compile Include="Totoro\BoneMask.cs" />
+    <Compile Include="Totoro\Damage.cs" />
+    <Compile Include="Totoro\AnimationDatReader.cs" />
+    <Compile Include="Totoro\AnimationDatWriter.cs" />
+    <Compile Include="Totoro\BodyDatReader.cs" />
+    <Compile Include="Totoro\AttackExtent.cs" />
+    <Compile Include="Totoro\Footstep.cs" />
+    <Compile Include="Totoro\BodyDaeWriter.cs" />
+    <Compile Include="Totoro\FootstepType.cs" />
+    <Compile Include="Totoro\KeyFrame.cs" />
+    <Compile Include="Totoro\MotionBlur.cs" />
+    <Compile Include="Totoro\Particle.cs" />
+    <Compile Include="Totoro\Position.cs" />
+    <Compile Include="Totoro\Shortcut.cs" />
+    <Compile Include="Totoro\Sound.cs" />
+    <Compile Include="Totoro\ThrowInfo.cs" />
+    <Compile Include="Totoro\AnimationXmlReader.cs" />
+    <Compile Include="Totoro\AnimationXmlWriter.cs" />
+    <Compile Include="BinaryReader.cs" />
+    <Compile Include="Akira\AlphaBspBuilder.cs" />
+    <Compile Include="Akira\AlphaBspNode.cs" />
+    <Compile Include="InstanceFileOperations.cs" />
+    <Compile Include="Metadata\MetaUInt16.cs" />
+    <Compile Include="DatUnpacker.cs" />
+    <Compile Include="InstanceFileManager.cs" />
+    <Compile Include="DatPacker.cs" />
+    <Compile Include="InstanceDescriptor.cs" />
+    <Compile Include="InstanceDescriptorFlags.cs" />
+    <Compile Include="InstanceFile.cs" />
+    <Compile Include="InstanceFileHeader.cs" />
+    <Compile Include="Sound\OsbdXmlExporter.cs" />
+    <Compile Include="Sound\OsbdXmlImporter.cs" />
+    <Compile Include="TemplateTag.cs" />
+    <Compile Include="Template.cs" />
+    <Compile Include="InstanceFileWriter.cs" />
+    <Compile Include="Program.cs" />
+    <Compile Include="Importer.cs" />
+    <Compile Include="Exporter.cs" />
+    <Compile Include="Utils.cs" />
+    <Compile Include="BinaryWriter.cs" />
+    <Compile Include="SubtitleExporter.cs" />
+    <Compile Include="SubtitleImporter.cs" />
+    <Compile Include="ImporterTask.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Akira\PolygonEdge.cs" />
+    <Compile Include="Akira\GunkFlags.cs" />
+    <Compile Include="Akira\Polygon.cs" />
+    <Compile Include="Akira\PolygonMesh.cs" />
+    <Compile Include="Totoro\BodySetImporter.cs" />
+    <Compile Include="Game\CharacterClass.cs" />
+    <Compile Include="Motoko\Geometry.cs" />
+    <Compile Include="DaeExporter.cs" />
+    <Compile Include="Motoko\GeometryImporter.cs" />
+    <Compile Include="Motoko\Stripify.cs" />
+    <Compile Include="Dae\EffectParameter.cs" />
+    <Compile Include="Dae\EffectTexture.cs" />
+    <Compile Include="Dae\EffectTextureChannel.cs" />
+    <Compile Include="Dae\EffectType.cs" />
+    <Compile Include="Dae\MaterialInstance.cs" />
+    <Compile Include="Dae\EffectSampler.cs" />
+    <Compile Include="Dae\EffectSurface.cs" />
+    <Compile Include="Dae\Effect.cs" />
+    <Compile Include="Dae\Entity.cs" />
+    <Compile Include="Dae\Geometry.cs" />
+    <Compile Include="Dae\Image.cs" />
+    <Compile Include="Dae\Instance.cs" />
+    <Compile Include="Dae\GeometryInstance.cs" />
+    <Compile Include="Dae\Material.cs" />
+    <Compile Include="Dae\Node.cs" />
+    <Compile Include="Dae\IO\DaeReader.cs" />
+    <Compile Include="Dae\Scene.cs" />
+    <Compile Include="Dae\Semantic.cs" />
+    <Compile Include="Dae\Transform.cs" />
+    <Compile Include="Dae\TransformMatrix.cs" />
+    <Compile Include="Dae\TransformRotate.cs" />
+    <Compile Include="Dae\TransformScale.cs" />
+    <Compile Include="Dae\TransformTranslate.cs" />
+    <Compile Include="Dae\Axis.cs" />
+    <Compile Include="Akira\OctreeNode.cs" />
+    <Compile Include="Akira\OctreeBuilder.cs" />
+    <Compile Include="Akira\QuadtreeNode.cs" />
+    <Compile Include="Imaging\Color.cs" />
+    <Compile Include="Imaging\DdsHeader.cs" />
+    <Compile Include="Imaging\Dxt1.cs" />
+    <Compile Include="Imaging\Surface.cs" />
+    <Compile Include="Imaging\SysReader.cs" />
+    <Compile Include="Motoko\TextureImporter.cs" />
+    <Compile Include="Motoko\TextureFlags.cs" />
+    <Compile Include="Imaging\TgaHeader.cs" />
+    <Compile Include="Imaging\TgaImageType.cs" />
+    <Compile Include="Motoko\Texture.cs" />
+    <Compile Include="Motoko\TextureExporter.cs" />
+    <Compile Include="Imaging\SurfaceFormat.cs" />
+    <Compile Include="Imaging\TgaReader.cs" />
+    <Compile Include="Imaging\DdsReader.cs" />
+    <Compile Include="Math\BoundingBox.cs" />
+    <Compile Include="Math\BoundingSphere.cs" />
+    <Compile Include="Math\Vector4.cs" />
+    <Compile Include="Math\MathHelper.cs" />
+    <Compile Include="Math\Plane.cs" />
+    <Compile Include="Math\Quaternion.cs" />
+    <Compile Include="Math\Vector2.cs" />
+    <Compile Include="Math\Vector3.cs" />
+    <Compile Include="Math\Matrix.cs" />
+    <Compile Include="Metadata\CompareVisitor.cs" />
+    <Compile Include="Metadata\CopyVisitor.cs" />
+    <Compile Include="Metadata\IMetaTypeVisitor.cs" />
+    <Compile Include="Metadata\LinkVisitor.cs" />
+    <Compile Include="Metadata\MetaEnum.cs" />
+    <Compile Include="Metadata\MetaTypeVisitor.cs" />
+    <Compile Include="Particles\EventAction.cs" />
+    <Compile Include="Particles\EventActionType.cs" />
+    <Compile Include="Particles\Appearance.cs" />
+    <Compile Include="Particles\Attractor.cs" />
+    <Compile Include="Particles\AttractorSelector.cs" />
+    <Compile Include="Particles\AttractorTarget.cs" />
+    <Compile Include="Particles\DisableDetailLevel.cs" />
+    <Compile Include="Particles\ImpactEffect.cs" />
+    <Compile Include="Particles\ImpactEffectComponent.cs" />
+    <Compile Include="Particles\ImpactEffectModifier.cs" />
+    <Compile Include="Particles\ImpactEffectParticle.cs" />
+    <Compile Include="Particles\ImpactEffectSound.cs" />
+    <Compile Include="Particles\Particle.cs" />
+    <Compile Include="Particles\ParticleFlags.cs" />
+    <Compile Include="Particles\Emitter.cs" />
+    <Compile Include="Particles\EmitterDirection.cs" />
+    <Compile Include="Particles\EmitterFlags.cs" />
+    <Compile Include="Particles\EmitterOrientation.cs" />
+    <Compile Include="Particles\EmitterPosition.cs" />
+    <Compile Include="Particles\EmitterRate.cs" />
+    <Compile Include="Particles\EmitterSpeed.cs" />
+    <Compile Include="Particles\Event.cs" />
+    <Compile Include="Particles\EventType.cs" />
+    <Compile Include="Particles\SpriteType.cs" />
+    <Compile Include="Particles\StorageType.cs" />
+    <Compile Include="Particles\Value.cs" />
+    <Compile Include="Particles\ValueType.cs" />
+    <Compile Include="Particles\Variable.cs" />
+    <Compile Include="Particles\VariableReference.cs" />
+    <Compile Include="Sound\AifImporter.cs" />
+    <Compile Include="Sound\WavFormat.cs" />
+    <Compile Include="Sound\WavImporter.cs" />
+    <Compile Include="Sound\AifExporter.cs" />
+    <Compile Include="Sound\SoundData.cs" />
+    <Compile Include="Sound\WavExporter.cs" />
+    <Compile Include="Sound\SoundExporter.cs" />
+    <Compile Include="Sound\AifFile.cs" />
+    <Compile Include="Sound\WavFile.cs" />
+    <Compile Include="Metadata\ObjectMetadata.cs" />
+    <Compile Include="Metadata\BinaryMetadata.cs" />
+    <Compile Include="Metadata\MetaBoundingSphere.cs" />
+    <Compile Include="Metadata\BinaryPartField.cs" />
+    <Compile Include="Metadata\OniMacMetadata.cs" />
+    <Compile Include="Metadata\MetaSepOffset.cs" />
+    <Compile Include="Metadata\OniPcMetadata.cs" />
+    <Compile Include="Metadata\InstanceMetadata.cs" />
+    <Compile Include="Metadata\MetaArray.cs" />
+    <Compile Include="Metadata\MetaBoundingBox.cs" />
+    <Compile Include="Metadata\MetaByte.cs" />
+    <Compile Include="Metadata\MetaChar.cs" />
+    <Compile Include="Metadata\MetaColor.cs" />
+    <Compile Include="Metadata\Field.cs" />
+    <Compile Include="Metadata\MetaFloat.cs" />
+    <Compile Include="Metadata\MetaPointer.cs" />
+    <Compile Include="Metadata\MetaInt16.cs" />
+    <Compile Include="Metadata\MetaInt32.cs" />
+    <Compile Include="Metadata\MetaInt64.cs" />
+    <Compile Include="Metadata\MetaMatrix4x3.cs" />
+    <Compile Include="Metadata\MetaPadding.cs" />
+    <Compile Include="Metadata\MetaPlane.cs" />
+    <Compile Include="Metadata\MetaQuaternion.cs" />
+    <Compile Include="Metadata\MetaRawOffset.cs" />
+    <Compile Include="Metadata\MetaString.cs" />
+    <Compile Include="Metadata\MetaStruct.cs" />
+    <Compile Include="Metadata\MetaType.cs" />
+    <Compile Include="Metadata\MetaVarArray.cs" />
+    <Compile Include="Metadata\MetaVector2.cs" />
+    <Compile Include="Metadata\MetaVector3.cs" />
+    <Compile Include="Sound\SabdXmlExporter.cs" />
+    <Compile Include="Sound\SabdXmlImporter.cs" />
+    <Compile Include="Xml\FilmToXmlConverter.cs" />
+    <Compile Include="Xml\GenericXmlWriter.cs" />
+    <Compile Include="Xml\OnieXmlImporter.cs" />
+    <Compile Include="Xml\OnieXmlExporter.cs" />
+    <Compile Include="Xml\TmbdXmlImporter.cs" />
+    <Compile Include="Xml\TmbdXmlExporter.cs" />
+    <Compile Include="Xml\ParticleXml.cs" />
+    <Compile Include="Xml\ParticleXmlImporter.cs" />
+    <Compile Include="Xml\ParticleXmlExporter.cs" />
+    <Compile Include="Xml\ObjcXmlImporter.cs" />
+    <Compile Include="Xml\ObjcXmlExporter.cs" />
+    <Compile Include="Xml\RawXmlExporter.cs" />
+    <Compile Include="Xml\RawXmlImporter.cs" />
+    <Compile Include="Motoko\TextureXmlExporter.cs" />
+    <Compile Include="Xml\XmlExporter.cs" />
+    <Compile Include="Xml\XmlImporter.cs" />
+    <Compile Include="Xml\XmlReaderExtensions.cs" />
+    <Compile Include="Xml\XmlWriterExtensions.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="app.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <PropertyGroup>
+    <PostBuildEvent>
+    </PostBuildEvent>
+  </PropertyGroup>
+</Project>
Index: /OniSplit/OniSplit.sln
===================================================================
--- /OniSplit/OniSplit.sln	(revision 1114)
+++ /OniSplit/OniSplit.sln	(revision 1114)
@@ -0,0 +1,22 @@
+﻿
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.24720.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OniSplit", "OniSplit.csproj", "{7337BAAA-A08B-4E98-97D6-F5B095D74BFF}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{7337BAAA-A08B-4E98-97D6-F5B095D74BFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7337BAAA-A08B-4E98-97D6-F5B095D74BFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7337BAAA-A08B-4E98-97D6-F5B095D74BFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7337BAAA-A08B-4E98-97D6-F5B095D74BFF}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal
Index: /OniSplit/Parallel.cs
===================================================================
--- /OniSplit/Parallel.cs	(revision 1114)
+++ /OniSplit/Parallel.cs	(revision 1114)
@@ -0,0 +1,61 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Oni
+{
+    internal static class Parallel
+    {
+        public static void ForEach<T>(IEnumerable<T> items, Action<T> action)
+        {
+            var array = items.ToArray();
+
+            if (array.Length == 0)
+            {
+                return;
+            }
+
+            if (array.Length == 1)
+            {
+                action(array[0]);
+                return;
+            }
+
+            int cpuCount = Environment.ProcessorCount;
+
+            if (cpuCount == 1)
+            {
+                foreach (T item in array)
+                    action(item);
+
+                return;
+            }
+
+#if NETCORE
+            var task = Task.Run(() =>
+            {
+                for (int i = array.Length / 2; i < array.Length; i++)
+                    action(array[i]);
+            });
+#else
+            var thread = new Thread(() =>
+            {
+                for (int i = array.Length / 2; i < array.Length; i++)
+                    action(array[i]);
+            });
+
+            thread.Start();
+#endif
+
+            for (int i = 0; i < array.Length / 2; i++)
+                action(array[i]);
+
+#if NETCORE
+            task.Wait();
+#else
+            thread.Join();
+#endif
+        }
+    }
+}
Index: /OniSplit/Particles/Appearance.cs
===================================================================
--- /OniSplit/Particles/Appearance.cs	(revision 1114)
+++ /OniSplit/Particles/Appearance.cs	(revision 1114)
@@ -0,0 +1,191 @@
+﻿using System;
+using Oni.Imaging;
+
+namespace Oni.Particles
+{
+    internal class Appearance
+    {
+        #region Private data
+        private Value scale;
+        private Value yScale;
+        private Value rotation;
+        private Value alpha;
+        private Value xOffset;
+        private Value xShorten;
+        private Value tint;
+        private Value edgeFadeMin;
+        private Value edgeFadeMax;
+        private Value maxContrail;
+        private Value lensFlareDistance;
+        private int fadeInFrames;
+        private int fadeOutFrames;
+        private int maxDecals;
+        private int decalFadeFrames;
+        private Value decalWrapAngle;
+        private string textureName;
+        #endregion
+
+        public Appearance()
+        {
+            scale = Value.FloatOne;
+            yScale = Value.FloatOne;
+            rotation = Value.FloatZero;
+            alpha = Value.FloatOne;
+            textureName = "notfoundtex";
+            xOffset = Value.FloatZero;
+            xShorten = Value.FloatZero;
+            tint = new Value(new Color(255, 255, 255));
+            edgeFadeMin = Value.FloatZero;
+            edgeFadeMax = Value.FloatZero;
+            maxContrail = Value.FloatZero;
+            lensFlareDistance = Value.FloatZero;
+            maxDecals = 100;
+            decalFadeFrames = 60;
+            decalWrapAngle = new Value(60.0f);
+        }
+
+        public Appearance(BinaryReader reader)
+        {
+            scale = Value.Read(reader);
+            yScale = Value.Read(reader);
+            rotation = Value.Read(reader);
+            alpha = Value.Read(reader);
+            textureName = reader.ReadString(32);
+            xOffset = Value.Read(reader);
+            xShorten = Value.Read(reader);
+            tint = Value.Read(reader);
+            edgeFadeMin = Value.Read(reader);
+            edgeFadeMax = Value.Read(reader);
+            maxContrail = Value.Read(reader);
+            lensFlareDistance = Value.Read(reader);
+            fadeInFrames = reader.ReadInt16();
+            fadeOutFrames = reader.ReadInt16();
+            maxDecals = reader.ReadInt16();
+            decalFadeFrames = reader.ReadInt16();
+            decalWrapAngle = Value.Read(reader);
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            scale.Write(writer);
+            yScale.Write(writer);
+            rotation.Write(writer);
+            alpha.Write(writer);
+            writer.Write(textureName, 32);
+            xOffset.Write(writer);
+            xShorten.Write(writer);
+            tint.Write(writer);
+            edgeFadeMin.Write(writer);
+            edgeFadeMax.Write(writer);
+            maxContrail.Write(writer);
+            lensFlareDistance.Write(writer);
+            writer.WriteInt16(fadeInFrames);
+            writer.WriteInt16(fadeOutFrames);
+            writer.WriteInt16(maxDecals);
+            writer.WriteInt16(decalFadeFrames);
+            decalWrapAngle.Write(writer);
+        }
+
+        public string TextureName
+        {
+            get { return textureName; }
+            set { textureName = value; }
+        }
+
+        public Value Scale
+        {
+            get { return scale; }
+            set { scale = value; }
+        }
+
+        public Value YScale
+        {
+            get { return yScale; }
+            set { yScale = value; }
+        }
+
+        public Value Rotation
+        {
+            get { return rotation; }
+            set { rotation = value; }
+        }
+
+        public Value Alpha
+        {
+            get { return alpha; }
+            set { alpha = value; }
+        }
+
+        public Value XOffset
+        {
+            get { return xOffset; }
+            set { xOffset = value; }
+        }
+
+        public Value XShorten
+        {
+            get { return xShorten; }
+            set { xShorten = value; }
+        }
+
+        public Value Tint
+        {
+            get { return tint; }
+            set { tint = value; }
+        }
+
+        public Value EdgeFadeMin
+        {
+            get { return edgeFadeMin; }
+            set { edgeFadeMin = value; }
+        }
+
+        public Value EdgeFadeMax
+        {
+            get { return edgeFadeMax; }
+            set { edgeFadeMax = value; }
+        }
+
+        public Value MaxContrail
+        {
+            get { return maxContrail; }
+            set { maxContrail = value; }
+        }
+
+        public Value LensFlareDistance
+        {
+            get { return lensFlareDistance; }
+            set { lensFlareDistance = value; }
+        }
+
+        public int LensFlareFadeInFrames
+        {
+            get { return fadeInFrames; }
+            set { fadeInFrames = value; }
+        }
+
+        public int LensFlareFadeOutFrames
+        {
+            get { return fadeOutFrames; }
+            set { fadeOutFrames = value; }
+        }
+
+        public int MaxDecals
+        {
+            get { return maxDecals; }
+            set { maxDecals = value; }
+        }
+
+        public int DecalFadeFrames
+        {
+            get { return decalFadeFrames; }
+            set { decalFadeFrames = value; }
+        }
+
+        public Value DecalWrapAngle
+        {
+            get { return decalWrapAngle; }
+            set { decalWrapAngle = value; }
+        }
+    }
+}
Index: /OniSplit/Particles/Attractor.cs
===================================================================
--- /OniSplit/Particles/Attractor.cs	(revision 1114)
+++ /OniSplit/Particles/Attractor.cs	(revision 1114)
@@ -0,0 +1,101 @@
+﻿namespace Oni.Particles
+{
+    internal class Attractor
+    {
+        #region Private data
+        private AttractorTarget target;
+        private AttractorSelector selector;
+        private string className;
+        private Value maxDistance;
+        private Value maxAngle;
+        private Value angleSelectMin;
+        private Value angleSelectMax;
+        private Value angleSelectWeight;
+        #endregion
+
+        public Attractor()
+        {
+            target = AttractorTarget.None;
+            selector = AttractorSelector.Distance;
+            maxDistance = new Value(150.0f);
+            maxAngle = new Value(30.0f);
+            angleSelectMin = new Value(3.0f);
+            angleSelectMax = new Value(3.0f);
+            angleSelectWeight = new Value(3.0f);
+        }
+
+        public Attractor(BinaryReader reader)
+        {
+            target = (AttractorTarget)reader.ReadInt32();
+            selector = (AttractorSelector)reader.ReadInt32();
+            reader.Skip(4);
+            className = reader.ReadString(64);
+            maxDistance = Value.Read(reader);
+            maxAngle = Value.Read(reader);
+            angleSelectMin = Value.Read(reader);
+            angleSelectMax = Value.Read(reader);
+            angleSelectWeight = Value.Read(reader);
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write((int)target);
+            writer.Write((int)selector);
+            writer.Skip(4);
+            writer.Write(className, 64);
+            maxDistance.Write(writer);
+            maxAngle.Write(writer);
+            angleSelectMin.Write(writer);
+            angleSelectMax.Write(writer);
+            angleSelectWeight.Write(writer);
+        }
+
+        public AttractorTarget Target
+        {
+            get { return target; }
+            set { target = value; }
+        }
+
+        public AttractorSelector Selector
+        {
+            get { return selector; }
+            set { selector = value; }
+        }
+
+        public string ClassName
+        {
+            get { return className; }
+            set { className = value; }
+        }
+
+        public Value MaxDistance
+        {
+            get { return maxDistance; }
+            set { maxDistance = value; }
+        }
+
+        public Value MaxAngle
+        {
+            get { return maxAngle; }
+            set { maxAngle = value; }
+        }
+
+        public Value AngleSelectMin
+        {
+            get { return angleSelectMin; }
+            set { angleSelectMin = value; }
+        }
+
+        public Value AngleSelectMax
+        {
+            get { return angleSelectMax; }
+            set { angleSelectMax = value; }
+        }
+
+        public Value AngleSelectWeight
+        {
+            get { return angleSelectWeight; }
+            set { angleSelectWeight = value; }
+        }
+    }
+}
Index: /OniSplit/Particles/AttractorSelector.cs
===================================================================
--- /OniSplit/Particles/AttractorSelector.cs	(revision 1114)
+++ /OniSplit/Particles/AttractorSelector.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Particles
+{
+    internal enum AttractorSelector
+    {
+        Distance,
+        Angle
+    }
+}
Index: /OniSplit/Particles/AttractorTarget.cs
===================================================================
--- /OniSplit/Particles/AttractorTarget.cs	(revision 1114)
+++ /OniSplit/Particles/AttractorTarget.cs	(revision 1114)
@@ -0,0 +1,15 @@
+﻿namespace Oni.Particles
+{
+    internal enum AttractorTarget
+    {
+        None,
+        Link,
+        Class,
+        Tag,
+        Characters,
+        Hostiles,
+        EmittedTowards,
+        ParentAttractor,
+        AllCharacters
+    }
+}
Index: /OniSplit/Particles/DisableDetailLevel.cs
===================================================================
--- /OniSplit/Particles/DisableDetailLevel.cs	(revision 1114)
+++ /OniSplit/Particles/DisableDetailLevel.cs	(revision 1114)
@@ -0,0 +1,9 @@
+﻿namespace Oni.Particles
+{
+    internal enum DisableDetailLevel
+    {
+        Never = 0,
+        Medium = 1,
+        Low = 3
+    }
+}
Index: /OniSplit/Particles/Emitter.cs
===================================================================
--- /OniSplit/Particles/Emitter.cs	(revision 1114)
+++ /OniSplit/Particles/Emitter.cs	(revision 1114)
@@ -0,0 +1,149 @@
+﻿using System;
+
+namespace Oni.Particles
+{
+    internal class Emitter
+    {
+        #region Private data
+        private string particleClass;
+        private EmitterFlags flags;
+        private int turnOffTreshold;
+        private int probability;
+        private float copies;
+        private int linkTo;
+        private EmitterRate rate;
+        private EmitterPosition position;
+        private EmitterDirection direction;
+        private EmitterSpeed speed;
+        private EmitterOrientation orientationDir;
+        private EmitterOrientation orientationUp;
+        private Value[] parameters;
+        #endregion
+
+        public Emitter()
+        {
+            parameters = new Value[12];
+        }
+
+        public Emitter(BinaryReader reader)
+        {
+            particleClass = reader.ReadString(64);
+            reader.Skip(4);
+            flags = (EmitterFlags)reader.ReadInt32();
+            turnOffTreshold = reader.ReadInt16();
+            probability = reader.ReadUInt16();
+            copies = reader.ReadSingle();
+            linkTo = reader.ReadInt32();
+            rate = (EmitterRate)reader.ReadInt32();
+            position = (EmitterPosition)reader.ReadInt32();
+            direction = (EmitterDirection)reader.ReadInt32();
+            speed = (EmitterSpeed)reader.ReadInt32();
+            orientationDir = (EmitterOrientation)reader.ReadInt32();
+            orientationUp = (EmitterOrientation)reader.ReadInt32();
+
+            parameters = new Value[12];
+
+            for (int i = 0; i < parameters.Length; i++)
+                parameters[i] = Value.Read(reader);
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write(particleClass, 64);
+            writer.Skip(4);
+            writer.Write((int)flags);
+            writer.WriteInt16(turnOffTreshold);
+            writer.WriteUInt16(probability);
+            writer.Write(copies);
+            writer.Write(linkTo);
+            writer.Write((int)rate);
+            writer.Write((int)position);
+            writer.Write((int)direction);
+            writer.Write((int)speed);
+            writer.Write((int)orientationDir);
+            writer.Write((int)orientationUp);
+
+            for (int i = 0; i < parameters.Length; i++)
+            {
+                if (parameters[i] != null)
+                    parameters[i].Write(writer);
+                else
+                    Value.Empty.Write(writer);
+            }
+        }
+
+        public EmitterFlags Flags
+        {
+            get { return flags; }
+            set { flags = value; }
+        }
+
+        public int Probability
+        {
+            get { return probability; }
+            set { probability = value; }
+        }
+
+        public int TurnOffTreshold
+        {
+            get { return turnOffTreshold; }
+            set { turnOffTreshold = value; }
+        }
+
+        public int LinkTo
+        {
+            get { return linkTo; }
+            set { linkTo = value; }
+        }
+
+        public string ParticleClass
+        {
+            get { return particleClass; }
+            set { particleClass = value; }
+        }
+
+        public float Copies
+        {
+            get { return copies; }
+            set { copies = value; }
+        }
+
+        public EmitterRate Rate
+        {
+            get { return rate; }
+            set { rate = value; }
+        }
+
+        public EmitterPosition Position
+        {
+            get { return position; }
+            set { position = value; }
+        }
+
+        public EmitterDirection Direction
+        {
+            get { return direction; }
+            set { direction = value; }
+        }
+
+        public EmitterSpeed Speed
+        {
+            get { return speed; }
+            set { speed = value; }
+        }
+
+        public EmitterOrientation OrientationDir
+        {
+            get { return orientationDir; }
+            set { orientationDir = value; }
+        }
+
+        public EmitterOrientation OrientationUp
+        {
+            get { return orientationUp; }
+            set { orientationUp = value; }
+        }
+
+        public Value[] Parameters => parameters;
+    }
+}
Index: /OniSplit/Particles/EmitterDirection.cs
===================================================================
--- /OniSplit/Particles/EmitterDirection.cs	(revision 1114)
+++ /OniSplit/Particles/EmitterDirection.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿namespace Oni.Particles
+{
+    internal enum EmitterDirection
+    {
+        Straight,
+        Random,
+        Cone,
+        Ring,
+        Offset,
+        Inaccurate,
+        Attractor
+    }
+}
Index: /OniSplit/Particles/EmitterFlags.cs
===================================================================
--- /OniSplit/Particles/EmitterFlags.cs	(revision 1114)
+++ /OniSplit/Particles/EmitterFlags.cs	(revision 1114)
@@ -0,0 +1,23 @@
+﻿using System;
+
+namespace Oni.Particles
+{
+    [Flags]
+    internal enum EmitterFlags
+    {
+        None = 0,
+
+        InitiallyOn = 0x0001,
+        IncreaseParticleCount = 0x0002,
+        TurnOffAtTreshold = 0x0004,
+
+        EmitWithParentVelocity = 0x0010,
+        Unknown0020 = 0x0020,
+        OrientToVelocity = 0x0040,
+        InheritTint = 0x0080,
+
+        OnePerAttractor = 0x0100,
+        AtLeastOne = 0x0200,
+        CycleAttractors = 0x0400
+    }
+}
Index: /OniSplit/Particles/EmitterOrientation.cs
===================================================================
--- /OniSplit/Particles/EmitterOrientation.cs	(revision 1114)
+++ /OniSplit/Particles/EmitterOrientation.cs	(revision 1114)
@@ -0,0 +1,22 @@
+﻿namespace Oni.Particles
+{
+    internal enum EmitterOrientation
+    {
+        LocalPosX,
+        LocalNegX,
+        LocalPosY,
+        LocalNegY,
+        LocalPosZ,
+        LocalNegZ,
+        WorldPosX,
+        WorldNegX,
+        WorldPosY,
+        WorldNegY,
+        WorldPosZ,
+        WorldNegZ,
+        Velocity,
+        ReverseVelocity,
+        TowardsEmitter,
+        AwayFromEmitter
+    }
+}
Index: /OniSplit/Particles/EmitterPosition.cs
===================================================================
--- /OniSplit/Particles/EmitterPosition.cs	(revision 1114)
+++ /OniSplit/Particles/EmitterPosition.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Particles
+{
+    internal enum EmitterPosition
+    {
+        Point,
+        Line,
+        Circle,
+        Sphere,
+        Offset,
+        Cylinder,
+        BodySurface,
+        BodyBones
+    }
+}
Index: /OniSplit/Particles/EmitterRate.cs
===================================================================
--- /OniSplit/Particles/EmitterRate.cs	(revision 1114)
+++ /OniSplit/Particles/EmitterRate.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Particles
+{
+    internal enum EmitterRate
+    {
+        Continous,
+        Random,
+        Instant,
+        Distance,
+        Attractor
+    }
+}
Index: /OniSplit/Particles/EmitterSpeed.cs
===================================================================
--- /OniSplit/Particles/EmitterSpeed.cs	(revision 1114)
+++ /OniSplit/Particles/EmitterSpeed.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Particles
+{
+    internal enum EmitterSpeed
+    {
+        Uniform,
+        Stratified
+    }
+}
Index: /OniSplit/Particles/Event.cs
===================================================================
--- /OniSplit/Particles/Event.cs	(revision 1114)
+++ /OniSplit/Particles/Event.cs	(revision 1114)
@@ -0,0 +1,28 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Particles
+{
+    internal class Event
+    {
+        private readonly EventType type;
+        private readonly List<EventAction> actions;
+
+        public Event(EventType type)
+        {
+            this.type = type;
+            this.actions = new List<EventAction>();
+        }
+
+        public Event(EventType type, EventAction[] actions, int start, int length)
+            : this(type)
+        {
+            for (int i = start; i < start + length; i++)
+                this.actions.Add(actions[i]);
+        }
+
+        public EventType Type => type;
+
+        public List<EventAction> Actions => actions;
+    }
+}
Index: /OniSplit/Particles/EventAction.cs
===================================================================
--- /OniSplit/Particles/EventAction.cs	(revision 1114)
+++ /OniSplit/Particles/EventAction.cs	(revision 1114)
@@ -0,0 +1,71 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Particles
+{
+    internal class EventAction
+    {
+        private readonly List<Value> parameters;
+        private readonly List<VariableReference> variables;
+        private readonly EventActionType type;
+
+        private EventAction()
+        {
+            this.parameters = new List<Value>();
+            this.variables = new List<VariableReference>();
+        }
+
+        public EventAction(EventActionType type)
+            : this()
+        {
+            this.type = type;
+        }
+
+        public EventAction(BinaryReader reader)
+            : this()
+        {
+            type = (EventActionType)reader.ReadInt32();
+            reader.ReadInt32();
+
+            for (int i = 0; i < 8; i++)
+            {
+                VariableReference value = new VariableReference(reader);
+
+                if (value.IsDefined)
+                    variables.Add(value);
+            }
+
+            for (int i = 0; i < 8; i++)
+            {
+                Value value = Value.Read(reader);
+
+                if (value != null)
+                    parameters.Add(value);
+            }
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write((int)type);
+            writer.Write(0);
+
+            foreach (VariableReference variable in variables)
+                variable.Write(writer);
+
+            for (int i = variables.Count; i < 8; i++)
+                VariableReference.Empty.Write(writer);
+
+            foreach (Value value in parameters)
+                value.Write(writer);
+
+            for (int i = parameters.Count; i < 8; i++)
+                Value.Empty.Write(writer);
+        }
+
+        public EventActionType Type => type;
+
+        public List<Value> Parameters => parameters;
+
+        public List<VariableReference> Variables => variables;
+    }
+}
Index: /OniSplit/Particles/EventActionType.cs
===================================================================
--- /OniSplit/Particles/EventActionType.cs	(revision 1114)
+++ /OniSplit/Particles/EventActionType.cs	(revision 1114)
@@ -0,0 +1,75 @@
+﻿namespace Oni.Particles
+{
+    internal enum EventActionType
+    {
+        AnimateLinear = 0,
+        AnimateAccelerated,
+        AnimateRandom,
+        AnimatePingPong,
+        AnimateLoop,
+        AnimateToValue,
+        ColorInterpolate,
+        FadeOut = 8,
+        EnableAtTime,
+        DisableAtTime,
+        Die,
+        SetLifetime,
+        EmitActivate,
+        EmitDeactivate,
+        EmitParticles,
+        ChangeClass,
+        KillLastEmitted,
+        ExplodeLastEmitted,
+        AmbientSound = 20,
+        EndAmbientSound,
+        XXSoundVolume,
+        ImpulseSound,
+        DamageChar = 26,
+        DamageBlast,
+        Explode,
+        DamageEnvironment,
+        GlassCharge,
+        Stop,
+        RotateX = 33,
+        RotateY,
+        RotateZ,
+        FindAttractor = 37,
+        AttractGravity,
+        AttractHoming,
+        AttractSpring,
+        MoveLine = 47,
+        MoveGravity,
+        MoveSpiral,
+        MoveResistance,
+        MoveDrift,
+        SetVelocity,
+        SpiralTangent,
+        KillBeyondPoint,
+        CollisionEffect,
+        StickToWall,
+        Bounce,
+        XXXCreateDecal,
+        Chop,
+        ImpactEffect,
+        Show = 62,
+        Hide,
+        SetTextureTick,
+        RandomTextureFrame,
+        SetVariable = 70,
+        RecalculateAll,
+        EnableAbove,
+        EnableBelow,
+        EnableNow,
+        DisableNow,
+        SuperBallTrigger = 77,
+        StopIfBreakable,
+        AvoidWalls,
+        RandomSwirl,
+        FloatAbovePlayer,
+        StopIfSlow,
+        SuperParticle,
+        StopLink,
+        CheckLink,
+        BreakLink
+    }
+}
Index: /OniSplit/Particles/EventType.cs
===================================================================
--- /OniSplit/Particles/EventType.cs	(revision 1114)
+++ /OniSplit/Particles/EventType.cs	(revision 1114)
@@ -0,0 +1,22 @@
+﻿namespace Oni.Particles
+{
+    internal enum EventType
+    {
+        Update,
+        Pulse,
+        Start,
+        Stop,
+        BackgroundFxStart,
+        BackgroundFxStop,
+        HitWall,
+        HitCharacter,
+        Lifetime,
+        Explode,
+        BrokenLink,
+        Create,
+        Die,
+        NewAttractor,
+        DelayStart,
+        DelayStop
+    }
+}
Index: /OniSplit/Particles/ImpactEffect.cs
===================================================================
--- /OniSplit/Particles/ImpactEffect.cs	(revision 1114)
+++ /OniSplit/Particles/ImpactEffect.cs	(revision 1114)
@@ -0,0 +1,177 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Particles
+{
+    internal class ImpactEffect
+    {
+        #region Private data
+        private string impactName;
+        private int impactIndex;
+        private string materialName;
+        private int materialIndex;
+        private ImpactEffectComponent component;
+        private ImpactEffectModifier modifier;
+        private ImpactEffectParticle[] particles;
+        private int particleIndex;
+        private ImpactEffectSound sound;
+        private int soundIndex;
+        #endregion
+
+        public ImpactEffect(BinaryReader reader, string[] impacts, string[] materials, ImpactEffectParticle[] particles, ImpactEffectSound[] sounds)
+        {
+            impactIndex = reader.ReadInt16();
+            impactName = impacts[impactIndex];
+            materialIndex = reader.ReadInt16();
+            materialName = materials[materialIndex];
+            component = (ImpactEffectComponent)reader.ReadInt16();
+            modifier = (ImpactEffectModifier)reader.ReadInt16();
+            int particleCount = reader.ReadInt16();
+            reader.Skip(2);
+            soundIndex = reader.ReadInt32();
+            particleIndex = reader.ReadInt32();
+
+            if (soundIndex != -1)
+                sound = sounds[soundIndex];
+
+            if (particleCount > 0)
+            {
+                this.particles = new ImpactEffectParticle[particleCount];
+                Array.Copy(particles, particleIndex, this.particles, 0, this.particles.Length);
+            }
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.WriteInt16(impactIndex);
+            writer.WriteInt16(materialIndex);
+            writer.WriteInt16((short)component);
+            writer.WriteInt16((short)modifier);
+            writer.WriteInt16(particles.Length);
+            writer.Skip(2);
+            writer.Write(soundIndex);
+            writer.Write(particleIndex);
+        }
+
+        public ImpactEffect(XmlReader xml, string impact, string material)
+        {
+            impactName = impact;
+            materialName = material;
+            component = MetaEnum.Parse<ImpactEffectComponent>(xml.ReadElementContentAsString("Component", ""));
+            modifier = MetaEnum.Parse<ImpactEffectModifier>(xml.ReadElementContentAsString("Modifier", ""));
+
+            if (xml.IsStartElement("Sound"))
+            {
+                if (xml.IsEmptyElement)
+                {
+                    xml.Skip();
+                }
+                else
+                {
+                    xml.ReadStartElement();
+                    sound = new ImpactEffectSound(xml);
+                    xml.ReadEndElement();
+                }
+            }
+
+            var list = new List<ImpactEffectParticle>();
+
+            if (xml.IsStartElement("Particles"))
+            {
+                if (xml.IsEmptyElement)
+                {
+                    xml.Skip();
+                }
+                else
+                {
+                    xml.ReadStartElement();
+
+                    while (xml.IsStartElement("Particle"))
+                    {
+                        xml.ReadStartElement();
+                        list.Add(new ImpactEffectParticle(xml));
+                        xml.ReadEndElement();
+                    }
+
+                    xml.ReadEndElement();
+                }
+            }
+
+            particles = list.ToArray();
+        }
+
+        public void Write(XmlWriter writer)
+        {
+            writer.WriteElementString("Component", component.ToString());
+            writer.WriteElementString("Modifier", modifier.ToString());
+
+            writer.WriteStartElement("Sound");
+
+            if (sound != null)
+                sound.Write(writer);
+
+            writer.WriteEndElement();
+
+            writer.WriteStartElement("Particles");
+
+            if (particles != null)
+            {
+                foreach (ImpactEffectParticle particle in particles)
+                {
+                    writer.WriteStartElement("Particle");
+                    particle.Write(writer);
+                    writer.WriteEndElement();
+                }
+            }
+
+            writer.WriteEndElement();
+        }
+
+        public string ImpactName => impactName;
+
+        public int ImpactIndex
+        {
+            get { return impactIndex; }
+            set { impactIndex = value; }
+        }
+
+        public string MaterialName => materialName;
+
+        public int MaterialIndex
+        {
+            get { return materialIndex; }
+            set { materialIndex = value; }
+        }
+
+        public ImpactEffectComponent Component
+        {
+            get { return component; }
+        }
+
+        public ImpactEffectModifier Modifier
+        {
+            get { return modifier; }
+        }
+
+        public ImpactEffectSound Sound
+        {
+            get { return sound; }
+        }
+
+        public int SoundIndex
+        {
+            get { return soundIndex; }
+            set { soundIndex = value; }
+        }
+
+        public int ParticleIndex
+        {
+            get { return particleIndex; }
+            set { particleIndex = value; }
+        }
+
+        public ImpactEffectParticle[] Particles => particles;
+    }
+}
Index: /OniSplit/Particles/ImpactEffectComponent.cs
===================================================================
--- /OniSplit/Particles/ImpactEffectComponent.cs	(revision 1114)
+++ /OniSplit/Particles/ImpactEffectComponent.cs	(revision 1114)
@@ -0,0 +1,9 @@
+﻿namespace Oni.Particles
+{
+    internal enum ImpactEffectComponent
+    {
+        Impact,
+        Damage,
+        Projectile
+    }
+}
Index: /OniSplit/Particles/ImpactEffectModifier.cs
===================================================================
--- /OniSplit/Particles/ImpactEffectModifier.cs	(revision 1114)
+++ /OniSplit/Particles/ImpactEffectModifier.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿namespace Oni.Particles
+{
+    internal enum ImpactEffectModifier
+    {
+        Any,
+        Heavy,
+        Medium,
+        Light
+    }
+}
Index: /OniSplit/Particles/ImpactEffectParticle.cs
===================================================================
--- /OniSplit/Particles/ImpactEffectParticle.cs	(revision 1114)
+++ /OniSplit/Particles/ImpactEffectParticle.cs	(revision 1114)
@@ -0,0 +1,114 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+
+namespace Oni.Particles
+{
+    internal class ImpactEffectParticle
+    {
+        #region Private data
+        private string particleClassName;
+        private int orientation;
+        private int location;
+        private float offset;
+        private bool decal1;
+        private bool decal2;
+        #endregion
+
+        public ImpactEffectParticle(BinaryReader reader)
+        {
+            particleClassName = reader.ReadString(64);
+            reader.Skip(4);
+            orientation = reader.ReadInt32();
+            location = reader.ReadInt32();
+
+            switch (location)
+            {
+                case 1:
+                    offset = reader.ReadSingle();
+                    reader.Skip(4);
+                    break;
+                case 4:
+                    decal1 = (reader.ReadByte() != 0);
+                    decal2 = (reader.ReadByte() != 0);
+                    reader.Skip(6);
+                    break;
+                default:
+                    reader.Skip(8);
+                    break;
+            }
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write(particleClassName, 64);
+            writer.Skip(4);
+            writer.Write(orientation);
+            writer.Write(location);
+
+            switch (location)
+            {
+                case 1:
+                    writer.Write(offset);
+                    break;
+
+                case 4:
+                    writer.WriteByte(decal1 ? 1 : 0);
+                    writer.WriteByte(decal2 ? 1 : 0);
+                    writer.WriteUInt16(0);
+                    break;
+
+                default:
+                    writer.Write(0);
+                    break;
+            }
+
+            writer.Write(0);
+        }
+
+        public ImpactEffectParticle(XmlReader xml)
+        {
+            particleClassName = xml.ReadElementContentAsString("Name", "");
+            orientation = XmlConvert.ToInt32(xml.ReadElementContentAsString("Orientation", ""));
+            location = XmlConvert.ToInt32(xml.ReadElementContentAsString("Location", ""));
+
+            switch (location)
+            {
+                case 1:
+                    offset = XmlConvert.ToSingle(xml.ReadElementContentAsString("Offset", ""));
+                    break;
+                case 4:
+                    decal1 = bool.Parse(xml.ReadElementContentAsString("Decal1", ""));
+                    decal2 = bool.Parse(xml.ReadElementContentAsString("Decal2", ""));
+                    break;
+            }
+        }
+
+        public void Write(XmlWriter writer)
+        {
+            writer.WriteElementString("Name", particleClassName);
+            writer.WriteElementString("Orientation", XmlConvert.ToString(orientation));
+            writer.WriteElementString("Location", XmlConvert.ToString(location));
+
+            switch (location)
+            {
+                case 1:
+                    writer.WriteElementString("Offset", XmlConvert.ToString(offset));
+                    break;
+                case 4:
+                    writer.WriteElementString("Decal1", XmlConvert.ToString(decal1));
+                    writer.WriteElementString("Decal2", XmlConvert.ToString(decal2));
+                    break;
+            }
+        }
+
+        public string ParticleClassName => particleClassName;
+
+        public int Orientation => orientation;
+        public int Location => location;
+        public float Offset => offset;
+
+        public bool Decal1 => decal1;
+        public bool Decal2 => decal2;
+    }
+}
Index: /OniSplit/Particles/ImpactEffectSound.cs
===================================================================
--- /OniSplit/Particles/ImpactEffectSound.cs	(revision 1114)
+++ /OniSplit/Particles/ImpactEffectSound.cs	(revision 1114)
@@ -0,0 +1,53 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Particles
+{
+    internal class ImpactEffectSound
+    {
+        private string soundName;
+        private bool aiCanHear;
+        private InstanceMetadata.DOORSoundType aiSoundType;
+        private float aiSoundRadius;
+
+        public ImpactEffectSound(BinaryReader reader)
+        {
+            soundName = reader.ReadString(32);
+            reader.Skip(8);
+            aiCanHear = (reader.ReadInt16() != 0);
+            aiSoundType = (InstanceMetadata.DOORSoundType)reader.ReadInt16();
+            aiSoundRadius = reader.ReadSingle();
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write(soundName, 32);
+            writer.Skip(8);
+            writer.WriteInt16(aiCanHear ? 1 : 0);
+            writer.WriteInt16((short)aiSoundType);
+            writer.Write(aiSoundRadius);
+        }
+
+        public ImpactEffectSound(XmlReader xml)
+        {
+            soundName = xml.ReadElementContentAsString("Name", "");
+            aiCanHear = bool.Parse(xml.ReadElementContentAsString("AICanHear", ""));
+            aiSoundType = MetaEnum.Parse<InstanceMetadata.DOORSoundType>(xml.ReadElementContentAsString("AISoundType", ""));
+            aiSoundRadius = XmlConvert.ToSingle(xml.ReadElementContentAsString("AISoundRadius", ""));
+        }
+
+        public void Write(XmlWriter writer)
+        {
+            writer.WriteElementString("Name", soundName);
+            writer.WriteElementString("AICanHear", XmlConvert.ToString(aiCanHear));
+            writer.WriteElementString("AISoundType", aiSoundType.ToString());
+            writer.WriteElementString("AISoundRadius", XmlConvert.ToString(aiSoundRadius));
+        }
+
+        public string SoundName => soundName;
+        public bool AICanHear => aiCanHear;
+        public InstanceMetadata.DOORSoundType AISoundType => aiSoundType;
+        public float AISoundRadius => aiSoundRadius;
+    }
+}
Index: /OniSplit/Particles/Particle.cs
===================================================================
--- /OniSplit/Particles/Particle.cs	(revision 1114)
+++ /OniSplit/Particles/Particle.cs	(revision 1114)
@@ -0,0 +1,286 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Particles
+{
+    internal class Particle
+    {
+        #region Private data
+        private ParticleFlags1 flags1;
+        private ParticleFlags2 flags2;
+        private SpriteType spriteType;
+        private DisableDetailLevel disableDetailLevel;
+        private Value lifetime;
+        private Value collisionRadius;
+        private float aiDodgeRadius;
+        private float aiAlertRadius;
+        private string flybySoundName;
+        private Appearance appearance;
+        private Attractor attractor;
+        private List<Variable> variables;
+        private List<Emitter> emitters;
+        private List<Event> events;
+        #endregion
+
+        #region private struct ActionRange
+
+        private struct ActionRange
+        {
+            internal ActionRange(BinaryReader reader)
+            {
+                First = reader.ReadUInt16();
+                Last = reader.ReadUInt16();
+            }
+
+            public bool IsEmpty => First == Last;
+
+            public int First { get; set; }
+            public int Last { get; set; }
+        }
+
+        #endregion
+
+        public Particle()
+        {
+            lifetime = Value.FloatZero;
+            collisionRadius = Value.FloatZero;
+            appearance = new Appearance();
+            attractor = new Attractor();
+            variables = new List<Variable>();
+            emitters = new List<Emitter>();
+            events = new List<Event>();
+        }
+
+        public Particle(BinaryReader reader)
+            : this()
+        {
+            appearance = new Appearance();
+            attractor = new Attractor();
+
+            reader.Skip(8);
+            flags1 = (ParticleFlags1)reader.ReadInt32();
+            flags2 = (ParticleFlags2)reader.ReadInt32();
+            spriteType = (SpriteType)((int)(flags1 & ParticleFlags1.SpriteModeMask) >> 5);
+            disableDetailLevel = (DisableDetailLevel)((int)(flags2 & ParticleFlags2.DisableLevelMask) >> 5);
+            reader.Skip(4);
+
+            int variableCount = reader.ReadUInt16();
+            int actionCount = reader.ReadUInt16();
+            int emitterCount = reader.ReadUInt16();
+            reader.Skip(2);
+
+            variables = new List<Variable>(variableCount);
+            emitters = new List<Emitter>(actionCount);
+            events = new List<Event>(emitterCount);
+
+            ActionRange[] eventActions = new ActionRange[16];
+
+            for (int i = 0; i < 16; i++)
+                eventActions[i] = new ActionRange(reader);
+
+            lifetime = Value.Read(reader);
+            collisionRadius = Value.Read(reader);
+            aiDodgeRadius = reader.ReadSingle();
+            aiAlertRadius = reader.ReadSingle();
+            flybySoundName = reader.ReadString(16);
+            appearance = new Appearance(reader);
+            attractor = new Attractor(reader);
+            reader.Skip(12);
+
+            for (int i = 0; i < variableCount; i++)
+                variables.Add(new Variable(reader));
+
+            EventAction[] actions = new EventAction[actionCount];
+
+            for (int i = 0; i < actionCount; i++)
+                actions[i] = new EventAction(reader);
+
+            try
+            {
+                for (int i = 0; i < emitterCount; i++)
+                    emitters.Add(new Emitter(reader));
+            }
+            catch (System.IO.EndOfStreamException)
+            {
+                // ignore corrupted particles that contain an invalid emitter count field
+            }
+
+            for (int i = 0; i < eventActions.Length; i++)
+            {
+                ActionRange range = eventActions[i];
+
+                if (!range.IsEmpty)
+                    events.Add(new Event((EventType)i, actions, range.First, range.Last - range.First));
+            }
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            List<EventAction> actions = new List<EventAction>();
+            ActionRange[] ranges = new ActionRange[16];
+
+            for (int i = 0; i < ranges.Length; i++)
+            {
+                Event e = events.Find(x => (int)x.Type == i);
+
+                ActionRange range = new ActionRange();
+                range.First = actions.Count;
+                range.Last = actions.Count + (e == null ? 0 : e.Actions.Count);
+                ranges[i] = range;
+
+                if (e != null)
+                    actions.AddRange(e.Actions);
+            }
+
+            writer.Write((int)flags1 | ((int)spriteType << 5));
+            writer.Write((int)flags2 | ((int)disableDetailLevel << 5));
+            writer.Skip(4);
+            writer.WriteUInt16(variables.Count);
+            writer.WriteUInt16(actions.Count);
+            writer.WriteUInt16(emitters.Count);
+            writer.WriteUInt16(256);
+
+            for (int i = 0; i < ranges.Length; i++)
+            {
+                writer.WriteUInt16(ranges[i].First);
+                writer.WriteUInt16(ranges[i].Last);
+            }
+
+            lifetime.Write(writer);
+            collisionRadius.Write(writer);
+            writer.Write(aiDodgeRadius);
+            writer.Write(aiAlertRadius);
+            writer.Write(flybySoundName, 16);
+            appearance.Write(writer);
+            attractor.Write(writer);
+            writer.Skip(12);
+
+            foreach (Variable variable in variables)
+                variable.Write(writer);
+
+            foreach (EventAction action in actions)
+                action.Write(writer);
+
+            foreach (Emitter emitter in emitters)
+                emitter.Write(writer);
+        }
+
+        public ParticleFlags1 Flags1
+        {
+            get
+            {
+                return (flags1 & ~ParticleFlags1.SpriteModeMask);
+            }
+            set
+            {
+                flags1 = value;
+            }
+        }
+
+        public ParticleFlags2 Flags2
+        {
+            get
+            {
+                return (flags2 & ~ParticleFlags2.DisableLevelMask);
+            }
+            set
+            {
+                flags2 = value;
+            }
+        }
+
+        public SpriteType SpriteType
+        {
+            get
+            {
+                return spriteType;
+            }
+            set
+            {
+                spriteType = value;
+            }
+        }
+
+        public DisableDetailLevel DisableDetailLevel
+        {
+            get
+            {
+                return disableDetailLevel;
+            }
+            set
+            {
+                disableDetailLevel = value;
+            }
+        }
+
+        public string FlyBySoundName
+        {
+            get
+            {
+                return flybySoundName;
+            }
+            set
+            {
+                flybySoundName = value;
+            }
+        }
+
+        public Value Lifetime
+        {
+            get
+            {
+                return lifetime;
+            }
+            set
+            {
+                lifetime = value;
+            }
+        }
+
+        public Value CollisionRadius
+        {
+            get
+            {
+                return collisionRadius;
+            }
+            set
+            {
+                collisionRadius = value;
+            }
+        }
+
+        public float AIDodgeRadius
+        {
+            get
+            {
+                return aiDodgeRadius;
+            }
+            set
+            {
+                aiDodgeRadius = value;
+            }
+        }
+
+        public float AIAlertRadius
+        {
+            get
+            {
+                return aiAlertRadius;
+            }
+            set
+            {
+                aiAlertRadius = value;
+            }
+        }
+
+        public Appearance Appearance => appearance;
+
+        public Attractor Attractor => attractor;
+
+        public List<Variable> Variables => variables;
+
+        public List<Emitter> Emitters => emitters;
+
+        public List<Event> Events => events;
+    }
+}
Index: /OniSplit/Particles/ParticleFlags.cs
===================================================================
--- /OniSplit/Particles/ParticleFlags.cs	(revision 1114)
+++ /OniSplit/Particles/ParticleFlags.cs	(revision 1114)
@@ -0,0 +1,79 @@
+﻿using System;
+
+namespace Oni.Particles
+{
+    [Flags]
+    internal enum ParticleFlags1
+    {
+        None = 0,
+
+        Decorative = 0x00000001,
+        UseSeparateYScale = 0x00000008,
+
+        SpriteMode0 = 0x00000020,
+        SpriteMode1 = 0x00000040,
+        SpriteMode2 = 0x00000080,
+
+        Geometry = 0x00000100,
+        CollideWithWalls = 0x00000200,
+        CollideWithChars = 0x00000400,
+        ScaleToVelocity = 0x00000800,
+
+        HasVelocity = 0x00001000, // 0x0c, vector3
+        HasOrientation = 0x00002000, // 0x24, matrix3x3
+        HasPositionOffset = 0x00004000, // 0x0c, vector3
+        HasAttachmentMatrix = 0x00008000, // 0x04, pointer to object transform matrix
+
+        HasUnknown = 0x00010000, // 0x10, 
+        HasDecalState = 0x00020000, // 0x14
+        HasTextureStartTick = 0x00040000, // 0x04
+        HasTextureTick = 0x00080000, // 0x04
+
+        HasDamageOwner = 0x00100000, // 0x04,
+        HasContrailData = 0x00200000, // 0x20,
+        HasLensFlareState = 0x00400000, // 0x04,
+        HasAttractor = 0x00800000, // 0x08,
+
+        HasCollisionCache = 0x01000000, // 0x14,
+
+        /// <summary>
+        /// SpriteModeMask
+        /// </summary>
+
+        SpriteModeMask = 0x000000e0,
+    }
+
+    [Flags]
+    internal enum ParticleFlags2 : uint
+    {
+        None = 0,
+
+        UseSpecialTint = 0x00000001,
+        DontAttractThroughWalls = 0x00000002,
+        ExpireOnCutscene = 0x00000008,
+
+        DieOnCutscene = 0x00000010,
+        DisableLevel0 = 0x00000020,
+        DisableLevel1 = 0x00000040,
+
+        DrawAsSky = 0x00100000,
+        DecalFullBrightness = 0x00200000,
+        Decal = 0x00800000,
+
+        InitiallyHidden = 0x01000000,
+        Invisible = 0x02000000,
+        FadeOutOnEdge = 0x04000000,
+        Vector = 0x08000000,
+
+        LockPositionToLink = 0x10000000,
+        IsContrailEmitter = 0x20000000,
+        LensFlare = 0x40000000,
+        OneSidedEdgeFade = 0x80000000,
+
+        /// <summary>
+        /// DisableLevelMask
+        /// </summary>
+
+        DisableLevelMask = 0x00000060
+    }
+}
Index: /OniSplit/Particles/SpriteType.cs
===================================================================
--- /OniSplit/Particles/SpriteType.cs	(revision 1114)
+++ /OniSplit/Particles/SpriteType.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿namespace Oni.Particles
+{
+    internal enum SpriteType
+    {
+        Sprite,
+        RotatedSprite,
+        Beam,
+        Arrow,
+        Flat,
+        OrientedContrail,
+        Contrail,
+        Discus
+    }
+}
Index: /OniSplit/Particles/StorageType.cs
===================================================================
--- /OniSplit/Particles/StorageType.cs	(revision 1114)
+++ /OniSplit/Particles/StorageType.cs	(revision 1114)
@@ -0,0 +1,30 @@
+﻿namespace Oni.Particles
+{
+    internal enum StorageType
+    {
+        // never used
+        //Int16 = 1,		
+        //AmbientSound = 4208,
+        //ImpulseSound = 4224,
+
+        // can be used as variables
+        Float = 2,
+        Color = 8,
+        PingPongState = 4096,
+
+        // action parameters only
+        ActionIndex = 4112,
+        Emitter = 4128,
+        BlastFalloff = 4144,
+        CoordFrame = 4160,
+        CollisionOrient = 4176,
+        Boolean = 4192,
+        ImpactModifier = 4240,
+        DamageType = 4256,
+        Direction = 4272,
+
+        ImpactName = 4,
+        AmbientSoundName = 8192,
+        ImpulseSoundName = 16384
+    }
+}
Index: /OniSplit/Particles/Value.cs
===================================================================
--- /OniSplit/Particles/Value.cs	(revision 1114)
+++ /OniSplit/Particles/Value.cs	(revision 1114)
@@ -0,0 +1,155 @@
+﻿using System;
+using Oni.Imaging;
+
+namespace Oni.Particles
+{
+    internal class Value
+    {
+        public const int ByteSize = 28;
+        public static readonly Value Empty = new Value(ValueType.Variable, string.Empty);
+        public static readonly Value FloatZero = new Value(0.0f);
+        public static readonly Value FloatOne = new Value(1.0f);
+
+        #region Private data
+        private ValueType type;
+        private string name;
+        private float f1, f2;
+        private Color c1, c2;
+        private int i;
+        #endregion
+
+        public Value(ValueType type, float value1, float value2)
+        {
+            this.type = type;
+            this.f1 = value1;
+            this.f2 = value2;
+        }
+
+        public Value(float value)
+            : this(ValueType.Float, value, 0.0f)
+        {
+        }
+
+        public Value(int value)
+        {
+            this.type = ValueType.Int32;
+            this.i = value;
+        }
+
+        public Value(ValueType type, string name)
+        {
+            this.type = type;
+            this.name = name;
+        }
+
+        public Value(Color color)
+            : this(ValueType.Color, color, Color.Black)
+        {
+        }
+
+        public Value(ValueType type, Color color1, Color color2)
+        {
+            this.type = type;
+            this.c1 = color1;
+            this.c2 = color2;
+        }
+
+        public static Value Read(BinaryReader reader)
+        {
+            int startPosition = (int)reader.Position;
+
+            ValueType type = (ValueType)reader.ReadInt32();
+            Value result = null;
+
+            switch (type)
+            {
+                case ValueType.Variable:
+                    string name = reader.ReadString(16);
+                    if (!string.IsNullOrEmpty(name))
+                        result = new Value(type, name);
+                    break;
+
+                case ValueType.InstanceName:
+                    result = new Value(type, reader.ReadString(16));
+                    break;
+
+                case ValueType.Float:
+                    result = new Value(reader.ReadSingle());
+                    break;
+
+                case ValueType.FloatRandom:
+                case ValueType.FloatBellCurve:
+                case ValueType.TimeCycle:
+                    result = new Value(type, reader.ReadSingle(), reader.ReadSingle());
+                    break;
+
+                case ValueType.Color:
+                    result = new Value(reader.ReadColor());
+                    break;
+
+                case ValueType.ColorRandom:
+                case ValueType.ColorBellCurve:
+                    result = new Value(type, reader.ReadColor(), reader.ReadColor());
+                    break;
+
+                case ValueType.Int32:
+                    result = new Value(reader.ReadInt32());
+                    break;
+            }
+
+            reader.Position = startPosition + ByteSize;
+
+            return result;
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            int startPosition = (int)writer.Position;
+
+            writer.Write((int)type);
+
+            switch (type)
+            {
+                case ValueType.Variable:
+                case ValueType.InstanceName:
+                    writer.Write(name, 16);
+                    break;
+
+                case ValueType.Float:
+                    writer.Write(f1);
+                    break;
+
+                case ValueType.FloatRandom:
+                case ValueType.FloatBellCurve:
+                case ValueType.TimeCycle:
+                    writer.Write(f1);
+                    writer.Write(f2);
+                    break;
+
+                case ValueType.Color:
+                    writer.Write(c1);
+                    break;
+
+                case ValueType.ColorRandom:
+                case ValueType.ColorBellCurve:
+                    writer.Write(c1);
+                    writer.Write(c2);
+                    break;
+
+                case ValueType.Int32:
+                    writer.Write(i);
+                    break;
+            }
+
+            writer.Position = startPosition + ByteSize;
+        }
+
+        public ValueType Type => type;
+        public string Name => name;
+        public float Float1 => f1;
+        public float Float2 => f2;
+        public Color Color1 => c1;
+        public Color Color2 => c2;
+        public int Int => i;
+    }
+}
Index: /OniSplit/Particles/ValueType.cs
===================================================================
--- /OniSplit/Particles/ValueType.cs	(revision 1114)
+++ /OniSplit/Particles/ValueType.cs	(revision 1114)
@@ -0,0 +1,23 @@
+﻿namespace Oni.Particles
+{
+    internal enum ValueType
+    {
+        Variable,
+
+        Int16,          // not supported
+        Int16Random,    // not supported
+
+        Float,
+        FloatRandom,
+        FloatBellCurve,
+
+        InstanceName,
+
+        Color,
+        ColorRandom,
+        ColorBellCurve,
+
+        Int32,
+        TimeCycle
+    }
+}
Index: /OniSplit/Particles/Variable.cs
===================================================================
--- /OniSplit/Particles/Variable.cs	(revision 1114)
+++ /OniSplit/Particles/Variable.cs	(revision 1114)
@@ -0,0 +1,41 @@
+﻿namespace Oni.Particles
+{
+    internal class Variable
+    {
+        private string name;
+        private StorageType storageType;
+        private Value value;
+        private int storageOffset;
+
+        public Variable(string name, StorageType type, Value value)
+        {
+            this.name = name;
+            this.storageType = type;
+            this.value = value;
+        }
+
+        public Variable(BinaryReader reader)
+        {
+            name = reader.ReadString(16);
+            storageType = (StorageType)reader.ReadInt32();
+            storageOffset = reader.ReadInt32();
+            value = Value.Read(reader);
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write(name, 16);
+            writer.Write((int)storageType);
+            writer.Write(0);
+            value.Write(writer);
+        }
+
+        public string Name => name;
+
+        public StorageType StorageType => storageType;
+
+        public int StorageOffset => storageOffset;
+
+        public Value Value => value;
+    }
+}
Index: /OniSplit/Particles/VariableReference.cs
===================================================================
--- /OniSplit/Particles/VariableReference.cs	(revision 1114)
+++ /OniSplit/Particles/VariableReference.cs	(revision 1114)
@@ -0,0 +1,31 @@
+﻿namespace Oni.Particles
+{
+    internal class VariableReference
+    {
+        public const int ByteSize = 24;
+        public static readonly VariableReference Empty = new VariableReference(string.Empty);
+
+        private string name;
+
+        public VariableReference(string name)
+        {
+            this.name = name;
+        }
+
+        public VariableReference(BinaryReader reader)
+        {
+            name = reader.ReadString(16);
+            reader.Skip(8);
+        }
+
+        public void Write(BinaryWriter writer)
+        {
+            writer.Write(name, 16);
+            writer.Skip(8);
+        }
+
+        public bool IsDefined => !string.IsNullOrEmpty(name);
+
+        public string Name => name;
+    }
+}
Index: /OniSplit/Physics/ObjectAnimation.cs
===================================================================
--- /OniSplit/Physics/ObjectAnimation.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectAnimation.cs	(revision 1114)
@@ -0,0 +1,43 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Physics
+{
+    internal class ObjectAnimation
+    {
+        public string Name;
+        public ObjectAnimationFlags Flags;
+        public int Length;
+        public int Stop;
+        public ObjectAnimationKey[] Keys;
+
+        public List<ObjectAnimationKey> Interpolate()
+        {
+            var result = new List<ObjectAnimationKey>(Length);
+
+            for (int i = 1; i < Keys.Length; i++)
+            {
+                var key0 = Keys[i - 1];
+                var key1 = Keys[i];
+
+                result.Add(key0);
+
+                for (int j = key0.Time + 1; j < key1.Time; j++)
+                {
+                    float k = (float)(j - key0.Time) / (key1.Time - key0.Time);
+
+                    result.Add(new ObjectAnimationKey {
+                        Time = j,
+                        Translation = Vector3.Lerp(key0.Translation, key1.Translation, k),
+                        Rotation = Quaternion.Lerp(key0.Rotation, key1.Rotation, k),
+                        Scale = Vector3.Lerp(key0.Scale, key1.Scale, k)
+                    });
+                }
+            }
+
+            result.Add(Keys.Last());
+
+            return result;
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectAnimationClip.cs
===================================================================
--- /OniSplit/Physics/ObjectAnimationClip.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectAnimationClip.cs	(revision 1114)
@@ -0,0 +1,22 @@
+﻿using System;
+
+namespace Oni.Physics
+{
+    internal class ObjectAnimationClip
+    {
+        public string Name;
+        public int Start;
+        public int End = int.MaxValue;
+        public int Stop;
+        public ObjectAnimationFlags Flags;
+
+        public ObjectAnimationClip()
+        {
+        }
+
+        public ObjectAnimationClip(string name)
+        {
+            this.Name = name;
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectAnimationFlags.cs
===================================================================
--- /OniSplit/Physics/ObjectAnimationFlags.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectAnimationFlags.cs	(revision 1114)
@@ -0,0 +1,15 @@
+﻿using System;
+
+namespace Oni.Physics
+{
+    [Flags]
+    internal enum ObjectAnimationFlags
+    {
+        None = 0,
+		Loop = 0x0001,
+		PingPong = 0x0002,
+		RandomStart = 0x0004,
+		AutoStart = 0x0008,
+		Local = 0x0010
+    }
+}
Index: /OniSplit/Physics/ObjectAnimationImporter.cs
===================================================================
--- /OniSplit/Physics/ObjectAnimationImporter.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectAnimationImporter.cs	(revision 1114)
@@ -0,0 +1,43 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Physics
+{
+    internal class ObjectAnimationImporter : Importer
+    {
+        public ObjectAnimationImporter(string[] args)
+        {
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            string name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (name.StartsWith("OBAN", StringComparison.Ordinal))
+                name = name.Substring(4);
+
+            var scene = Dae.Reader.ReadFile(filePath);
+
+            var importer = new ObjectDaeImporter(null, new Dictionary<string, Akira.AkiraDaeNodeProperties> {
+                { scene.Id, new ObjectDaeNodeProperties {
+                    HasPhysics = true,
+                    Animations = { new ObjectAnimationClip {
+                    } }
+                } }
+            });
+
+            importer.Import(scene);
+
+            BeginImport();
+
+            foreach (var node in importer.Nodes)
+            {
+                foreach (var anim in node.Animations)
+                    ObjectDatWriter.WriteAnimation(anim, this);
+            }
+
+            Write(outputDirPath);
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectAnimationKey.cs
===================================================================
--- /OniSplit/Physics/ObjectAnimationKey.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectAnimationKey.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿using System;
+
+namespace Oni.Physics
+{
+    internal class ObjectAnimationKey
+    {
+        public int Time;
+        public Vector3 Scale;
+        public Quaternion Rotation;
+        public Vector3 Translation;
+    }
+}
Index: /OniSplit/Physics/ObjectDaeImporter.cs
===================================================================
--- /OniSplit/Physics/ObjectDaeImporter.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectDaeImporter.cs	(revision 1114)
@@ -0,0 +1,232 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Akira;
+using Oni.Motoko;
+
+namespace Oni.Physics
+{
+    internal class ObjectDaeImporter
+    {
+        private readonly TextureImporter3 textureImporter;
+        private readonly Dictionary<string, AkiraDaeNodeProperties> properties;
+        private readonly List<ObjectNode> nodes = new List<ObjectNode>();
+
+        public ObjectDaeImporter(TextureImporter3 textureImporter, Dictionary<string, AkiraDaeNodeProperties> properties)
+        {
+            this.textureImporter = textureImporter;
+            this.properties = properties;
+        }
+
+        public List<ObjectNode> Nodes
+        {
+            get { return nodes; }
+        }
+
+        public void Import(Dae.Scene scene)
+        {
+            ImportNode(scene, null, GetNodeProperties(scene));
+        }
+
+        private void ImportNode(Dae.Node node, List<ObjectAnimationKey> parentAnimation, ObjectDaeNodeProperties parentNodeProperties)
+        {
+            Console.WriteLine("\t{0}", node.Id);
+
+            var animation = ImportNodeAnimation(node, parentAnimation);
+            var nodeProperties = GetNodeProperties(node);
+
+            if (nodeProperties == null && parentNodeProperties != null)
+            {
+                nodeProperties = new ObjectDaeNodeProperties
+                {
+                    HasPhysics = parentNodeProperties.HasPhysics,
+                    ScriptId = parentNodeProperties.ScriptId,
+                    ObjectFlags = parentNodeProperties.ObjectFlags
+                };
+
+                //
+                // We can't use the same anim name when we inherit the properties of the parent,
+                // generate a name by appending "_anim" to the node name.
+                //
+
+                nodeProperties.Animations.AddRange(
+                    from a in parentNodeProperties.Animations
+                    select new ObjectAnimationClip
+                    {
+                        Name = node.Name + "_anim",
+                        Start = a.Start,
+                        Stop = a.Stop,
+                        End = a.End,
+                        Flags = a.Flags
+                    });
+            }
+
+            if (nodeProperties != null && nodeProperties.HasPhysics)
+            {
+                var geometries = GeometryDaeReader.Read(node, textureImporter).ToList();
+
+                if (animation.Count > 0 || geometries.Count > 0)
+                {
+                    nodes.Add(new ObjectNode(from g in geometries select new ObjectGeometry(g))
+                    {
+                        Name = node.Name,
+                        FileName = node.FileName,
+                        Animations = CreateAnimations(animation, nodeProperties),
+                        ScriptId = nodeProperties.ScriptId,
+                        Flags = nodeProperties.ObjectFlags
+                    });
+                }
+            }
+
+            foreach (var child in node.Nodes)
+            {
+                ImportNode(child, animation, nodeProperties);
+            }
+        }
+
+        private List<ObjectAnimationKey> ImportNodeAnimation(Dae.Node node, List<ObjectAnimationKey> parentAnimation)
+        {
+            var scale = Vector3.One;
+            var scaleTransform = node.Transforms.OfType<Dae.TransformScale>().FirstOrDefault();
+
+            if (scaleTransform != null)
+            {
+                scale.X = scaleTransform.Values[0];
+                scale.Y = scaleTransform.Values[1];
+                scale.Z = scaleTransform.Values[2];
+            }
+
+            if (parentAnimation != null && parentAnimation.Count > 0)
+            {
+                scale *= parentAnimation[0].Scale;
+            }
+
+            var rotateTransforms = new List<Dae.TransformRotate>();
+            var angles = new List<float[]>();
+
+            foreach (var rotate in node.Transforms.OfType<Dae.TransformRotate>())
+            {
+                rotateTransforms.Add(rotate);
+
+                var angleAnimation = rotate.AngleAnimation;
+
+                if (angleAnimation != null)
+                    angles.Add(angleAnimation.Sample());
+                else
+                    angles.Add(new[] { rotate.Angle });
+            }
+
+            var translateTransforms = node.Transforms.OfType<Dae.TransformTranslate>().FirstOrDefault();
+            var positions = new List<float[]>();
+
+            if (translateTransforms != null)
+            {
+                for (int i = 0; i < 3; i++)
+                {
+                    var positionAnimation = translateTransforms.Animations[i];
+
+                    if (positionAnimation != null)
+                        positions.Add(positionAnimation.Sample());
+                    else
+                        positions.Add(new[] { translateTransforms.Translation[i] });
+                }
+            }
+
+            var frames = new List<ObjectAnimationKey>();
+            int frameCount = Math.Max(angles.Max(a => a.Length), positions.Max(a => a.Length));
+
+            for (int time = 0; time < frameCount; time++)
+            {
+                var rotation = Quaternion.Identity;
+
+                for (int i = 0; i < rotateTransforms.Count; i++)
+                {
+                    float angle;
+
+                    float[] values = angles[i];
+
+                    if (time >= values.Length)
+                        angle = values.Last();
+                    else
+                        angle = values[time];
+
+                    rotation *= Quaternion.CreateFromAxisAngle(rotateTransforms[i].Axis, MathHelper.ToRadians(angle));
+                }
+
+                var translation = Vector3.Zero;
+
+                if (translateTransforms != null)
+                {
+                    translation.X = positions[0][MathHelper.Clamp(time, 0, positions[0].Length - 1)];
+                    translation.Y = positions[1][MathHelper.Clamp(time, 0, positions[1].Length - 1)];
+                    translation.Z = positions[2][MathHelper.Clamp(time, 0, positions[2].Length - 1)];
+                }
+
+                if (parentAnimation != null)
+                {
+                    var parentFrame = time < parentAnimation.Count ? parentAnimation[time] : parentAnimation.LastOrDefault();
+
+                    if (parentFrame != null)
+                    {
+                        rotation = parentFrame.Rotation * rotation;
+                        translation = parentFrame.Translation + Vector3.Transform(translation * parentFrame.Scale, parentFrame.Rotation);
+                    }
+                }
+
+                frames.Add(new ObjectAnimationKey
+                {
+                    Time = time,
+                    Scale = scale,
+                    Rotation = rotation,
+                    Translation = translation
+                });
+            }
+
+            return frames;
+        }
+
+        private ObjectAnimation[] CreateAnimations(List<ObjectAnimationKey> allFrames, ObjectDaeNodeProperties props)
+        {
+            var anims = new List<ObjectAnimation>();
+
+            foreach (var clip in props.Animations)
+            {
+                int start = clip.Start;
+                int end = clip.End != int.MaxValue ? clip.End : allFrames.Last().Time;
+
+                var clipFrames = (from f in allFrames
+                                  where start <= f.Time && f.Time <= end
+                                  select new ObjectAnimationKey
+                                  {
+                                      Time = f.Time - start,
+                                      Scale = f.Scale,
+                                      Rotation = f.Rotation,
+                                      Translation = f.Translation
+                                  }).ToArray();
+
+                if (clipFrames.Length == 0)
+                    continue;
+
+                anims.Add(new ObjectAnimation
+                {
+                    Name = clip.Name,
+                    Flags = clip.Flags,
+                    Stop = clip.Stop,
+                    Length = end - start + 1,
+                    Keys = clipFrames
+                });
+            }
+
+            return anims.ToArray();
+        }
+
+        private ObjectDaeNodeProperties GetNodeProperties(Dae.Node node)
+        {
+            AkiraDaeNodeProperties nodeProperties;
+
+            if (properties == null || !properties.TryGetValue(node.Id, out nodeProperties))
+                return null;
+
+            return nodeProperties as ObjectDaeNodeProperties;
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectDaeNodeProperties.cs
===================================================================
--- /OniSplit/Physics/ObjectDaeNodeProperties.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectDaeNodeProperties.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System.Collections.Generic;
+
+namespace Oni.Physics
+{
+    using Akira;
+
+    internal class ObjectDaeNodeProperties : AkiraDaeNodeProperties
+    {
+        public ObjectSetupFlags ObjectFlags;
+        public ObjectPhysicsType PhysicsType;
+        public readonly List<ObjectAnimationClip> Animations = new List<ObjectAnimationClip>();
+        public readonly List<ObjectParticle> Particles = new List<ObjectParticle>();
+    }
+}
Index: /OniSplit/Physics/ObjectDatReader.cs
===================================================================
--- /OniSplit/Physics/ObjectDatReader.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectDatReader.cs	(revision 1114)
@@ -0,0 +1,126 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Akira;
+using Oni.Motoko;
+
+namespace Oni.Physics
+{
+    internal class ObjectDatReader
+    {
+        public static ObjectNode ReadObjectGeometry(InstanceDescriptor ofga)
+        {
+            ObjectGeometry[] geometries = null;
+            ObjectParticle[] particles = new ObjectParticle[0];
+
+            if (ofga.Template.Tag == TemplateTag.OFGA)
+            {
+                using (var reader = ofga.OpenRead(16))
+                {
+                    var particlesDescriptor = reader.ReadInstance();
+                    geometries = ReadGeometries(reader);
+
+                    if (particlesDescriptor != null)
+                        particles = ReadParticles(particlesDescriptor);
+                }
+            }
+            else if (ofga.Template.Tag == TemplateTag.M3GM)
+            {
+                geometries = new ObjectGeometry[1];
+
+                geometries[0] = new ObjectGeometry
+                {
+                    Flags = GunkFlags.NoOcclusion,
+                    Geometry = GeometryDatReader.Read(ofga)
+                };
+            }
+
+            return new ObjectNode(geometries, particles);
+        }
+
+        private static ObjectGeometry[] ReadGeometries(BinaryReader reader)
+        {
+            uint partCount = reader.ReadUInt32();
+
+            var nodes = new ObjectGeometry[partCount];
+
+            for (int i = 0; i < nodes.Length; i++)
+                nodes[i] = ReadGeometry(reader);
+
+            return nodes;
+        }
+
+        private static ObjectGeometry ReadGeometry(BinaryReader reader)
+        {
+            var node = new ObjectGeometry
+            {
+                Flags = (GunkFlags)reader.ReadInt32(),
+                Geometry = GeometryDatReader.Read(reader.ReadInstance())
+            };
+
+            reader.Skip(4);
+
+            return node;
+        }
+
+        private static ObjectParticle[] ReadParticles(InstanceDescriptor particlesDescriptor)
+        {
+            using (var reader = particlesDescriptor.OpenRead(22))
+            {
+                var particles = new ObjectParticle[reader.ReadUInt16()];
+
+                for (int i = 0; i < particles.Length; i++)
+                    particles[i] = ReadParticle(reader);
+
+                return particles;
+            }
+        }
+
+        private static ObjectParticle ReadParticle(BinaryReader reader)
+        {
+            var particle = new ObjectParticle
+            {
+                ParticleClass = reader.ReadString(64),
+                Tag = reader.ReadString(48),
+                Matrix = reader.ReadMatrix4x3(),
+                DecalScale = reader.ReadVector2(),
+                Flags = (Objects.ParticleFlags)reader.ReadUInt16()
+            };
+
+            reader.Skip(38);
+
+            return particle;
+        }
+
+        public static ObjectAnimation ReadAnimation(InstanceDescriptor oban)
+        {
+            var animation = new ObjectAnimation
+            {
+                Name = oban.Name
+            };
+
+            using (var reader = oban.OpenRead(12))
+            {
+                animation.Flags = (ObjectAnimationFlags)reader.ReadInt32();
+                reader.Skip(48);
+                var scale = reader.ReadMatrix4x3().Scale;
+                reader.Skip(2);
+                animation.Length = reader.ReadUInt16();
+                animation.Stop = reader.ReadInt16();
+                animation.Keys = new ObjectAnimationKey[reader.ReadUInt16()];
+
+                for (int i = 0; i < animation.Keys.Length; i++)
+                {
+                    animation.Keys[i] = new ObjectAnimationKey
+                    {
+                        Scale = scale,
+                        Rotation = reader.ReadQuaternion(),
+                        Translation = reader.ReadVector3(),
+                        Time = reader.ReadInt32()
+                    };
+                }
+            }
+
+            return animation;
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectDatWriter.cs
===================================================================
--- /OniSplit/Physics/ObjectDatWriter.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectDatWriter.cs	(revision 1114)
@@ -0,0 +1,39 @@
+﻿using System;
+
+namespace Oni.Physics
+{
+    internal class ObjectDatWriter
+    {
+        internal static ImporterDescriptor WriteAnimation(ObjectAnimation animation, Importer importer)
+        {
+            var frame0 = animation.Keys[0];
+            var scaleMatrix = Matrix.CreateScale(frame0.Scale);
+
+            var startMatrix = scaleMatrix
+                * Matrix.CreateFromQuaternion(frame0.Rotation)
+                * Matrix.CreateTranslation(frame0.Translation);
+
+            var oban = importer.CreateInstance(TemplateTag.OBAN, animation.Name);
+
+            using (var writer = oban.OpenWrite(12))
+            {
+                writer.Write((int)animation.Flags);
+                writer.WriteMatrix4x3(startMatrix);
+                writer.WriteMatrix4x3(scaleMatrix);
+                writer.WriteInt16(1);
+                writer.WriteUInt16(animation.Length);
+                writer.WriteInt16(animation.Stop);
+                writer.WriteUInt16(animation.Keys.Length);
+
+                foreach (var key in animation.Keys)
+                {
+                    writer.Write(key.Rotation);
+                    writer.Write(key.Translation);
+                    writer.Write(key.Time);
+                }
+            }
+
+            return oban;
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectGeometry.cs
===================================================================
--- /OniSplit/Physics/ObjectGeometry.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectGeometry.cs	(revision 1114)
@@ -0,0 +1,21 @@
+﻿using System;
+using Oni.Akira;
+using Oni.Motoko;
+
+namespace Oni.Physics
+{
+    internal class ObjectGeometry
+    {
+        public Geometry Geometry;
+        public GunkFlags Flags;
+
+        public ObjectGeometry()
+        {
+        }
+
+        public ObjectGeometry(Geometry geometry)
+        {
+            this.Geometry = geometry;
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectNode.cs
===================================================================
--- /OniSplit/Physics/ObjectNode.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectNode.cs	(revision 1114)
@@ -0,0 +1,28 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Physics
+{
+    internal class ObjectNode
+    {
+        public string Name;
+        public string FileName;
+        public ObjectSetupFlags Flags;
+        public int ScriptId;
+        public readonly ObjectGeometry[] Geometries;
+        public readonly ObjectParticle[] Particles;
+        public ObjectAnimation[] Animations = new ObjectAnimation[0];
+
+        public ObjectNode(IEnumerable<ObjectGeometry> geometries)
+        {
+            this.Geometries = geometries.ToArray();
+            this.Particles = new ObjectParticle[0];
+        }
+
+        public ObjectNode(ObjectGeometry[] geometries, ObjectParticle[] particles)
+        {
+            this.Geometries = geometries;
+            this.Particles = particles;
+        }
+    }
+}
Index: /OniSplit/Physics/ObjectParticle.cs
===================================================================
--- /OniSplit/Physics/ObjectParticle.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectParticle.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+using Oni.Objects;
+
+namespace Oni.Physics
+{
+    internal class ObjectParticle
+    {
+        public string ParticleClass;
+        public string Tag;
+        public Matrix Matrix;
+        public Vector2 DecalScale;
+        public ParticleFlags Flags;
+    }
+}
Index: /OniSplit/Physics/ObjectPhysicsType.cs
===================================================================
--- /OniSplit/Physics/ObjectPhysicsType.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectPhysicsType.cs	(revision 1114)
@@ -0,0 +1,13 @@
+﻿using System;
+
+namespace Oni.Physics
+{
+    internal enum ObjectPhysicsType
+    {
+        None = 0,
+        Static = 1,
+        Linear = 2,
+        Animated = 3,
+        Newton = 4,
+    }
+}
Index: /OniSplit/Physics/ObjectSetup.cs
===================================================================
--- /OniSplit/Physics/ObjectSetup.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectSetup.cs	(revision 1114)
@@ -0,0 +1,24 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Physics;
+
+namespace Oni.Level
+{
+    internal class ObjectSetup
+    {
+        public object[] Geometries;
+        public ObjectAnimation Animation;
+        public readonly List<ObjectParticle> Particles = new List<ObjectParticle>();
+        public ObjectSetupFlags Flags;
+        //public int DoorGunkIndex;
+        public int DoorScriptId;
+        public ObjectPhysicsType PhysicsType;
+        public int ScriptId = 65535;
+        public Vector3 Position;
+        public Quaternion Orientation = Quaternion.Identity;
+        public float Scale = 1.0f;
+        public Matrix Origin;
+        public string Name;
+        public string FileName;
+    }
+}
Index: /OniSplit/Physics/ObjectSetupFlags.cs
===================================================================
--- /OniSplit/Physics/ObjectSetupFlags.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectSetupFlags.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+
+namespace Oni.Physics
+{
+    [Flags]
+    internal enum ObjectSetupFlags
+    {
+        None = 0,
+        InUse = 0x200,
+        NoCollision = 0x400,
+        NoGravity = 0x800,
+        FaceCollision = 0x1000,
+    }
+}
Index: /OniSplit/Physics/ObjectXmlReader.cs
===================================================================
--- /OniSplit/Physics/ObjectXmlReader.cs	(revision 1114)
+++ /OniSplit/Physics/ObjectXmlReader.cs	(revision 1114)
@@ -0,0 +1,27 @@
+﻿using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Physics
+{
+    internal class ObjectXmlReader
+    {
+        public static ObjectParticle ReadParticle(XmlReader xml)
+        {
+            xml.ReadStartElement("Particle");
+
+            var particle = new ObjectParticle
+            {
+                ParticleClass = xml.ReadElementContentAsString("Class", ""),
+                Tag = xml.ReadElementContentAsString("Tag", ""),
+                Matrix = xml.ReadElementContentAsMatrix43("Transform"),
+                DecalScale = xml.ReadElementContentAsVector2("DecalScale"),
+                Flags = xml.ReadElementContentAsEnum<Objects.ParticleFlags>("Flags") & Objects.ParticleFlags.NotInitiallyCreated
+            };
+
+            xml.ReadEndElement();
+
+            return particle;
+        }
+    }
+}
Index: /OniSplit/Program.cs
===================================================================
--- /OniSplit/Program.cs	(revision 1114)
+++ /OniSplit/Program.cs	(revision 1114)
@@ -0,0 +1,964 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using Oni.Akira;
+using Oni.Collections;
+using Oni.Metadata;
+using Oni.Sound;
+using Oni.Xml;
+
+namespace Oni
+{
+    internal class Program
+    {
+        private static readonly InstanceFileManager fileManager = new InstanceFileManager();
+
+        private static int Main(string[] args)
+        {
+            if (args.Length == 0)
+            {
+                Help(args);
+                Console.WriteLine("Press any key to continue");
+                Console.ReadKey();
+                return 0;
+            }
+
+            if (args[0] == "-cdump")
+            {
+                InstanceMetadata.DumpCStructs(Console.Out);
+                return 0;
+            }
+
+            Dae.IO.DaeReader.CommandLineArgs = args;
+
+            if (args[0] == "-silent")
+            {
+                Console.SetOut(new StreamWriter(Stream.Null));
+                Console.SetError(new StreamWriter(Stream.Null));
+
+                var newArgs = new string[args.Length - 1];
+                Array.Copy(args, 1, newArgs, 0, newArgs.Length);
+                args = newArgs;
+            }
+
+            if (args[0] == "-noexcept")
+            {
+                var newArgs = new string[args.Length - 1];
+                Array.Copy(args, 1, newArgs, 0, newArgs.Length);
+                args = newArgs;
+                return Execute(args);
+            }
+
+            args = AddSearchPaths(args);
+
+            try
+            {
+                return Execute(args);
+            }
+            catch (Exception ex)
+            {
+                Console.Error.WriteLine(ex.ToString());
+                return 1;
+            }
+        }
+
+        private static int Execute(string[] args)
+        {
+            if (args[0].StartsWith("-export:", StringComparison.Ordinal))
+                return Unpack(args);
+
+            switch (args[0])
+            {
+                case "-help":
+                    return Help(args);
+
+                case "-version":
+                    return PrintVersion();
+
+                case "-export":
+                    return Unpack(args);
+
+                case "pack":
+                case "-import":
+                case "-import:nosep":
+                case "-import:sep":
+                case "-import:ppc":
+                case "-import:pc":
+                    return Pack(args);
+
+                case "-copy":
+                    return Copy(args);
+
+                case "-move":
+                case "-move:overwrite":
+                case "-move:delete":
+                    return Move(args);
+
+                case "-list":
+                    return List(args);
+
+                case "-deps":
+                    return GetDependencies(args);
+
+                case "extract":
+                case "-extract:xml":
+                    return ExportXml(args);
+
+                case "-extract:tga":
+                case "-extract:dds":
+                case "-extract:png":
+                case "-extract:jpg":
+                case "-extract:bmp":
+                case "-extract:tif":
+                    return ExportTextures(args);
+
+                case "-extract:wav":
+                case "-extract:aif":
+                    return ExportSounds(args);
+
+                case "-extract:obj":
+                case "-extract:dae":
+                    return ExportGeometry(args);
+
+                case "-extract:txt":
+                    return ExportSubtitles(args);
+
+                case "-create:akev":
+                    return CreateAkira(args);
+
+                case "-create:tram":
+                case "-create:trbs":
+                case "-create:txmp":
+                case "-create:m3gm":
+                case "-create:subt":
+                case "-create:oban":
+                case "-create":
+                case "create":
+                    return CreateGeneric(args);
+
+                case "-grid:create":
+                    return CreateGrids(args);
+
+                case "-create:level":
+                    return ImportLevel(args);
+
+                case "-room:extract":
+                    return ExtractRooms(args);
+
+                case "film2xml":
+                    return ConvertFilm2Xml(args);
+
+                default:
+                    Console.Error.WriteLine("Unknown command {0}", args[0]);
+                    return 1;
+            }
+        }
+
+        private static string[] AddSearchPaths(string[] args)
+        {
+            List<string> newArgs = new List<string>();
+
+            for (int i = 0; i < args.Length; i++)
+            {
+                if (args[i] == "-search")
+                {
+                    i++;
+
+                    if (i < args.Length)
+                        fileManager.AddSearchPath(args[i]);
+                }
+                else
+                {
+                    newArgs.Add(args[i]);
+                }
+            }
+
+            return newArgs.ToArray();
+        }
+
+        private static int Help(string[] args)
+        {
+            if (args.Length > 1 && args[1] == "enums")
+            {
+                HelpEnums();
+                return 0;
+            }
+
+            Console.WriteLine("{0} [options] datfile", Environment.GetCommandLineArgs()[0]);
+            Console.WriteLine();
+            Console.WriteLine("Options:");
+            Console.WriteLine("\t-export <directory>\t\tExport a Oni .dat file to directory");
+            Console.WriteLine("\t-import <directory>\t\tImport a Oni .dat file from directory");
+            Console.WriteLine("\t\t\t\t\tTarget file format is determined from source files (when possible)");
+            Console.WriteLine("\t-import:sep <directory>\t\tImport a Oni .dat file from directory");
+            Console.WriteLine("\t\t\t\t\tCreate a .dat file that uses .raw and .sep binary files (Mac and PC Demo)");
+            Console.WriteLine("\t-import:nosep <directory>\tImport a Oni .dat file from directory");
+            Console.WriteLine("\t\t\t\t\tCreate a .dat file that uses only .raw binary file (PC)");
+            Console.WriteLine();
+            Console.WriteLine("\t-extract:dds <directory>\tExtracts all textures (TXMP) from a Oni .dat/.oni file in DDS format");
+            Console.WriteLine("\t-extract:tga <directory>\tExtracts all textures (TXMP) from a Oni .dat/.oni file in TGA format");
+            Console.WriteLine("\t-extract:png <directory>\tExtracts all textures (TXMP) from a Oni .dat/.oni file in PNG format");
+            Console.WriteLine("\t-extract:wav <directory>\tExtracts all sounds (SNDD) from a Oni .dat/.oni file in WAV format");
+            Console.WriteLine("\t-extract:aif <directory>\tExtracts all sounds (SNDD) from a Oni .dat/.oni file in AIF format");
+            Console.WriteLine("\t-extract:txt <directory>\tExtracts all subtitles (SUBT) from a Oni .dat/.oni file in TXT format");
+            Console.WriteLine("\t-extract:obj <directory>\tExtracts all M3GM and ONCC instances to Wavefront OBJ files");
+            Console.WriteLine("\t-extract:dae <directory>\tExtracts all M3GM and ONCC instances to Collada files");
+            Console.WriteLine("\t-extract:xml <directory>\tExtracts all instances to XML files");
+            Console.WriteLine();
+            Console.WriteLine("\t-create:txmp <directory> [-nomipmaps] [-nouwrap] [-novwrap] [-format:bgr|rgba|bgr555|bgra5551|bgra4444|dxt1] [-envmap:texture_name] [-large] image_file");
+            Console.WriteLine("\t-create:m3gm <directory> [-tex:texture_name] obj_file");
+            Console.WriteLine("\t-create:trbs <directory> dae_file");
+            Console.WriteLine("\t-create:subt <directory> txt_file");
+            Console.WriteLine("\t-create <directory> xml_file\tCreates an .oni file from an XML file");
+            Console.WriteLine();
+            Console.WriteLine("\t-grid:create -out:<directory> rooms_src.dae level_geometry1.dae level_geometry2.dae ...\tGenerates pathfinding grids");
+            Console.WriteLine();
+            Console.WriteLine("\t-list\t\t\t\tLists the named instances contained in datfile");
+            Console.WriteLine("\t-copy <directory>\t\tCopy an exported .oni file and its dependencies to directory");
+            Console.WriteLine("\t-move <directory>\t\tMove an exported .oni file and its dependencies to directory");
+            Console.WriteLine("\t-move:overwrite <directory>\tMove an exported .oni file and its dependencies to directory");
+            Console.WriteLine("\t\t\t\t\tOverwrites any existing files");
+            Console.WriteLine("\t-move:delete <directory>\tMove an exported .oni file and its dependencies to directory");
+            Console.WriteLine("\t\t\t\t\tDeletes files at source when they already exist at destination");
+            Console.WriteLine("\t-deps\t\t\t\tGet a list of exported .oni files the specified files depends on");
+            Console.WriteLine("\t-version\t\t\tShow OniSplit versions");
+            Console.WriteLine("\t-help\t\t\t\tShow this help");
+            Console.WriteLine("\t-help enums\t\t\tShow a list of enums and flags used in XML files");
+            Console.WriteLine();
+
+            return 0;
+        }
+
+        private static void HelpEnums()
+        {
+            WriteEnums(typeof(InstanceMetadata));
+            WriteEnums(typeof(ObjectMetadata));
+
+            Console.WriteLine("-----------------------------------------------------");
+            Console.WriteLine("Particles enums");
+            Console.WriteLine("-----------------------------------------------------");
+            Console.WriteLine();
+
+            Utils.WriteEnum(typeof(Particles.ParticleFlags1));
+            Utils.WriteEnum(typeof(Particles.ParticleFlags2));
+            Utils.WriteEnum(typeof(Particles.EmitterFlags));
+            Utils.WriteEnum(typeof(Particles.EmitterOrientation));
+            Utils.WriteEnum(typeof(Particles.EmitterPosition));
+            Utils.WriteEnum(typeof(Particles.EmitterRate));
+            Utils.WriteEnum(typeof(Particles.EmitterSpeed));
+            Utils.WriteEnum(typeof(Particles.EmitterDirection));
+            Utils.WriteEnum(typeof(Particles.DisableDetailLevel));
+            Utils.WriteEnum(typeof(Particles.AttractorSelector));
+            Utils.WriteEnum(typeof(Particles.AttractorTarget));
+            Utils.WriteEnum(typeof(Particles.EventType));
+            Utils.WriteEnum(typeof(Particles.SpriteType));
+            Utils.WriteEnum(typeof(Particles.StorageType));
+            Utils.WriteEnum(typeof(Particles.ValueType));
+
+            Console.WriteLine("-----------------------------------------------------");
+            Console.WriteLine("Object enums");
+            Console.WriteLine("-----------------------------------------------------");
+            Console.WriteLine();
+
+            Utils.WriteEnum(typeof(Physics.ObjectSetupFlags));
+            Utils.WriteEnum(typeof(Physics.ObjectPhysicsType));
+            Utils.WriteEnum(typeof(Physics.ObjectAnimationFlags));
+        }
+
+        private static void WriteEnums(Type type)
+        {
+#if !NETCORE
+            foreach (var enumType in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic))
+            {
+                if (!enumType.IsEnum)
+                    continue;
+
+                Utils.WriteEnum(enumType);
+            }
+#endif
+        }
+
+        private static int Unpack(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var prefixes = new List<string>();
+            string outputDirPath = null;
+            string sourceFilePath = null;
+
+            foreach (string arg in args)
+            {
+                if (arg.StartsWith("-export", StringComparison.Ordinal))
+                {
+                    string prefix = null;
+                    int i = arg.IndexOf(':');
+
+                    if (i != -1)
+                        prefix = arg.Substring(i + 1);
+
+                    if (!string.IsNullOrEmpty(prefix))
+                        prefixes.Add(prefix);
+                }
+                else if (outputDirPath == null)
+                {
+                    outputDirPath = Path.GetFullPath(arg);
+                }
+                else if (sourceFilePath == null)
+                {
+                    sourceFilePath = Path.GetFullPath(arg);
+                }
+            }
+
+            var unpacker = new DatUnpacker(fileManager, outputDirPath);
+
+            if (prefixes.Count > 0)
+                unpacker.NameFilter = Utils.WildcardToRegex(prefixes);
+
+            unpacker.ExportFiles(new[] { sourceFilePath });
+
+            return 0;
+        }
+
+        private static int ExportTextures(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            int i = args[0].IndexOf(':');
+            string fileType = null;
+
+            if (i != -1)
+                fileType = args[0].Substring(i + 1);
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var sourceFilePaths = GetFileList(args, 2);
+
+            var exporter = new Oni.Motoko.TextureExporter(fileManager, outputDirPath, fileType);
+            exporter.ExportFiles(sourceFilePaths);
+
+            return 0;
+        }
+
+        private static int ExportSounds(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            int i = args[0].IndexOf(':');
+            string fileType = null;
+
+            if (i != -1)
+                fileType = args[0].Substring(i + 1);
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var sourceFilePaths = GetFileList(args, 2);
+
+            SoundExporter exporter;
+
+            switch (fileType)
+            {
+                case "aif":
+                    exporter = new AifExporter(fileManager, outputDirPath);
+                    break;
+                case "wav":
+                    exporter = new WavExporter(fileManager, outputDirPath);
+                    break;
+                default:
+                    throw new NotSupportedException(string.Format("Unsupported file type {0}", fileType));
+            }
+
+            exporter.ExportFiles(sourceFilePaths);
+
+            return 0;
+        }
+
+        private static int ExportGeometry(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            int i = args[0].IndexOf(':');
+            string fileType = null;
+
+            if (i != -1)
+                fileType = args[0].Substring(i + 1);
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var sourceFilePaths = GetFileList(args, 2);
+
+            var exporter = new DaeExporter(args, fileManager, outputDirPath, fileType);
+            exporter.ExportFiles(sourceFilePaths);
+
+            return 0;
+        }
+
+        private static int ExportSubtitles(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var sourceFilePaths = GetFileList(args, 2);
+
+            var exporter = new SubtitleExporter(fileManager, outputDirPath);
+            exporter.ExportFiles(sourceFilePaths);
+
+            return 0;
+        }
+
+        private static int ExportXml(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var sourceFilePaths = GetFileList(args, 2);
+
+            var exporter = new XmlExporter(fileManager, outputDirPath)
+            {
+                Recursive = args.Any(a => a == "-recurse"),
+                MergeAnimations = args.Any(a => a == "-anim-merge"),
+                NoAnimation = args.Any(a => a == "-noanim")
+            };
+
+            var animBodyFilePath = args.FirstOrDefault(a => a.StartsWith("-anim-body:", StringComparison.Ordinal));
+
+            if (animBodyFilePath != null)
+            {
+                animBodyFilePath = Path.GetFullPath(animBodyFilePath.Substring("-anim-body:".Length));
+                var file = fileManager.OpenFile(animBodyFilePath);
+                exporter.AnimationBody = Totoro.BodyDatReader.Read(file.Descriptors[0]);
+            }
+
+            exporter.ExportFiles(sourceFilePaths);
+
+            return 0;
+        }
+
+        private static int Pack(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var packer = new DatPacker();
+            var inputPaths = new List<string>();
+
+            if (args[0] == "pack")
+            {
+                for (int i = 1; i < args.Length; i++)
+                {
+                    var arg = args[i];
+
+                    if (arg == "-out")
+                    {
+                        i++;
+                        packer.TargetFilePath = Path.GetFullPath(args[i]);
+                        continue;
+                    }
+
+                    if (arg.StartsWith("-type:", StringComparison.Ordinal))
+                    {
+                        switch (arg.Substring(6))
+                        {
+                            case "nosep":
+                            case "pc":
+                                packer.TargetTemplateChecksum = InstanceFileHeader.OniPCTemplateChecksum;
+                                break;
+
+                            case "sep":
+                            case "pcdemo":
+                            case "macintel":
+                                packer.TargetTemplateChecksum = InstanceFileHeader.OniMacTemplateChecksum;
+                                break;
+
+                            case "ppc":
+                                packer.TargetTemplateChecksum = InstanceFileHeader.OniMacTemplateChecksum;
+                                packer.TargetBigEndian = true;
+                                break;
+
+                            default:
+                                throw new ArgumentException(string.Format("Unknown output type {0}", arg.Substring(6)));
+                        }
+
+                        continue;
+                    }
+
+                    if (Directory.Exists(arg))
+                    {
+                        arg = Path.GetFullPath(arg);
+                        Console.WriteLine("Reading directory {0}", arg);
+                        inputPaths.AddRange(Directory.GetFiles(arg, "*.oni", SearchOption.AllDirectories));
+                    }
+                    else
+                    {
+                        var dirPath = Path.GetDirectoryName(arg);
+                        var fileSpec = Path.GetFileName(arg);
+
+                        if (string.IsNullOrEmpty(dirPath))
+                            dirPath = Directory.GetCurrentDirectory();
+                        else
+                            dirPath = Path.GetFullPath(dirPath);
+
+                        if (Directory.Exists(dirPath))
+                        {
+                            foreach (string filePath in Directory.GetFiles(dirPath, fileSpec))
+                            {
+                                Console.WriteLine("Reading {0}", filePath);
+                                inputPaths.Add(filePath);
+                            }
+                        }
+                    }
+                }
+
+                packer.Pack(fileManager, inputPaths);
+            }
+            else
+            {
+                switch (args[0])
+                {
+                    case "-import:nosep":
+                    case "-import:pc":
+                        packer.TargetTemplateChecksum = InstanceFileHeader.OniPCTemplateChecksum;
+                        break;
+
+                    case "-import:sep":
+                    case "-import:pcdemo":
+                    case "-import:macintel":
+                        packer.TargetTemplateChecksum = InstanceFileHeader.OniMacTemplateChecksum;
+                        break;
+
+                    case "-import:ppc":
+                        packer.TargetTemplateChecksum = InstanceFileHeader.OniMacTemplateChecksum;
+                        packer.TargetBigEndian = true;
+                        break;
+                }
+
+                for (int i = 1; i < args.Length - 1; i++)
+                    inputPaths.Add(Path.GetFullPath(args[i]));
+
+                packer.TargetFilePath = Path.GetFullPath(args[args.Length - 1]);
+                packer.Import(fileManager, inputPaths.ToArray());
+            }
+
+            return 0;
+        }
+
+        private static int Copy(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var inputFilePaths = GetFileList(args, 2);
+            var copy = new InstanceFileOperations();
+
+            copy.Copy(fileManager, inputFilePaths, outputDirPath);
+
+            return 0;
+        }
+
+        private static int Move(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var inputFilePaths = GetFileList(args, 2);
+            var copy = new InstanceFileOperations();
+
+            if (args[0] == "-move:delete")
+                copy.MoveDelete(fileManager, inputFilePaths, outputDirPath);
+            else if (args[0] == "-move:overwrite")
+                copy.MoveOverwrite(fileManager, inputFilePaths, outputDirPath);
+            else
+                copy.Move(fileManager, inputFilePaths, outputDirPath);
+
+            return 0;
+        }
+
+        private static int List(string[] args)
+        {
+            if (args.Length < 2)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            string sourceFilePath = Path.GetFullPath(args[1]);
+
+            var file = fileManager.OpenFile(sourceFilePath);
+
+            foreach (var descriptor in file.GetNamedDescriptors())
+                Console.WriteLine(descriptor.FullName);
+
+            return 0;
+        }
+
+        private static int GetDependencies(string[] args)
+        {
+            if (args.Length < 2)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var inputFilePaths = GetFileList(args, 1);
+
+            InstanceFileOperations copy = new InstanceFileOperations();
+            copy.GetDependencies(fileManager, inputFilePaths);
+
+            return 0;
+        }
+
+        private static int PrintVersion()
+        {
+            Console.WriteLine("OniSplit version {0}", Utils.Version);
+            return 0;
+        }
+
+        private static int CreateGrids(string[] args)
+        {
+            if (args.Length < 2)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var filePaths = GetFileList(args, 1);
+
+            var roomsScene = Dae.Reader.ReadFile(filePaths[0]);
+            var geometryMesh = Akira.AkiraDaeReader.Read(filePaths.Skip(1));
+            var gridBuilder = new Akira.RoomGridBuilder(roomsScene, geometryMesh);
+            string outputDirPath = null;
+
+            foreach (string arg in args)
+            {
+                if (arg.StartsWith("-out:", StringComparison.Ordinal))
+                    outputDirPath = Path.GetFullPath(arg.Substring(5));
+            }
+
+            if (string.IsNullOrEmpty(outputDirPath))
+            {
+                Console.Error.WriteLine("Output path must be specified");
+                return 1;
+            }
+
+            gridBuilder.Build();
+            AkiraDaeWriter.WriteRooms(gridBuilder.Mesh, Path.GetFileNameWithoutExtension(filePaths[0]), outputDirPath);
+
+            return 0;
+        }
+
+        private static int ExtractRooms(string[] args)
+        {
+            if (args.Length < 2)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            string outputFilePath = null;
+
+            foreach (string arg in args)
+            {
+                if (arg.StartsWith("-out:", StringComparison.Ordinal))
+                    outputFilePath = Path.GetFullPath(arg.Substring(5));
+            }
+
+            if (string.IsNullOrEmpty(outputFilePath))
+            {
+                Console.Error.WriteLine("Output file path must be specified");
+                return 1;
+            }
+
+            var extractor = new Akira.RoomExtractor(GetFileList(args, 1), outputFilePath);
+            extractor.Extract();
+            return 0;
+        }
+
+        private static int CreateAkira(string[] args)
+        {
+            if (args.Length < 2)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var filePaths = GetFileList(args, 2);
+
+            Directory.CreateDirectory(outputDirPath);
+
+            var importedFiles = new Set<string>(StringComparer.OrdinalIgnoreCase);
+            var taskQueue = new Queue<ImporterTask>();
+
+            foreach (string filePath in filePaths)
+                importedFiles.Add(filePath);
+
+            var importer = new AkiraImporter(args);
+
+            Console.WriteLine("Importing {0}", filePaths[0]);
+
+            importer.Import(filePaths, outputDirPath);
+
+            QueueTasks(importedFiles, taskQueue, importer);
+            ExecuteTasks(args, outputDirPath, importedFiles, taskQueue);
+
+            return 0;
+        }
+
+        private static int CreateGeneric(string[] args)
+        {
+            if (args.Length < 2)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var targetType = TemplateTag.NONE;
+            int colonIndex = args[0].IndexOf(':');
+
+            if (colonIndex != -1)
+            {
+                string tagName = args[0].Substring(colonIndex + 1);
+                targetType = (TemplateTag)Enum.Parse(typeof(TemplateTag), tagName, true);
+            }
+
+            string outputDirPath = Path.GetFullPath(args[1]);
+            var filePaths = GetFileList(args, 2);
+
+            Directory.CreateDirectory(outputDirPath);
+
+            var importedFiles = new Set<string>(StringComparer.OrdinalIgnoreCase);
+            var importQueue = new Queue<ImporterTask>();
+
+            foreach (string filePath in filePaths)
+            {
+                if (importedFiles.Add(filePath))
+                    importQueue.Enqueue(new ImporterTask(filePath, targetType));
+            }
+
+            ExecuteTasks(args, outputDirPath, importedFiles, importQueue);
+
+            return 0;
+        }
+
+        private static void ExecuteTasks(string[] args, string outputDirPath, Set<string> importedFiles, Queue<ImporterTask> taskQueue)
+        {
+            while (taskQueue.Count > 0)
+            {
+                var task = taskQueue.Dequeue();
+
+                if (!File.Exists(task.FilePath))
+                {
+                    Console.Error.WriteLine("File {0} does not exist", task.FilePath);
+                    continue;
+                }
+
+                var importer = CreateImporterFromFileName(args, task);
+
+                if (importer == null)
+                {
+                    Console.Error.WriteLine("{0} files cannot be imported as {1}", Path.GetExtension(task.FilePath), task.Type);
+                    continue;
+                }
+
+                Console.WriteLine("Importing {0}", task.FilePath);
+
+                importer.Import(task.FilePath, outputDirPath);
+
+                QueueTasks(importedFiles, taskQueue, importer);
+            }
+        }
+
+        private static Importer CreateImporterFromFileName(string[] args, ImporterTask task)
+        {
+            Importer importer = null;
+
+            switch (Path.GetExtension(task.FilePath).ToLowerInvariant())
+            {
+                case ".bin":
+                    importer = new BinImporter();
+                    break;
+
+                case ".xml":
+                    importer = new XmlImporter(args);
+                    break;
+
+                case ".tga":
+                case ".dds":
+                case ".png":
+                case ".jpg":
+                case ".bmp":
+                case ".tif":
+                    if (task.Type == TemplateTag.NONE || task.Type == TemplateTag.TXMP)
+                        importer = new Oni.Motoko.TextureImporter(args);
+                    break;
+
+                case ".obj":
+                case ".dae":
+                    if (task.Type == TemplateTag.NONE || task.Type == TemplateTag.M3GM)
+                        importer = new Motoko.GeometryImporter(args);
+                    else if (task.Type == TemplateTag.AKEV)
+                        importer = new AkiraImporter(args);
+                    else if (task.Type == TemplateTag.TRBS)
+                        importer = new Totoro.BodySetImporter(args);
+                    else if (task.Type == TemplateTag.OBAN)
+                        importer = new Physics.ObjectAnimationImporter(args);
+                    break;
+
+                case ".wav":
+                    if (task.Type == TemplateTag.NONE || task.Type == TemplateTag.SNDD)
+                        importer = new WavImporter();
+                    break;
+
+                case ".aif":
+                case ".aifc":
+                case ".afc":
+                    if (task.Type == TemplateTag.NONE || task.Type == TemplateTag.SNDD)
+                        importer = new AifImporter();
+                    break;
+
+                case ".txt":
+                    if (task.Type == TemplateTag.NONE || task.Type == TemplateTag.SUBT)
+                        importer = new SubtitleImporter();
+                    break;
+            }
+
+            return importer;
+        }
+
+        private static void QueueTasks(Set<string> imported, Queue<ImporterTask> importQueue, Importer importer)
+        {
+            foreach (ImporterTask child in importer.Dependencies)
+            {
+                if (!imported.Contains(child.FilePath))
+                {
+                    imported.Add(child.FilePath);
+                    importQueue.Enqueue(child);
+                }
+            }
+        }
+
+        private static int ConvertFilm2Xml(string[] args)
+        {
+            if (args.Length < 3)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+            var inputFilePaths = GetFileList(args, 2);
+
+            Directory.CreateDirectory(outputDirPath);
+
+            foreach (string filePath in inputFilePaths)
+                FilmToXmlConverter.Convert(filePath, outputDirPath);
+
+            return 0;
+        }
+
+        private static int ImportLevel(string[] args)
+        {
+            if (args.Length < 2)
+            {
+                Console.Error.WriteLine("Invalid command line.");
+                return 1;
+            }
+
+            var levelImporter = new Level.LevelImporter
+            {
+                Debug = args.Any(a => a == "-debug")
+            };
+
+            var outputDirPath = Path.GetFullPath(args[1]);
+
+            if (string.IsNullOrEmpty(outputDirPath))
+            {
+                Console.Error.WriteLine("Output path must be specified");
+                return 1;
+            }
+
+            var inputFiles = GetFileList(args, 2);
+
+            if (inputFiles.Count == 0)
+            {
+                Console.Error.WriteLine("No input files specified");
+                return 1;
+            }
+
+            if (inputFiles.Count > 1)
+            {
+                Console.Error.WriteLine("Too many input files specified, only one level can be created at a time");
+                return 1;
+            }
+
+            levelImporter.Import(inputFiles[0], outputDirPath);
+            return 0;
+        }
+
+        private static List<string> GetFileList(string[] args, int startIndex)
+        {
+            var fileSet = new Set<string>(StringComparer.OrdinalIgnoreCase);
+            var fileList = new List<string>();
+
+            foreach (var arg in args.Skip(startIndex))
+            {
+                if (arg[0] == '-')
+                    continue;
+
+                string dirPath = Path.GetDirectoryName(arg);
+                string fileSpec = Path.GetFileName(arg);
+
+                if (string.IsNullOrEmpty(dirPath))
+                    dirPath = Directory.GetCurrentDirectory();
+                else
+                    dirPath = Path.GetFullPath(dirPath);
+
+                if (Directory.Exists(dirPath))
+                {
+                    foreach (string filePath in Directory.GetFiles(dirPath, fileSpec))
+                    {
+                        if (fileSet.Add(filePath))
+                            fileList.Add(filePath);
+                    }
+                }
+            }
+
+            if (fileList.Count == 0)
+                throw new ArgumentException("No input files found");
+
+            return fileList;
+        }
+    }
+}
Index: /OniSplit/Properties/AssemblyInfo.cs
===================================================================
--- /OniSplit/Properties/AssemblyInfo.cs	(revision 1114)
+++ /OniSplit/Properties/AssemblyInfo.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("OniSplit")]
+[assembly: AssemblyDescription("Oni game data import/export tool")]
+[assembly: AssemblyProduct("OniSplit")]
+[assembly: AssemblyCopyright("Copyright © 2007-2014 Neo")]
+
+[assembly: ComVisible(false)]
+
+[assembly: AssemblyVersion("0.9.99")]
+[assembly: AssemblyFileVersion("0.9.99")]
Index: /OniSplit/SceneExporter.cs
===================================================================
--- /OniSplit/SceneExporter.cs	(revision 1114)
+++ /OniSplit/SceneExporter.cs	(revision 1114)
@@ -0,0 +1,371 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using Oni.Motoko;
+using Oni.Physics;
+using Oni.Totoro;
+
+namespace Oni
+{
+    internal class SceneExporter
+    {
+        private readonly InstanceFileManager fileManager;
+        private readonly string outputDirPath;
+        private readonly TextureDaeWriter textureWriter;
+        private readonly GeometryDaeWriter geometryWriter;
+        private readonly BodyDaeWriter bodyWriter;
+        private string basePath;
+
+        private class SceneNode
+        {
+            public string Name;
+            public readonly List<Geometry> Geometries = new List<Geometry>();
+            public readonly List<SceneNodeAnimation> Animations = new List<SceneNodeAnimation>();
+            public readonly List<SceneNode> Nodes = new List<SceneNode>();
+            public Body Body;
+            public bool IsCamera;
+        }
+
+        private class SceneNodeAnimation
+        {
+            public int Start;
+            public ObjectAnimation ObjectAnimation;
+        }
+
+        public SceneExporter(InstanceFileManager fileManager, string outputDirPath)
+        {
+            this.fileManager = fileManager;
+            this.outputDirPath = outputDirPath;
+
+            textureWriter = new TextureDaeWriter(outputDirPath);
+            geometryWriter = new GeometryDaeWriter(textureWriter);
+            bodyWriter = new BodyDaeWriter(geometryWriter);
+        }
+
+        public void ExportScene(string sourceFilePath)
+        {
+            basePath = Path.GetDirectoryName(sourceFilePath);
+
+            var scene = new Dae.Scene();
+
+            var settings = new XmlReaderSettings
+            {
+                IgnoreWhitespace = true,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
+
+            var nodes = new List<SceneNode>();
+
+            using (var xml = XmlReader.Create(sourceFilePath, settings))
+            {
+                scene.Name = xml.GetAttribute("Name");
+                xml.ReadStartElement("Scene");
+
+                while (xml.IsStartElement())
+                    nodes.Add(ReadNode(xml));
+
+                xml.ReadEndElement();
+            }
+
+            foreach (var node in nodes)
+            {
+                scene.Nodes.Add(WriteNode(node, null));
+            }
+
+            Dae.Writer.WriteFile(Path.Combine(outputDirPath, Path.GetFileNameWithoutExtension(sourceFilePath)) + ".dae", scene);
+        }
+
+        private string ResolvePath(string path)
+        {
+            return Path.Combine(basePath, path);
+        }
+
+
+        private SceneNode ReadNode(XmlReader xml)
+        {
+            var node = new SceneNode
+            {
+                Name = xml.GetAttribute("Name")
+            };
+
+            xml.ReadStartElement("Node");
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Geometry":
+                        ReadGeometry(xml, node);
+                        break;
+                    case "Body":
+                        ReadBody(xml, node);
+                        break;
+                    case "Camera":
+                        ReadCamera(xml, node);
+                        break;
+                    case "Animation":
+                        ReadAnimation(xml, node);
+                        break;
+                    case "Node":
+                        node.Nodes.Add(ReadNode(xml));
+                        break;
+                    default:
+                        Console.WriteLine("Unknown element name {0}", xml.LocalName);
+                        xml.Skip();
+                        break;
+                }
+            }
+
+            xml.ReadEndElement();
+
+            return node;
+        }
+
+        private void ReadGeometry(XmlReader xml, SceneNode node)
+        {
+            var file = fileManager.OpenFile(ResolvePath(xml.ReadElementContentAsString()));
+            var geometry = GeometryDatReader.Read(file.Descriptors[0]);
+
+            node.Geometries.Add(geometry);
+        }
+
+        private void ReadBody(XmlReader xml, SceneNode node)
+        {
+            var file = fileManager.OpenFile(ResolvePath(xml.ReadElementContentAsString()));
+            var body = BodyDatReader.Read(file.Descriptors[0]);
+
+            node.Body = body;
+
+            ReadBodyNode(node, body.Root);
+        }
+
+        private static void ReadBodyNode(SceneNode node, BodyNode bodyNode)
+        {
+            node.Name = bodyNode.Name;
+            node.Geometries.Add(bodyNode.Geometry);
+
+            foreach (var childBodyNode in bodyNode.Nodes)
+            {
+                var childNode = new SceneNode();
+                node.Nodes.Add(childNode);
+                ReadBodyNode(childNode, childBodyNode);
+            }
+        }
+
+        private void ReadAnimation(XmlReader xml, SceneNode node)
+        {
+            var startValue = xml.GetAttribute("Start");
+            var isMax = xml.GetAttribute("Type") == "Max";
+            var noRotation = xml.GetAttribute("NoRotation") == "true";
+            var filePath = xml.ReadElementContentAsString();
+
+            var start = string.IsNullOrEmpty(startValue) ? 0 : int.Parse(startValue);
+            var file = fileManager.OpenFile(ResolvePath(filePath));
+
+            if (node.Body != null)
+            {
+                var animations = AnimationDatReader.Read(file.Descriptors[0]).ToObjectAnimation(node.Body);
+
+                ReadBodyAnimation(start, node, node.Body.Root, animations);
+            }
+            else
+            {
+                node.Animations.Add(new SceneNodeAnimation
+                {
+                    Start = start,
+                    ObjectAnimation = ObjectDatReader.ReadAnimation(file.Descriptors[0])
+                });
+
+                if (noRotation)
+                {
+                    foreach (var key in node.Animations.Last().ObjectAnimation.Keys)
+                        key.Rotation = Quaternion.Identity;
+                }
+                else if (isMax)
+                {
+                    foreach (var key in node.Animations.Last().ObjectAnimation.Keys)
+                        key.Rotation *= Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathHelper.HalfPi);
+                }
+            }
+        }
+
+        private void ReadBodyAnimation(int start, SceneNode node, BodyNode bodyNode, ObjectAnimation[] animations)
+        {
+            node.Animations.Add(new SceneNodeAnimation
+            {
+                Start = start,
+                ObjectAnimation = animations[bodyNode.Index]
+            });
+
+            for (int i = 0; i < node.Nodes.Count; i++)
+                ReadBodyAnimation(start, node.Nodes[i], bodyNode.Nodes[i], animations);
+        }
+
+        private void ReadCamera(XmlReader xml, SceneNode node)
+        {
+            node.IsCamera = true;
+
+            xml.Skip();
+        }
+
+
+        private Dae.Node WriteNode(SceneNode node, List<ObjectAnimationKey> parentFrames)
+        {
+            var daeNode = new Dae.Node
+            {
+                Name = node.Name
+            };
+
+            foreach (var geometry in node.Geometries)
+                daeNode.Instances.Add(geometryWriter.WriteGeometryInstance(geometry, geometry.Name));
+
+            if (node.IsCamera)
+                WriteCamera(daeNode);
+
+            List<ObjectAnimationKey> frames = null;
+
+            if (node.Animations.Count > 0)
+            {
+                frames = BuildFrames(node);
+                WriteAnimation(daeNode, BuildLocalFrames(node.Body == null ? parentFrames : null, frames));
+            }
+
+            foreach (var child in node.Nodes)
+                daeNode.Nodes.Add(WriteNode(child, frames));
+
+            return daeNode;
+        }
+
+        private static List<ObjectAnimationKey> BuildFrames(SceneNode node)
+        {
+            var frames = new List<ObjectAnimationKey>();
+
+            foreach (var animation in node.Animations)
+            {
+                var animFrames = animation.ObjectAnimation.Interpolate();
+                var start = animation.Start;
+
+                if (frames.Count > 0)
+                    start += frames.Last().Time + 1;
+
+                foreach (var frame in animFrames)
+                    frame.Time += start;
+
+                if (frames.Count > 0)
+                {
+                    while (frames.Last().Time >= animFrames.First().Time)
+                    {
+                        frames.RemoveAt(frames.Count - 1);
+                    }
+
+                    while (frames.Last().Time + 1 < animFrames.First().Time)
+                    {
+                        frames.Add(new ObjectAnimationKey
+                        {
+                            Time = frames.Last().Time + 1,
+                            Rotation = frames.Last().Rotation,
+                            Translation = frames.Last().Translation,
+                            Scale = frames.Last().Scale
+                        });
+                    }
+                }
+
+                frames.AddRange(animFrames);
+            }
+
+            return frames;
+        }
+
+        private static List<ObjectAnimationKey> BuildLocalFrames(List<ObjectAnimationKey> parentFrames, List<ObjectAnimationKey> frames)
+        {
+            var localFrames = frames;
+
+            if (parentFrames != null)
+            {
+                localFrames = new List<ObjectAnimationKey>(localFrames.Count);
+
+                for (int i = 0; i < frames.Count; i++)
+                {
+                    var frame = frames[i];
+                    var parentFrame = parentFrames[i];
+
+                    localFrames.Add(new ObjectAnimationKey
+                    {
+                        Time = frame.Time,
+                        Scale = frame.Scale / parentFrame.Scale,
+                        Rotation = Quaternion.Conjugate(parentFrame.Rotation) * frame.Rotation,
+                        Translation = Vector3.Transform(frame.Translation - parentFrame.Translation, parentFrame.Rotation.Inverse()) / parentFrame.Scale
+                    });
+                }
+            }
+
+            return localFrames;
+        }
+
+        private static void WriteAnimation(Dae.Node node, List<ObjectAnimationKey> frames)
+        {
+            var times = new float[frames.Count];
+            var interpolations = new string[times.Length];
+            var positions = new Vector3[frames.Count];
+            var angles = new Vector3[frames.Count];
+
+            for (int i = 0; i < times.Length; ++i)
+                times[i] = frames[i].Time / 60.0f;
+
+            for (int i = 0; i < interpolations.Length; i++)
+                interpolations[i] = "LINEAR";
+
+            for (int i = 0; i < frames.Count; i++)
+                positions[i] = frames[i].Translation;
+
+            for (int i = 0; i < frames.Count; i++)
+                angles[i] = frames[i].Rotation.ToEulerXYZ();
+
+            var translate = node.Transforms.Translate("translate", positions[0]); ;
+            var rotateX = node.Transforms.Rotate("rotX", Vector3.UnitX, angles[0].X);
+            var rotateY = node.Transforms.Rotate("rotY", Vector3.UnitY, angles[0].Y);
+            var rotateZ = node.Transforms.Rotate("rotZ", Vector3.UnitZ, angles[0].Z);
+            var scale = node.Transforms.Scale("scale", frames[0].Scale);
+
+            WriteSampler(times, interpolations, i => positions[i].X, translate, "X");
+            WriteSampler(times, interpolations, i => positions[i].Y, translate, "Y");
+            WriteSampler(times, interpolations, i => positions[i].Z, translate, "Z");
+            WriteSampler(times, interpolations, i => angles[i].X, rotateX, "ANGLE");
+            WriteSampler(times, interpolations, i => angles[i].Y, rotateY, "ANGLE");
+            WriteSampler(times, interpolations, i => angles[i].Z, rotateZ, "ANGLE");
+        }
+
+        private static void WriteSampler(float[] times, string[] interpolations, Func<int, float> getValue, Dae.Transform transform, string targetName)
+        {
+            var values = new float[times.Length];
+
+            for (int i = 0; i < values.Length; ++i)
+                values[i] = getValue(i);
+
+            transform.BindAnimation(targetName, new Dae.Sampler
+            {
+                Inputs = {
+                    new Dae.Input(Dae.Semantic.Input, new Dae.Source(times, 1)),
+                    new Dae.Input(Dae.Semantic.Output, new Dae.Source(values, 1)),
+                    new Dae.Input(Dae.Semantic.Interpolation, new Dae.Source(interpolations, 1))
+                }
+            });
+        }
+
+        private static void WriteCamera(Dae.Node daeNode)
+        {
+            daeNode.Instances.Add(new Dae.CameraInstance
+            {
+                Target = new Dae.Camera
+                {
+                    XFov = 45.0f,
+                    AspectRatio = 4.0f / 3.0f,
+                    ZNear = 1.0f,
+                    ZFar = 10000.0f
+                }
+            });
+        }
+    }
+}
Index: /OniSplit/Sound/AifExporter.cs
===================================================================
--- /OniSplit/Sound/AifExporter.cs	(revision 1114)
+++ /OniSplit/Sound/AifExporter.cs	(revision 1114)
@@ -0,0 +1,63 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Sound
+{
+    internal class AifExporter : SoundExporter
+    {
+        #region Private data
+        private const int fcc_FORM = 0x464f524d;
+        private const int fcc_AIFC = 0x41494643;
+        private const int fcc_COMM = 0x434f4d4d;
+        private const int fcc_ima4 = 0x696d6134;
+        private const int fcc_SSND = 0x53534e44;
+
+        private static readonly byte[] sampleRate = new byte[10]
+        {
+            0x40, 0x0d, 0xac, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+        };
+
+        #endregion
+
+        public AifExporter(InstanceFileManager fileManager, string outputDirPath)
+            : base(fileManager, outputDirPath)
+        {
+        }
+
+        protected override void ExportInstance(InstanceDescriptor descriptor)
+        {
+            var sound = SoundData.Read(descriptor);
+
+            using (var stream = File.Create(Path.Combine(OutputDirPath, descriptor.FullName + ".aif")))
+            using (var writer = new BinaryWriter(stream))
+            {
+                writer.Write(Utils.ByteSwap(fcc_FORM));
+                writer.Write(Utils.ByteSwap(50 + sound.Data.Length));
+                writer.Write(Utils.ByteSwap(fcc_AIFC));
+
+                //
+                // write COMM chunk
+                //
+
+                writer.Write(Utils.ByteSwap(fcc_COMM));
+                writer.Write(Utils.ByteSwap(22));                   // chunk size
+                writer.Write(Utils.ByteSwap((short)sound.ChannelCount));
+                writer.Write(Utils.ByteSwap(sound.Data.Length / (sound.ChannelCount * 34))); // numSampleFrames
+                writer.Write(Utils.ByteSwap((short)16));            // sampleSize
+                writer.Write(sampleRate);                           // sampleRate
+                writer.Write(Utils.ByteSwap(fcc_ima4));
+
+                //
+                // write SSND chunk
+                //
+
+                writer.Write(Utils.ByteSwap(fcc_SSND));
+                writer.Write(Utils.ByteSwap(8 + sound.Data.Length)); // chunk size
+                writer.Write(0);
+                writer.Write(0);
+                writer.Write(sound.Data);
+            }
+        }
+
+    }
+}
Index: /OniSplit/Sound/AifFile.cs
===================================================================
--- /OniSplit/Sound/AifFile.cs	(revision 1114)
+++ /OniSplit/Sound/AifFile.cs	(revision 1114)
@@ -0,0 +1,74 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Sound
+{
+    internal class AifFile
+    {
+        #region Private data
+        private const int fcc_FORM = 0x464f524d;
+        private const int fcc_AIFC = 0x41494643;
+        private const int fcc_COMM = 0x434f4d4d;
+        private const int fcc_SSND = 0x53534e44;
+
+        private int channelCount;
+        private int numSampleFrames;
+        private int sampleSize;
+        private byte[] sampleRate;
+        private int format;
+        private byte[] soundData;
+        #endregion
+
+        public static AifFile FromFile(string filePath)
+        {
+            using (var reader = new BinaryReader(filePath, true))
+            {
+                var header = new AifFile();
+
+                if (reader.ReadInt32() != fcc_FORM)
+                    throw new InvalidDataException("Not an AIF file");
+
+                int size = reader.ReadInt32();
+
+                if (reader.ReadInt32() != fcc_AIFC)
+                    throw new InvalidDataException("Not a compressed AIF file");
+
+                for (int chunkType, chunkSize, chunkStart; reader.Position < size; reader.Position = chunkStart + chunkSize)
+                {
+                    chunkType = reader.ReadInt32();
+                    chunkSize = reader.ReadInt32();
+                    chunkStart = reader.Position;
+
+                    if (chunkType == fcc_COMM)
+                        header.ReadFormatChunk(reader, chunkSize);
+                    else if (chunkType == fcc_SSND)
+                        header.ReadDataChunk(reader, chunkSize);
+                }
+
+                return header;
+            }
+        }
+
+        private void ReadFormatChunk(BinaryReader reader, int chunkSize)
+        {
+            channelCount = reader.ReadInt16();
+            numSampleFrames = reader.ReadInt32();
+            sampleSize = reader.ReadInt16();
+            sampleRate = reader.ReadBytes(10);
+            format = reader.ReadInt32();
+        }
+
+        private void ReadDataChunk(BinaryReader reader, int chunkSize)
+        {
+            reader.Position += 8;
+            soundData = reader.ReadBytes(chunkSize - 8);
+        }
+
+        public int ChannelCount => channelCount;
+        public int SampleFrames => numSampleFrames;
+        public int SampleSize => sampleSize;
+        public byte[] SampleRate => sampleRate;
+        public int Format => format;
+        public byte[] SoundData => soundData;
+    }
+}
Index: /OniSplit/Sound/AifImporter.cs
===================================================================
--- /OniSplit/Sound/AifImporter.cs	(revision 1114)
+++ /OniSplit/Sound/AifImporter.cs	(revision 1114)
@@ -0,0 +1,58 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Sound
+{
+	internal class AifImporter : Importer
+	{
+		private const int fcc_ima4 = 0x696d6134;
+		private static readonly byte[] sampleRate = new byte[10] { 0x40, 0x0d, 0xac, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+
+		public AifImporter()
+			: base(InstanceFileHeader.OniMacTemplateChecksum)
+		{
+		}
+
+		public override void Import(string filePath, string outputDirPath)
+		{
+			var aif = AifFile.FromFile(filePath);
+
+			if (aif.Format != fcc_ima4)
+			{
+				Console.Error.WriteLine("Unsupported AIF compression (0x{0:X})", aif.Format);
+				return;
+			}
+
+			if (!Utils.ArrayEquals(aif.SampleRate, sampleRate))
+			{
+				Console.Error.WriteLine("Unsupported sample rate");
+				return;
+			}
+
+			if (aif.ChannelCount != 1 && aif.ChannelCount != 2)
+			{
+				Console.Error.WriteLine("Unsupported number of channels ({0})", aif.ChannelCount);
+				return;
+			}
+
+			BeginImport();
+			WriteSNDD(Path.GetFileNameWithoutExtension(filePath), aif);
+			Write(outputDirPath);
+		}
+
+		private void WriteSNDD(string name, AifFile aif)
+		{
+            int duration = (int)(aif.SampleFrames * 64.0f / 22050.0f * 60.0f);
+
+            var sndd = CreateInstance(TemplateTag.SNDD, name);
+
+            using (var writer = sndd.OpenWrite())
+            {
+                writer.Write((aif.ChannelCount == 1) ? 1 : 3);
+                writer.Write(duration);
+                writer.Write(aif.SoundData.Length);
+                writer.Write(WriteRawPart(aif.SoundData));
+            }
+		}
+	}
+}
Index: /OniSplit/Sound/OsbdXmlExporter.cs
===================================================================
--- /OniSplit/Sound/OsbdXmlExporter.cs	(revision 1114)
+++ /OniSplit/Sound/OsbdXmlExporter.cs	(revision 1114)
@@ -0,0 +1,149 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Sound
+{
+    internal sealed class OsbdXmlExporter : RawXmlExporter
+    {
+        private OsbdXmlExporter(BinaryReader reader, XmlWriter xml)
+            : base(reader, xml)
+        {
+        }
+
+        public static void Export(BinaryReader reader, XmlWriter xml)
+        {
+            var exporter = new OsbdXmlExporter(reader, xml);
+            exporter.Export();
+        }
+
+        private void Export()
+        {
+            int tag = Reader.ReadInt32();
+            int size = Reader.ReadInt32();
+            int version = Reader.ReadInt32();
+
+            if (version > 6)
+                throw new NotSupportedException("Sound version {0} is not supported");
+
+            switch (tag)
+            {
+                case SoundMetadata.OSAm:
+                    Xml.WriteStartElement("AmbientSound");
+                    ExportAmbient(version);
+                    break;
+
+                case SoundMetadata.OSGr:
+                    Xml.WriteStartElement("SoundGroup");
+                    ExportGroup(version);
+                    break;
+
+                case SoundMetadata.OSIm:
+                    Xml.WriteStartElement("ImpulseSound");
+                    ExportImpulse(version);
+                    break;
+
+                default:
+                    throw new NotSupportedException(string.Format("Unknown sound binary data tag {0:X}", tag));
+            }
+
+            Xml.WriteEndElement();
+        }
+
+        private void ExportGroup(int version)
+        {
+            if (version < 6)
+            {
+                float volume = 1.0f;
+                float pitch = 1.0f;
+                var flags = SoundMetadata.OSGrFlags.None;
+                int channelCount;
+                int permutationCount;
+
+                if (version >= 2)
+                    volume = Reader.ReadSingle();
+
+                if (version >= 3)
+                    pitch = Reader.ReadSingle();
+
+                channelCount = Reader.ReadInt32();
+                permutationCount = Reader.ReadInt32();
+
+                if (permutationCount >= 4)
+                    flags |= SoundMetadata.OSGrFlags.PreventRepeat;
+
+                Xml.WriteElementString("Volume", XmlConvert.ToString(volume));
+                Xml.WriteElementString("Pitch", XmlConvert.ToString(pitch));
+                Xml.WriteStartElement("Flags");
+                Xml.WriteString(MetaEnum.ToString(flags));
+                Xml.WriteEndElement();
+                Xml.WriteElementString("NumberOfChannels", XmlConvert.ToString(channelCount));
+                Xml.WriteStartElement("Permutations");
+
+                for (int i = 0; i < permutationCount; i++)
+                {
+                    Xml.WriteStartElement("Permutation");
+                    SoundMetadata.osgrPermutation.Accept(this);
+                    Xml.WriteEndElement();
+                }
+
+                Xml.WriteEndElement();
+            }
+            else
+            {
+                SoundMetadata.osgr6.Accept(this);
+            }
+        }
+
+        private void ExportAmbient(int version)
+        {
+            if (version <= 4)
+            {
+                SoundMetadata.osam4.Accept(this);
+                Xml.WriteElementString("Treshold", "3");
+                Xml.WriteElementString("MinOcclusion", "0");
+            }
+            else if (version <= 5)
+            {
+                SoundMetadata.osam5.Accept(this);
+                Xml.WriteElementString("MinOcclusion", "0");
+            }
+            else
+            {
+                SoundMetadata.osam6.Accept(this);
+            }
+        }
+
+        private void ExportImpulse(int version)
+        {
+            if (version <= 3)
+            {
+                SoundMetadata.osim3.Accept(this);
+                Xml.WriteStartElement("AlternateImpulse");
+                Xml.WriteElementString("Treshold", "0");
+                Xml.WriteStartElement("Impulse");
+                Xml.WriteString("");
+                Xml.WriteEndElement();
+                Xml.WriteEndElement();
+                Xml.WriteElementString("ImpactVelocity", "0");
+                Xml.WriteElementString("MinOcclusion", "0");
+            }
+            else if (version <= 4)
+            {
+                SoundMetadata.osim4.Accept(this);
+                Xml.WriteElementString("ImpactVelocity", "0");
+                Xml.WriteElementString("MinOcclusion", "0");
+            }
+            else if (version <= 5)
+            {
+                SoundMetadata.osim5.Accept(this);
+                Xml.WriteElementString("MinOcclusion", "0");
+            }
+            else
+            {
+                SoundMetadata.osim6.Accept(this);
+            }
+        }
+    }
+}
Index: /OniSplit/Sound/OsbdXmlImporter.cs
===================================================================
--- /OniSplit/Sound/OsbdXmlImporter.cs	(revision 1114)
+++ /OniSplit/Sound/OsbdXmlImporter.cs	(revision 1114)
@@ -0,0 +1,46 @@
+﻿using System;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Sound
+{
+    internal sealed class OsbdXmlImporter : RawXmlImporter
+    {
+        public OsbdXmlImporter(XmlReader reader, BinaryWriter writer)
+            : base(reader, writer)
+        {
+        }
+
+        public void Import()
+        {
+            switch (Xml.LocalName)
+            {
+                case "AmbientSound":
+                    Import(SoundMetadata.OSAm, SoundMetadata.osam6);
+                    break;
+
+                case "ImpulseSound":
+                    Import(SoundMetadata.OSIm, SoundMetadata.osim6);
+                    break;
+
+                case "SoundGroup":
+                    Import(SoundMetadata.OSGr, SoundMetadata.osgr6);
+                    break;
+            }
+        }
+
+        private void Import(int tag, MetaStruct type)
+        {
+            Xml.ReadStartElement();
+
+            BeginStruct(0);
+
+            Writer.Write(tag);
+            Writer.Write(0);
+            Writer.Write(6);
+
+            ReadStruct(type);
+        }
+    }
+}
Index: /OniSplit/Sound/SabdXmlExporter.cs
===================================================================
--- /OniSplit/Sound/SabdXmlExporter.cs	(revision 1114)
+++ /OniSplit/Sound/SabdXmlExporter.cs	(revision 1114)
@@ -0,0 +1,124 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using Oni.Xml;
+
+namespace Oni.Sound
+{
+    internal class SabdXmlExporter : RawXmlExporter
+    {
+        private SabdXmlExporter(BinaryReader reader, XmlWriter xml)
+            : base(reader, xml)
+        {
+        }
+
+        public static void Export(BinaryReader reader, XmlWriter xml)
+        {
+            var exporter = new SabdXmlExporter(reader, xml);
+            exporter.Export();
+        }
+
+        private void Export()
+        {
+            var data = new SoundAnimationData(Reader);
+            data.Write(Xml);
+        }
+
+        private class SoundAnimationData
+        {
+            private readonly string variant;
+            private readonly List<SoundAssignment> assignments;
+
+            private enum Tag
+            {
+                SAFT = 0x54464153,
+                SAVT = 0x54564153,
+                SASA = 0x41534153
+            }
+
+            public SoundAnimationData(BinaryReader reader)
+            {
+                int dataEnd = reader.ReadInt32() + reader.Position;
+
+                int tag = reader.ReadInt32();
+
+                if ((Tag)tag != Tag.SAFT)
+                    throw new InvalidDataException(string.Format("Unknown tag {0:X} found in sound animation", tag));
+
+                int size = reader.ReadInt32();
+                int unknown = reader.ReadInt32();
+
+                tag = reader.ReadInt32();
+
+                if ((Tag)tag != Tag.SAVT)
+                    throw new InvalidDataException(string.Format("Unknown tag {0:X} found in sound animation", tag));
+
+                size = reader.ReadInt32();
+                variant = reader.ReadString(32);
+
+                assignments = new List<SoundAssignment>();
+
+                while (reader.Position < dataEnd)
+                {
+                    tag = reader.ReadInt32();
+
+                    if ((Tag)tag != Tag.SASA)
+                        throw new InvalidDataException(string.Format("Unknown tag {0:X} found in sound animation", tag));
+
+                    size = reader.ReadInt32();
+                    assignments.Add(new SoundAssignment(reader));
+                }
+            }
+
+            public void Write(XmlWriter xml)
+            {
+                xml.WriteStartElement("SoundAnimation");
+                xml.WriteAttributeString("Variant", variant);
+
+                foreach (var assignment in assignments)
+                    assignment.Write(xml);
+
+                xml.WriteEndElement();
+            }
+        }
+
+        private class SoundAssignment
+        {
+            private readonly int frame;
+            private readonly string modifier;
+            private readonly string type;
+            private readonly string animationName;
+            private readonly string soundName;
+
+            public SoundAssignment(BinaryReader reader)
+            {
+                frame = reader.ReadInt32();
+                modifier = reader.ReadString(32);
+                type = reader.ReadString(32);
+                animationName = reader.ReadString(32);
+                soundName = reader.ReadString(32);
+            }
+
+            public void Write(XmlWriter xml)
+            {
+                xml.WriteStartElement("Assignment");
+                xml.WriteStartElement("Target");
+
+                if (type != "Animation")
+                    xml.WriteElementString("Type", type.Replace(" ", ""));
+                else
+                    xml.WriteElementString("Animation", animationName);
+
+                if (modifier != "Any")
+                    xml.WriteElementString("Modifier", modifier.Replace(" ", ""));
+
+                xml.WriteElementString("Frame", XmlConvert.ToString(frame));
+                xml.WriteEndElement();
+
+                xml.WriteElementString("Sound", soundName);
+                xml.WriteEndElement();
+            }
+        }
+    }
+}
Index: /OniSplit/Sound/SabdXmlImporter.cs
===================================================================
--- /OniSplit/Sound/SabdXmlImporter.cs	(revision 1114)
+++ /OniSplit/Sound/SabdXmlImporter.cs	(revision 1114)
@@ -0,0 +1,166 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Particles;
+using Oni.Xml;
+
+namespace Oni.Sound
+{
+    internal class SabdXmlImporter : RawXmlImporter
+    {
+        #region Private data
+        private static readonly Dictionary<string, string> modifierMap = new Dictionary<string, string>();
+        private static readonly Dictionary<string, string> typeMap = new Dictionary<string, string>();
+        #endregion
+
+        static SabdXmlImporter()
+        {
+            var modifiers = new[] {
+                "Any", "Crouch", "Jump", "Heavy Damage", "Medium Damage", "Light Damage"
+            };
+
+            var types = new[] {
+                "Block", "Draw Weapon", "Fall", "Fly", "Getting Hit", "Holster",
+                "Kick", "Knockdown", "Land", "Jump", "Pickup", "Punch",
+                "Reload Pistol", "Reload Rifle", "Reload Stream", "Reload Superball",
+                "Reload Vandegraf", "Reload Scram Cannon", "Reload Mercury Bow", "Reload Screamer",
+                "Run", "Slide", "Stand", "Starle", "Walk", "Powerup", "Roll", "Falling Flail"
+            };
+
+            foreach (string modifier in modifiers)
+                modifierMap.Add(modifier.Replace(" ", ""), modifier);
+
+            foreach (string type in types)
+                typeMap.Add(type.Replace(" ", ""), type);
+        }
+
+        private SabdXmlImporter(XmlReader reader, BinaryWriter writer)
+            : base(reader, writer)
+        {
+        }
+
+        public static void Import(XmlReader reader, BinaryWriter writer)
+        {
+            var importer = new SabdXmlImporter(reader, writer);
+            importer.Import();
+        }
+
+        #region private class SoundAnimationData
+
+        private class SoundAnimationData
+        {
+            #region Private data
+            private string variant;
+            private List<SoundAssignment> assignments;
+            #endregion
+
+            private enum Tag
+            {
+                SAFT = 0x54464153,
+                SAVT = 0x54564153,
+                SASA = 0x41534153
+            }
+
+            public SoundAnimationData(XmlReader xml)
+            {
+                variant = xml.GetAttribute("Variant");
+
+                xml.ReadStartElement("SoundAnimation");
+                assignments = new List<SoundAssignment>();
+
+                while (xml.IsStartElement("Assignment"))
+                {
+                    xml.ReadStartElement();
+                    assignments.Add(new SoundAssignment(xml));
+                    xml.ReadEndElement();
+                }
+
+                xml.ReadEndElement();
+            }
+
+            public void Write(BinaryWriter writer)
+            {
+                writer.Write((int)Tag.SAFT);
+                writer.Write(4);
+                writer.Write(6);
+                writer.Write((int)Tag.SAVT);
+                writer.Write(32);
+                writer.Write(variant, 32);
+
+                foreach (SoundAssignment assignment in assignments)
+                {
+                    writer.Write((int)Tag.SASA);
+                    writer.Write(132);
+                    assignment.Write(writer);
+                }
+            }
+        }
+
+        #endregion
+        #region private class SoundAssignment
+
+        private class SoundAssignment
+        {
+            #region Private data
+            private int frame;
+            private string modifier;
+            private string type;
+            private string animationName;
+            private string soundName;
+            #endregion
+
+            public SoundAssignment(XmlReader xml)
+            {
+                xml.ReadStartElement("Target");
+
+                if (xml.LocalName == "Animation")
+                {
+                    type = "Animation";
+                    animationName = xml.ReadElementContentAsString("Animation", "");
+                }
+                else
+                {
+                    type = xml.ReadElementContentAsString("Type", "");
+                    animationName = string.Empty;
+
+                    if (!typeMap.TryGetValue(type, out type))
+                        throw new NotSupportedException(string.Format("Unknown assignment type '{0}' found", type));
+                }
+
+                if (xml.IsStartElement("Modifier"))
+                    modifier = xml.ReadElementContentAsString();
+                else
+                    modifier = "Any";
+
+                if (!modifierMap.TryGetValue(modifier, out modifier))
+                    throw new NotSupportedException(string.Format("Unknown assignment modifier '{0}' found", modifier));
+
+                xml.ReadStartElement("Frame");
+                frame = xml.ReadContentAsInt();
+                xml.ReadEndElement();
+
+                xml.ReadEndElement();
+
+                soundName = xml.ReadElementContentAsString("Sound", "");
+            }
+
+            public void Write(BinaryWriter writer)
+            {
+                writer.Write(frame);
+                writer.Write(modifier, 32);
+                writer.Write(type, 32);
+                writer.Write(animationName, 32);
+                writer.Write(soundName, 32);
+            }
+        }
+
+        #endregion
+
+        private void Import()
+        {
+            var data = new SoundAnimationData(Xml);
+            data.Write(Writer);
+        }
+    }
+}
Index: /OniSplit/Sound/SoundData.cs
===================================================================
--- /OniSplit/Sound/SoundData.cs	(revision 1114)
+++ /OniSplit/Sound/SoundData.cs	(revision 1114)
@@ -0,0 +1,49 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Oni.Sound
+{
+    internal class SoundData
+    {
+        public int SampleRate;
+        public int ChannelCount;
+        public byte[] Data;
+
+        public static SoundData Read(InstanceDescriptor sndd)
+        {
+            if (sndd.Template.Tag != TemplateTag.SNDD)
+                throw new ArgumentException("descriptor");
+
+            var sound = new SoundData();
+
+            int dataSize;
+            int dataOffset;
+
+            using (var reader = sndd.OpenRead())
+            {
+                if (sndd.IsMacFile)
+                {
+                    sound.ChannelCount = (reader.ReadInt32() >> 1) + 1;
+                    sound.SampleRate = 22050;
+                    reader.Skip(4);
+                }
+                else
+                {
+                    reader.Skip(6);
+                    sound.ChannelCount = reader.ReadInt16();
+                    sound.SampleRate = reader.ReadInt32();
+                    reader.Skip(44);
+                }
+
+                dataSize = reader.ReadInt32();
+                dataOffset = reader.ReadInt32();
+            }
+
+            using (var rawReader = sndd.GetRawReader(dataOffset))
+                sound.Data = rawReader.ReadBytes(dataSize);
+
+            return sound;
+        }
+    }
+}
Index: /OniSplit/Sound/SoundExporter.cs
===================================================================
--- /OniSplit/Sound/SoundExporter.cs	(revision 1114)
+++ /OniSplit/Sound/SoundExporter.cs	(revision 1114)
@@ -0,0 +1,18 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Sound
+{
+    internal abstract class SoundExporter : Exporter
+    {
+        public SoundExporter(InstanceFileManager fileManager, string outputDirPath)
+            : base(fileManager, outputDirPath)
+        {
+        }
+
+        protected override List<InstanceDescriptor> GetSupportedDescriptors(InstanceFile file)
+        {
+            return file.GetNamedDescriptors(TemplateTag.SNDD);
+        }
+    }
+}
Index: /OniSplit/Sound/WavExporter.cs
===================================================================
--- /OniSplit/Sound/WavExporter.cs	(revision 1114)
+++ /OniSplit/Sound/WavExporter.cs	(revision 1114)
@@ -0,0 +1,63 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Sound
+{
+    internal class WavExporter : SoundExporter
+    {
+        #region Private data
+        private const int fcc_RIFF = 0x46464952;
+        private const int fcc_WAVE = 0x45564157;
+        private const int fcc_fmt = 0x20746d66;
+        private const int fcc_data = 0x61746164;
+
+        private static readonly byte[] formatTemplate = new byte[50]
+        {
+            0x02, 0, 0, 0, 0x22, 0x56, 0, 0, 0, 0, 0, 0, 0, 0x02, 0x04, 0,
+            0x20, 0, 0xf4, 0x03, 0x07, 0, 0, 0x01, 0, 0, 0, 0x02, 0, 0xff, 0, 0, 0, 0, 0xc0, 0, 0x40, 0,
+            0xf0, 0, 0, 0, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff
+        };
+
+        #endregion
+
+        public WavExporter(InstanceFileManager fileManager, string outputDirPath)
+            : base(fileManager, outputDirPath)
+        {
+        }
+
+        protected override void ExportInstance(InstanceDescriptor descriptor)
+        {
+            var sound = SoundData.Read(descriptor);
+
+            using (var stream = File.Create(Path.Combine(OutputDirPath, descriptor.FullName + ".wav")))
+            using (var writer = new BinaryWriter(stream))
+            {
+                var format = (byte[])formatTemplate.Clone();
+
+                Array.Copy(BitConverter.GetBytes(sound.ChannelCount), 0, format, 2, 2);
+                Array.Copy(BitConverter.GetBytes(sound.ChannelCount == 1 ? 11155 : 22311), 0, format, 8, 4);
+                Array.Copy(BitConverter.GetBytes(sound.ChannelCount == 1 ? 512 : 1024), 0, format, 12, 2);
+
+                writer.Write(fcc_RIFF);
+                writer.Write(8 + format.Length + 8 + sound.Data.Length);
+                writer.Write(fcc_WAVE);
+
+                //
+                // write format chunk
+                //
+
+                writer.Write(fcc_fmt);
+                writer.Write(format.Length);
+                writer.Write(format);
+
+                //
+                // write data chunk
+                //
+
+                writer.Write(fcc_data);
+                writer.Write(sound.Data.Length);
+                writer.Write(sound.Data);
+            }
+        }
+    }
+}
Index: /OniSplit/Sound/WavFile.cs
===================================================================
--- /OniSplit/Sound/WavFile.cs	(revision 1114)
+++ /OniSplit/Sound/WavFile.cs	(revision 1114)
@@ -0,0 +1,84 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Sound
+{
+    internal class WavFile
+    {
+        #region Private data
+        private const int fcc_RIFF = 0x46464952;
+        private const int fcc_WAVE = 0x45564157;
+        private const int fcc_fmt = 0x20746d66;
+        private const int fcc_data = 0x61746164;
+
+        private WavFormat format;
+        private int channelCount;
+        private int sampleRate;
+        private int averageBytesPerSecond;
+        private int blockAlign;
+        private int bitsPerSample;
+        private byte[] extraData;
+        private byte[] soundData;
+        #endregion
+
+        public static WavFile FromFile(string filePath)
+        {
+            using (var reader = new BinaryReader(filePath))
+            {
+                if (reader.ReadInt32() != fcc_RIFF)
+                    throw new InvalidDataException("Not a WAV file");
+
+                int size = reader.ReadInt32();
+
+                if (reader.ReadInt32() != fcc_WAVE)
+                    throw new InvalidDataException("Not a WAV file");
+
+                var header = new WavFile();
+
+                for (int chunkType, chunkSize, chunkStart; reader.Position < size; reader.Position = chunkStart + chunkSize)
+                {
+                    chunkType = reader.ReadInt32();
+                    chunkSize = reader.ReadInt32();
+                    chunkStart = reader.Position;
+
+                    if (chunkType == fcc_fmt)
+                        header.ReadFormatChunk(reader, chunkSize);
+                    else if (chunkType == fcc_data)
+                        header.ReadDataChunk(reader, chunkSize);
+                }
+
+                return header;
+            }
+        }
+
+        private void ReadFormatChunk(BinaryReader reader, int chunkSize)
+        {
+            format = (WavFormat)reader.ReadInt16();
+            channelCount = reader.ReadInt16();
+            sampleRate = reader.ReadInt32();
+            averageBytesPerSecond = reader.ReadInt32();
+            blockAlign = reader.ReadInt16();
+            bitsPerSample = reader.ReadInt16();
+
+            if (chunkSize > 16)
+                extraData = reader.ReadBytes(reader.ReadInt16());
+            else
+                extraData = new byte[0];
+        }
+
+        private void ReadDataChunk(BinaryReader reader, int chunkSize)
+        {
+            soundData = reader.ReadBytes(chunkSize);
+        }
+
+        public WavFormat Format => format;
+        public int ChannelCount => channelCount;
+        public int SampleRate => sampleRate;
+        public int AverageBytesPerSecond => averageBytesPerSecond;
+        public int BlockAlign => blockAlign;
+        public int BitsPerSample => bitsPerSample;
+        public byte[] ExtraData => extraData;
+        public byte[] SoundData => soundData;
+    }
+}
Index: /OniSplit/Sound/WavFormat.cs
===================================================================
--- /OniSplit/Sound/WavFormat.cs	(revision 1114)
+++ /OniSplit/Sound/WavFormat.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Sound
+{
+    internal enum WavFormat
+    {
+        Pcm = 1,
+        Adpcm = 2
+    }
+}
Index: /OniSplit/Sound/WavImporter.cs
===================================================================
--- /OniSplit/Sound/WavImporter.cs	(revision 1114)
+++ /OniSplit/Sound/WavImporter.cs	(revision 1114)
@@ -0,0 +1,64 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Sound
+{
+    internal class WavImporter : Importer
+    {
+        public override void Import(string filePath, string outputDirPath)
+        {
+            var wav = WavFile.FromFile(filePath);
+
+            if (wav.Format != WavFormat.Pcm && wav.Format != WavFormat.Adpcm)
+            {
+                Console.Error.WriteLine("Unsupported WAV format (0x{0:X})", wav.Format);
+                return;
+            }
+
+            if (wav.ChannelCount != 1 && wav.ChannelCount != 2)
+            {
+                Console.Error.WriteLine("Unsupported number of channels ({0})", wav.ChannelCount);
+                return;
+            }
+
+            if (wav.SampleRate != 22050 && wav.SampleRate != 44100)
+            {
+                Console.Error.WriteLine("Unsupported sample rate ({0} Hz)", wav.SampleRate);
+                return;
+            }
+
+            if (wav.ExtraData.Length > 32)
+                throw new NotSupportedException(string.Format("Unsupported wave format extra data size ({0})", wav.ExtraData.Length));
+
+            BeginImport();
+            WriteSNDD(Path.GetFileNameWithoutExtension(filePath), wav);
+            Write(outputDirPath);
+        }
+
+        private void WriteSNDD(string name, WavFile wav)
+        {
+            float duration = wav.SoundData.Length * 8.0f / wav.BitsPerSample;
+            duration /= wav.SampleRate;
+            duration /= wav.ChannelCount;
+            duration *= 60.0f;
+
+            var sndd = CreateInstance(TemplateTag.SNDD, name);
+
+            using (var writer = sndd.OpenWrite(8))
+            {
+                writer.Write((short)wav.Format);
+                writer.WriteInt16(wav.ChannelCount);
+                writer.Write(wav.SampleRate);
+                writer.Write(wav.AverageBytesPerSecond);
+                writer.WriteInt16(wav.BlockAlign);
+                writer.WriteInt16(wav.BitsPerSample);
+                writer.WriteInt16(wav.ExtraData.Length);
+                writer.Write(wav.ExtraData);
+                writer.Skip(32 - wav.ExtraData.Length);
+                writer.Write((short)duration);
+                writer.Write(wav.SoundData.Length);
+                writer.Write(WriteRawPart(wav.SoundData));
+            }
+        }
+    }
+}
Index: /OniSplit/SubtitleExporter.cs
===================================================================
--- /OniSplit/SubtitleExporter.cs	(revision 1114)
+++ /OniSplit/SubtitleExporter.cs	(revision 1114)
@@ -0,0 +1,79 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Oni
+{
+    internal sealed class SubtitleExporter : Exporter
+    {
+        public SubtitleExporter(InstanceFileManager fileManager, string outputDirPath)
+            : base(fileManager, outputDirPath)
+        {
+        }
+
+        protected override List<InstanceDescriptor> GetSupportedDescriptors(InstanceFile file)
+        {
+            return file.GetNamedDescriptors(TemplateTag.SUBT);
+        }
+
+        protected override void ExportInstance(InstanceDescriptor descriptor)
+        {
+            var filePath = Path.Combine(OutputDirPath, descriptor.FullName + ".txt");
+
+            int baseOffset;
+            int[] offsets;
+
+            using (var reader = descriptor.OpenRead(16))
+            {
+                baseOffset = reader.ReadInt32();
+                offsets = reader.ReadInt32Array(reader.ReadInt32());
+            }
+
+            using (var rawReader = descriptor.GetRawReader(baseOffset))
+            using (var outStream = File.Create(filePath))
+            using (var writer = new BinaryWriter(outStream))
+            {
+                int fileOffset = (int)rawReader.Position;
+                var buffer = new List<byte>();
+
+                foreach (int offset in offsets)
+                {
+                    rawReader.Position = fileOffset + offset;
+
+                    while (true)
+                    {
+                        byte b = rawReader.ReadByte();
+
+                        if (b == 0)
+                        {
+                            buffer.Add((byte)'=');
+                            break;
+                        }
+
+                        buffer.Add(b);
+                    }
+
+                    writer.Write(buffer.ToArray());
+                    buffer.Clear();
+
+                    while (true)
+                    {
+                        byte b = rawReader.ReadByte();
+
+                        if (b == 0)
+                        {
+                            buffer.AddRange(Encoding.UTF8.GetBytes(Environment.NewLine));
+                            break;
+                        }
+
+                        buffer.Add(b);
+                    }
+
+                    writer.Write(buffer.ToArray());
+                    buffer.Clear();
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/SubtitleImporter.cs
===================================================================
--- /OniSplit/SubtitleImporter.cs	(revision 1114)
+++ /OniSplit/SubtitleImporter.cs	(revision 1114)
@@ -0,0 +1,160 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Oni
+{
+    internal sealed class SubtitleImporter : Importer
+    {
+        #region Private data
+        private List<byte> subtitles;
+        private List<int> offsets;
+        #endregion
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            ReadTextFile(filePath);
+
+            string name = Path.GetFileNameWithoutExtension(filePath);
+
+            BeginImport();
+            WriteSUBT(name, subtitles.ToArray());
+            Write(outputDirPath);
+        }
+
+        private void ReadTextFile(string filePath)
+        {
+            subtitles = new List<byte>();
+            offsets = new List<int>();
+
+            var data = File.ReadAllBytes(filePath);
+            int i = SkipPreamble(data);
+
+            while (i < data.Length)
+            {
+                int nameStart = i;
+
+                while (!IsNewLine(data, i) && data[i] != '=')
+                    i++;
+
+                int nameEnd = i;
+
+                if (IsNewLine(data, i))
+                {
+                    //
+                    // This was an empty line or a line that does not contain a =. Skip it.
+                    //
+
+                    continue;
+                }
+
+                i++;
+
+                int textStart = i;
+
+                while (!IsNewLine(data, i))
+                    i++;
+
+                int textEnd = i;
+
+                if (nameEnd > nameStart)
+                {
+                    offsets.Add(subtitles.Count);
+
+                    for (int j = nameStart; j < nameEnd; j++)
+                        subtitles.Add(data[j]);
+
+                    subtitles.Add(0);
+
+                    for (int j = textStart; j < textEnd; j++)
+                        subtitles.Add(data[j]);
+
+                    subtitles.Add(0);
+                }
+
+                i = SkipNewLine(data, i);
+            }
+        }
+
+        private static int SkipPreamble(byte[] data)
+        {
+            int offset = CheckPreamble(data, Encoding.UTF8.GetPreamble());
+
+            if (offset > 0)
+                return offset;
+
+            if (CheckPreamble(data, Encoding.Unicode.GetPreamble()) != 0
+                || CheckPreamble(data, Encoding.BigEndianUnicode.GetPreamble()) != 0
+                || CheckPreamble(data, Encoding.UTF32.GetPreamble()) != 0)
+            {
+                throw new InvalidDataException("UTF16/32 input text files are not supported.");
+            }
+
+            if (data.Length >= 4)
+            {
+                if ((data[1] == 0 && data[3] == 0)
+                    || (data[0] == 0 && data[2] == 0))
+                {
+                    throw new InvalidDataException("UTF16/32 input text files are not supported.");
+                }
+            }
+
+            return 0;
+        }
+
+        private static int CheckPreamble(byte[] data, byte[] preamble)
+        {
+            if (data.Length < preamble.Length)
+                return 0;
+
+            for (int i = 0; i < preamble.Length; i++)
+            {
+                if (data[i] != preamble[i])
+                    return 0;
+            }
+
+            return preamble.Length;
+        }
+
+        private static bool IsNewLine(byte[] data, int offset)
+        {
+            if (offset >= data.Length)
+                return true;
+
+            return (SkipNewLine(data, offset) > offset);
+        }
+
+        private static int SkipNewLine(byte[] data, int offset)
+        {
+            if (offset < data.Length)
+            {
+                if (data[offset] == '\n')
+                {
+                    offset++;
+                }
+                else if (data[offset] == '\r')
+                {
+                    offset++;
+
+                    if (offset < data.Length && data[offset] == '\n')
+                        offset++;
+                }
+            }
+
+            return offset;
+        }
+
+        private void WriteSUBT(string name, byte[] subtitles)
+        {
+            var subt = CreateInstance(TemplateTag.SUBT, name);
+
+            using (var writer = subt.OpenWrite(16))
+            {
+                writer.Write(WriteRawPart(subtitles));
+                writer.Write(offsets.Count);
+                writer.Write(offsets.ToArray());
+            }
+        }
+    }
+}
Index: /OniSplit/Template.cs
===================================================================
--- /OniSplit/Template.cs	(revision 1114)
+++ /OniSplit/Template.cs	(revision 1114)
@@ -0,0 +1,27 @@
+using System;
+using Oni.Metadata;
+
+namespace Oni
+{
+    internal sealed class Template
+    {
+        private readonly TemplateTag tag;
+        private readonly string description;
+        private readonly MetaStruct type;
+        private readonly long checksum;
+
+        internal Template(TemplateTag tag, MetaStruct type, long checksum, string description)
+        {
+            this.tag = tag;
+            this.type = type;
+            this.checksum = checksum;
+            this.description = description;
+        }
+
+        public TemplateTag Tag => tag;
+        public MetaStruct Type => type;
+        public long Checksum => checksum;
+        public string Description => description;
+        public bool IsLeaf => type.IsLeaf;
+    }
+}
Index: /OniSplit/TemplateTag.cs
===================================================================
--- /OniSplit/TemplateTag.cs	(revision 1114)
+++ /OniSplit/TemplateTag.cs	(revision 1114)
@@ -0,0 +1,106 @@
+namespace Oni
+{
+    internal enum TemplateTag
+    {
+        NONE = 0x00000000,
+        AISA = 0x41495341,
+        AITR = 0x41495452,
+        AKAA = 0x414b4141,
+        ABNA = 0x41424e41,
+        AKVA = 0x414b5641,
+        AKBA = 0x414b4241,
+        AKBP = 0x414b4250,
+        AKDA = 0x414b4441,
+        AKEV = 0x414b4556,
+        AGQC = 0x41475143,
+        AGDB = 0x41474442,
+        AGQG = 0x41475147,
+        AGQR = 0x41475152,
+        AKOT = 0x414b4f54,
+        OTIT = 0x4f544954,
+        OTLF = 0x4f544c46,
+        QTNA = 0x51544e41,
+        BINA = 0x42494e41,
+        ENVP = 0x454e5650,
+        M3GM = 0x4d33474d,
+        M3GA = 0x4d334741,
+        PLEA = 0x504c4541,
+        PNTA = 0x504e5441,
+        TXCA = 0x54584341,
+        TXMP = 0x54584d50,
+        TXAN = 0x5458414e,
+        TXMA = 0x54584d41,
+        TXMB = 0x54584d42,
+        VCRA = 0x56435241,
+        Impt = 0x496d7074,
+        Mtrl = 0x4d74726c,
+        CONS = 0x434f4e53,
+        DOOR = 0x444f4f52,
+        OFGA = 0x4f464741,
+        TRIG = 0x54524947,
+        TRGE = 0x54524745,
+        TURR = 0x54555252,
+        OBAN = 0x4f42414e,
+        OBDC = 0x4f424443,
+        OBLS = 0x4f424c53,
+        OBOA = 0x4f424f41,
+        CBPI = 0x43425049,
+        CBPM = 0x4342504d,
+        ONCC = 0x4f4e4343,
+        ONIA = 0x4f4e4941,
+        ONCP = 0x4f4e4350,
+        ONCV = 0x4f4e4356,
+        CRSA = 0x43525341,
+        DPge = 0x44506765,
+        FILM = 0x46494c4d,
+        ONFA = 0x4f4e4641,
+        ONGS = 0x4f4e4753,
+        HPge = 0x48506765,
+        IGHH = 0x49474848,
+        IGPG = 0x49475047,
+        IGPA = 0x49475041,
+        IGSt = 0x49475374,
+        IGSA = 0x49475341,
+        IPge = 0x49506765,
+        KeyI = 0x4b657949,
+        ONLV = 0x4f4e4c56,
+        ONLD = 0x4f4e4c44,
+        ONMA = 0x4f4e4d41,
+        ONOA = 0x4f4e4f41,
+        OPge = 0x4f506765,
+        ONSK = 0x4f4e534b,
+        ONSA = 0x4f4e5341,
+        TxtC = 0x54787443,
+        ONTA = 0x4f4e5441,
+        ONVL = 0x4f4e564c,
+        WPge = 0x57506765,
+        OSBD = 0x4f534244,
+        PSpc = 0x50537063,
+        PSpL = 0x5053704c,
+        PSUI = 0x50535549,
+        SNDD = 0x534e4444,
+        SUBT = 0x53554254,
+        IDXA = 0x49445841,
+        TStr = 0x54537472,
+        StNA = 0x53744e41,
+        TRAS = 0x54524153,
+        TRAM = 0x5452414d,
+        TRAC = 0x54524143,
+        TRCM = 0x5452434d,
+        TRBS = 0x54524253,
+        TRMA = 0x54524d41,
+        TRGA = 0x54524741,
+        TRIA = 0x54524941,
+        TRSC = 0x54525343,
+        TRTA = 0x54525441,
+        TSFT = 0x54534654,
+        TSFF = 0x54534646,
+        TSFL = 0x5453464c,
+        TSGA = 0x54534741,
+        WMCL = 0x574d434c,
+        WMDD = 0x574d4444,
+        WMMB = 0x574d4d42,
+        WMM_ = 0x574d4d5f,
+        ONWC = 0x4f4e5743,
+    }
+}
Index: /OniSplit/Totoro/Animation.cs
===================================================================
--- /OniSplit/Totoro/Animation.cs	(revision 1114)
+++ /OniSplit/Totoro/Animation.cs	(revision 1114)
@@ -0,0 +1,291 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Physics;
+
+namespace Oni.Totoro
+{
+    internal class Animation
+    {
+        public string Name;
+        public AnimationFlags Flags;
+        public readonly string[] DirectAnimations = new string[2];
+        public float FinalRotation;
+        public Direction Direction = Direction.Forward;
+        public int Vocalization = 65535;
+        public string Impact;
+        public int HardPause;
+        public int SoftPause;
+        public AnimationType Type;
+        public AnimationType AimingType;
+        public AnimationState FromState;
+        public AnimationState ToState;
+        public AnimationVarient Varient;
+        public int ActionFrame = 65535;
+        public int FirstLevelAvailable;
+        public BoneMask OverlayUsedBones;
+        public BoneMask OverlayReplacedBones;
+        public int AtomicStart;
+        public int AtomicEnd;
+        public int InvulnerableStart;
+        public int InvulnerableEnd;
+        public int InterpolationMax;
+        public int InterpolationEnd;
+        public int FrameSize;
+
+        public readonly List<float> Heights = new List<float>();
+        public readonly List<Vector2> Velocities = new List<Vector2>();
+        public readonly List<List<KeyFrame>> Rotations = new List<List<KeyFrame>>();
+        public readonly List<Shortcut> Shortcuts = new List<Shortcut>();
+        public readonly List<Position> Positions = new List<Position>();
+        public readonly List<Damage> SelfDamage = new List<Damage>();
+        public ThrowInfo ThrowSource;
+        public readonly List<Sound> Sounds = new List<Sound>();
+        public readonly List<Footstep> Footsteps = new List<Footstep>();
+        public readonly List<Particle> Particles = new List<Particle>();
+        public readonly List<MotionBlur> MotionBlur = new List<MotionBlur>();
+        public readonly List<Attack> Attacks = new List<Attack>();
+        public readonly float[] AttackRing = new float[36];
+        public readonly List<List<Vector3>> AllPoints = new List<List<Vector3>>();
+
+        public void ValidateFrames()
+        {
+            var error = Console.Error;
+            int frameCount = Heights.Count;
+
+            foreach (var sound in Sounds.FindAll(s => s.Start >= frameCount))
+            {
+                error.WriteLine("Warning: sound start {0} is beyond the last animation frame", sound.Start);
+                Sounds.Remove(sound);
+            }
+
+            foreach (var footstep in Footsteps.FindAll(f => f.Frame >= frameCount))
+            {
+                error.WriteLine("Warning: footstep frame {0} is beyond the last animation frame", footstep.Frame);
+                Footsteps.Remove(footstep);
+            }
+
+            foreach (var damage in SelfDamage.FindAll(d => d.Frame > frameCount))
+            {
+                error.WriteLine("Warning: damage frame {0} is beyond the last animation frame", damage.Frame);
+                SelfDamage.Remove(damage);
+            }
+
+            foreach (var attack in Attacks.FindAll(a => a.Start >= frameCount))
+            {
+                error.WriteLine("Warning: attack start frame {0} is beyond the last animation frame", attack.Start);
+                Attacks.Remove(attack);
+            }
+
+            foreach (var particle in Particles.FindAll(p => p.Start >= frameCount))
+            {
+                error.WriteLine("Warning: particle start frame {0} is beyond the last animation frame", particle.Start);
+                Particles.Remove(particle);
+            }
+        }
+
+        public void ComputeExtents(Body body)
+        {
+            Positions.Clear();
+            AllPoints.Clear();
+
+            int frameCount = Heights.Count;
+            int boneCount = Rotations.Count;
+
+            var rotations = new Quaternion[frameCount, boneCount];
+
+            //
+            // Compute the quaternions for each bone and frame
+            //
+
+            for (int bone = 0; bone < boneCount; bone++)
+            {
+                var keys = Rotations[bone];
+
+                for (int frame = 0; frame < keys.Count; frame++)
+                    rotations[frame, bone] = new Quaternion(keys[frame].Rotation);
+            }
+
+            var transforms = new Matrix[boneCount];
+            var offset = Vector2.Zero;
+
+            for (int frame = 0; frame < frameCount; frame++)
+            {
+                //
+                // Create transforms
+                //
+
+                for (int bone = 0; bone < boneCount; bone++)
+                {
+                    transforms[bone] = Matrix.CreateFromQuaternion(rotations[frame, bone]);
+                    transforms[bone].Translation = body.Nodes[bone].Translation;
+                }
+
+                //
+                // Propagate transforms through the hierarchy
+                //
+
+                PropagateTransforms(body.Root, transforms);
+
+                //
+                // Apply the root translation
+                //
+
+                for (int bone = 0; bone < boneCount; bone++)
+                {
+                    transforms[bone] *= Matrix.CreateTranslation(offset.X, Heights[frame], offset.Y);
+                }
+
+                //
+                // Compute the vertical extent for this frame
+                //
+
+                var minY = 1e09f;
+                var maxY = -1e09f;
+                var allFramePoints = new List<Vector3>(8 * boneCount);
+
+                for (int bone = 0; bone < boneCount; bone++)
+                {
+                    var points = body.Nodes[bone].Geometry.Points;
+                    var box = BoundingBox.CreateFromPoints(points);
+                    var sphere = BoundingSphere.CreateFromBoundingBox(box);
+                    var worldCorners = Vector3.Transform(box.GetCorners(), ref transforms[bone]);
+                    var worldCenter = Vector3.Transform(sphere.Center, ref transforms[bone]);
+
+                    minY = Math.Min(minY, worldCenter.Y - sphere.Radius);
+                    maxY = Math.Max(maxY, worldCenter.Y + sphere.Radius);
+                    allFramePoints.AddRange(worldCorners);
+                }
+
+                Positions.Add(new Position {
+                    Height = maxY - minY,
+                    YOffset = minY,
+                    X = offset.X,
+                    Z = offset.Y
+                });
+
+                AllPoints.Add(allFramePoints);
+
+                offset += Velocities[frame];
+            }
+        }
+
+        private static void PropagateTransforms(BodyNode bodyNode, Matrix[] transforms)
+        {
+            foreach (var child in bodyNode.Nodes)
+            {
+                transforms[child.Index] *= transforms[bodyNode.Index];
+                PropagateTransforms(child, transforms);
+            }
+        }
+
+        public ObjectAnimation[] ToObjectAnimation(Body body)
+        {
+            var anims = new ObjectAnimation[body.Nodes.Count];
+
+            foreach (var node in body.Nodes)
+            {
+                anims[node.Index] = new ObjectAnimation {
+                    Name = Name + "_" + node.Name,
+                    Length = Heights.Count,
+                };
+            }
+
+            FillObjectAnimationFrames(anims, body.Root, null);
+
+            return anims;
+        }
+
+        private void FillObjectAnimationFrames(ObjectAnimation[] anims, BodyNode node, BodyNode parentNode)
+        {
+            var frames = new ObjectAnimationKey[Velocities.Count];
+
+            //
+            // Scale is always 1. Frame length is always 1 too.
+            //
+
+            for (int i = 0; i < frames.Length; i++)
+            {
+                frames[i] = new ObjectAnimationKey {
+                    Time = i,
+                    Scale = Vector3.One
+                };
+            }
+
+            //
+            // Transform key frames to quaternions.
+            //
+
+            var keys = Rotations[node.Index];
+            var quats = new Quaternion[keys.Count];
+            var isCompressed = FrameSize == 6;
+
+            for (int k = 0; k < keys.Count; k++)
+            {
+                var key = keys[k];
+
+                if (isCompressed)
+                {
+                    quats[k] = Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathHelper.ToRadians(key.Rotation.X))
+                             * Quaternion.CreateFromAxisAngle(Vector3.UnitY, MathHelper.ToRadians(key.Rotation.Y))
+                             * Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(key.Rotation.Z));
+                }
+                else
+                {
+                    quats[k] = new Quaternion(key.Rotation);
+                }
+            }
+
+            //
+            // Interpolate the quaternions.
+            //
+
+            int frame = 0;
+
+            for (int k = 0; k < keys.Count; k++)
+            {
+                var duration = keys[k].Duration;
+
+                var q1 = quats[k];
+                var q2 = (k == keys.Count - 1) ? quats[k] : quats[k + 1];
+
+                for (int t = 0; t < duration; t++)
+                    frames[frame++].Rotation = Quaternion.Lerp(q1, q2, (float)t / (float)duration);
+            }
+
+            //
+            // Build translation and merge with parent anim.
+            //
+
+            if (parentNode == null)
+            {
+                Vector2 offset = Vector2.Zero;
+
+                for (int i = 0; i < frames.Length; i++)
+                {
+                    //frames[i].Translation = new Vector3(offset.X, 0.0f, offset.Y);
+                    offset += Velocities[i];
+                }
+            }
+            else
+            {
+                for (int i = 0; i < frames.Length; i++)
+                {
+                    frames[i].Translation = node.Translation;
+                }
+
+                var parentFrames = anims[parentNode.Index].Keys;
+
+                for (int i = 0; i < frames.Length; i++)
+                {
+                    frames[i].Rotation = parentFrames[i].Rotation * frames[i].Rotation;
+                    frames[i].Translation = parentFrames[i].Translation + Vector3.Transform(frames[i].Translation, parentFrames[i].Rotation);
+                }
+            }
+
+            anims[node.Index].Keys = frames;
+
+            foreach (var child in node.Nodes)
+                FillObjectAnimationFrames(anims, child, node);
+        }
+    }
+}
Index: /OniSplit/Totoro/AnimationDaeReader.cs
===================================================================
--- /OniSplit/Totoro/AnimationDaeReader.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationDaeReader.cs	(revision 1114)
@@ -0,0 +1,168 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Totoro
+{
+    internal class AnimationDaeReader
+    {
+        private Animation animation;
+        private Dae.Scene scene;
+        private int startFrame;
+        private int endFrame;
+        private Body body;
+        private int frameCount;
+
+        public void Read(Animation targetAnimation)
+        {
+            animation = targetAnimation;
+
+            body = BodyDaeReader.Read(scene);
+
+            ComputeFrameCount();
+            ImportTranslation();
+            ImportRotations();
+            animation.ComputeExtents(body);
+        }
+
+        public Dae.Scene Scene
+        {
+            get { return scene; }
+            set { scene = value; }
+        }
+
+        public int StartFrame
+        {
+            get { return startFrame; }
+            set { startFrame = value; }
+        }
+
+        public int EndFrame
+        {
+            get { return endFrame; }
+            set { endFrame = value; }
+        }
+
+        private void ComputeFrameCount()
+        {
+            float maxTime = float.MinValue;
+
+            var inputs = body.Nodes
+                .SelectMany(n => n.DaeNode.Transforms).Where(t => t.HasAnimations)
+                .SelectMany(t => t.Animations).Where(a => a != null)
+                .SelectMany(a => a.Inputs).Where(i => i.Semantic == Dae.Semantic.Input);
+
+            foreach (var input in inputs)
+                maxTime = Math.Max(maxTime, input.Source.FloatData.Max());
+
+            float maxFrameF = maxTime * 60.0f;
+            int maxFrame;
+
+            if (maxFrameF - Math.Round(maxFrameF) < 0.0005)
+                maxFrame = FMath.RoundToInt32(maxFrameF);
+            else
+                maxFrame = FMath.TruncateToInt32(maxFrameF);
+
+            //Console.Error.WriteLine("Info: The last keyframe time is {0}s. The animation length is {1} (at 60fps).",
+            //    maxTime, maxFrame + 1);
+
+            if (endFrame == 0)
+            {
+                endFrame = maxFrame;
+            }
+            else if (endFrame > maxFrame)
+            {
+                Console.Error.WriteLine("Warning: the specified animation end frame ({0}) is beyond the last key frame ({1}), using the last frame instead", endFrame, maxFrame);
+                endFrame = maxFrame;
+            }
+
+            if (startFrame >= maxFrame)
+            {
+                Console.Error.WriteLine("Warning: the specified animation start frame ({0}) is beyond the last key frame ({1}), using 0 instead", startFrame, maxFrame);
+                startFrame = 0;
+            }
+
+            frameCount = endFrame - startFrame;
+        }
+
+        private void ImportTranslation()
+        {
+            var rootNode = body.Nodes[0].DaeNode;
+            var translate = rootNode.Transforms[0] as Dae.TransformTranslate;
+
+            if (translate == null)
+            {
+                animation.Heights.AddRange(Enumerable.Repeat(0.0f, frameCount));
+                animation.Velocities.AddRange(Enumerable.Repeat(Vector2.Zero, frameCount));
+            }
+            else
+            {
+                animation.Heights.AddRange(Sample(translate, 1, endFrame - 1));
+
+                var x = Sample(translate, 0, endFrame);
+                var z = Sample(translate, 2, endFrame);
+
+                for (int i = 1; i < x.Length; i++)
+                    animation.Velocities.Add(new Vector2(x[i] - x[i - 1], z[i] - z[i - 1]));
+            }
+        }
+
+        private void ImportRotations()
+        {
+            animation.FrameSize = 16;
+
+            foreach (var node in body.Nodes.Select(n => n.DaeNode))
+            {
+                var keys = new List<KeyFrame>();
+                animation.Rotations.Add(keys);
+
+                var rotations = new List<Dae.TransformRotate>();
+                var angles = new List<float[]>();
+
+                foreach (var transform in node.Transforms)
+                {
+                    var rotate = transform as Dae.TransformRotate;
+
+                    if (rotate != null)
+                    {
+                        rotations.Add(rotate);
+                        angles.Add(Sample(rotate, 3, endFrame - 1));
+                    }
+                }
+
+                for (int i = 0; i < frameCount; i++)
+                {
+                    var q = Quaternion.Identity;
+
+                    for (int j = 0; j < rotations.Count; j++)
+                        q *= Quaternion.CreateFromAxisAngle(rotations[j].Axis, MathHelper.ToRadians(angles[j][i]));
+
+                    keys.Add(new KeyFrame {
+                        Duration = 1,
+                        Rotation = q.ToVector4()
+                    });
+                }
+            }
+        }
+
+        private float[] Sample(Dae.Transform transform, int index, int endFrame)
+        {
+            Dae.Sampler sampler = null;
+
+            if (transform.HasAnimations)
+                sampler = transform.Animations[index];
+
+            if (sampler == null)
+            {
+                var value = transform.Values[index];
+                var values = new float[endFrame - startFrame + 1];
+
+                for (int i = 0; i < values.Length; i++)
+                    values[i] = value;
+
+                return values;
+            }
+
+            return sampler.Sample(startFrame, endFrame);
+        }
+    }
+}
Index: /OniSplit/Totoro/AnimationDaeWriter.cs
===================================================================
--- /OniSplit/Totoro/AnimationDaeWriter.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationDaeWriter.cs	(revision 1114)
@@ -0,0 +1,262 @@
+﻿using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+
+namespace Oni.Totoro
+{
+    internal static class AnimationDaeWriter
+    {
+        public static void AppendFrames(Animation anim1, Animation anim2)
+        {
+            var isOverlay = (anim2.Flags & AnimationFlags.Overlay) != 0;
+
+            if (isOverlay)
+            {
+                Console.Error.WriteLine("Cannot merge {0} because it's an overlay animation", anim2.Name);
+                return;
+            }
+
+            if (anim1.FrameSize == 0)
+            {
+                anim1.FrameSize = anim2.FrameSize;
+            }
+            else if (anim1.FrameSize != anim2.FrameSize)
+            {
+                Console.Error.WriteLine("Cannot merge {0} because its frame size doesn't match the frame size of the previous animation", anim2.Name);
+                return;
+            }
+
+            anim1.Velocities.AddRange(anim2.Velocities);
+            anim1.Heights.AddRange(anim2.Heights);
+
+            if (anim1.Rotations.Count == 0)
+            {
+                anim1.Rotations.AddRange(anim2.Rotations);
+            }
+            else
+            {
+                for (int i = 0; i < anim1.Rotations.Count; i++)
+                    anim1.Rotations[i].AddRange(anim2.Rotations[i]);
+            }
+        }
+
+        public static void Write(Dae.Node root, Animation animation, int startFrame = 0)
+        {
+            var velocities = animation.Velocities;
+            var heights = animation.Heights;
+            var rotations = animation.Rotations;
+            var isCompressed = animation.FrameSize == 6;
+            var isOverlay = (animation.Flags & AnimationFlags.Overlay) != 0;
+            var isRealWorld = (animation.Flags & AnimationFlags.RealWorld) != 0;
+            var activeBones = (uint)(animation.OverlayUsedBones | animation.OverlayReplacedBones);
+
+            var nodes = FindNodes(root);
+
+            if (!isOverlay && !isRealWorld)
+            {
+                var pelvis = nodes[0];
+
+                //
+                // Write pelvis position animation
+                //
+
+                var positions = new Vector2[velocities.Count + 1];
+
+                for (int i = 1; i < positions.Length; i++)
+                    positions[i] = positions[i - 1] + velocities[i - 1];
+
+                CreateAnimationCurve(startFrame, positions.Select(p => p.X).ToList(), pelvis, "pos", "X");
+                CreateAnimationCurve(startFrame, positions.Select(p => p.Y).ToList(), pelvis, "pos", "Z");
+
+                //
+                // Write pelvis height animation
+                //
+
+                CreateAnimationCurve(startFrame, heights.ToList(), pelvis, "pos", "Y");
+            }
+
+            //
+            // Write rotation animations for all bones
+            //
+
+            bool plot = true;
+
+            for (int i = 0; i < rotations.Count; i++)
+            {
+                if (isOverlay && (activeBones & (1u << i)) == 0)
+                    continue;
+
+                var node = nodes[i];
+                var keys = rotations[i];
+
+                int length;
+
+                if (plot)
+                    length = keys.Sum(k => k.Duration);
+                else
+                    length = keys.Count;
+
+                var times = new float[length];
+                var xAngles = new float[length];
+                var yAngles = new float[length];
+                var zAngles = new float[length];
+
+                if (plot)
+                {
+                    //
+                    // Transform key frames to quaternions.
+                    //
+
+                    var quats = new Quaternion[keys.Count];
+
+                    for (int k = 0; k < keys.Count; k++)
+                    {
+                        var key = keys[k];
+
+                        if (isCompressed)
+                        {
+                            quats[k] = Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathHelper.ToRadians(key.Rotation.X))
+                                     * Quaternion.CreateFromAxisAngle(Vector3.UnitY, MathHelper.ToRadians(key.Rotation.Y))
+                                     * Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(key.Rotation.Z));
+                        }
+                        else
+                        {
+                            quats[k] = new Quaternion(key.Rotation);
+                        }
+                    }
+
+                    //
+                    // Interpolate the quaternions.
+                    //
+
+                    int frame = 0;
+
+                    for (int k = 0; k < keys.Count; k++)
+                    {
+                        var duration = keys[k].Duration;
+
+                        var q1 = quats[k];
+                        var q2 = (k == keys.Count - 1) ? quats[k] : quats[k + 1];
+
+                        for (int t = 0; t < duration; t++)
+                        {
+                            var q = Quaternion.Lerp(q1, q2, (float)t / (float)duration);
+                            var euler = q.ToEulerXYZ();
+
+                            times[frame] = (frame + startFrame) * (1.0f / 60.0f);
+
+                            xAngles[frame] = euler.X;
+                            yAngles[frame] = euler.Y;
+                            zAngles[frame] = euler.Z;
+
+                            frame++;
+                        }
+                    }
+
+                    MakeRotationCurveContinuous(xAngles);
+                    MakeRotationCurveContinuous(yAngles);
+                    MakeRotationCurveContinuous(zAngles);
+                }
+                else
+                {
+                    int frame = 0;
+
+                    for (int k = 0; k < keys.Count; k++)
+                    {
+                        var key = keys[k];
+
+                        times[k] = (frame + startFrame) * (1.0f / 60.0f);
+                        frame += key.Duration;
+
+                        if (isCompressed)
+                        {
+                            xAngles[k] = key.Rotation.X;
+                            yAngles[k] = key.Rotation.Y;
+                            zAngles[k] = key.Rotation.Z;
+                        }
+                        else
+                        {
+                            var euler = new Quaternion(key.Rotation).ToEulerXYZ();
+
+                            xAngles[k] = euler.X;
+                            yAngles[k] = euler.Y;
+                            zAngles[k] = euler.Z;
+                        }
+                    }
+                }
+
+                CreateAnimationCurve(times, xAngles, node, "rotX", "ANGLE");
+                CreateAnimationCurve(times, yAngles, node, "rotY", "ANGLE");
+                CreateAnimationCurve(times, zAngles, node, "rotZ", "ANGLE");
+            }
+        }
+
+        private static void MakeRotationCurveContinuous(float[] curve)
+        {
+            for (int i = 1; i < curve.Length; i++)
+            {
+                float v1 = curve[i - 1];
+                float v2 = curve[i];
+
+                if (Math.Abs(v2 - v1) > 180.0f)
+                {
+                    if (v2 > v1)
+                        v2 -= 360.0f;
+                    else
+                        v2 += 360.0f;
+
+                    curve[i] = v2;
+                }
+            }
+        }
+
+        private static void CreateAnimationCurve(int startFrame, IList<float> values, Dae.Node targetNode, string targetSid, string targetValue)
+        {
+            if (values.Count == 0)
+                return;
+
+            var times = new float[values.Count];
+
+            for (int i = 0; i < times.Length; i++)
+                times[i] = (i + startFrame) * (1.0f / 60.0f);
+
+            CreateAnimationCurve(times, values, targetNode, targetSid, targetValue);
+        }
+
+        private static void CreateAnimationCurve(IList<float> times, IList<float> values, Dae.Node targetNode, string targetSid, string targetValue)
+        {
+            Debug.Assert(times.Count > 0);
+            Debug.Assert(times.Count == values.Count);
+
+            var interpolations = new string[times.Count];
+
+            for (int i = 0; i < interpolations.Length; i++)
+                interpolations[i] = "LINEAR";
+
+            var targetTransform = targetNode.Transforms.Find(x => x.Sid == targetSid);
+
+            targetTransform.BindAnimation(targetValue, new Dae.Sampler {
+                Inputs = {
+                    new Dae.Input(Dae.Semantic.Input, new Dae.Source(times, 1)),
+                    new Dae.Input(Dae.Semantic.Output, new Dae.Source(values, 1)),
+                    new Dae.Input(Dae.Semantic.Interpolation, new Dae.Source(interpolations, 1))
+                }
+            });
+        }
+
+        private static List<Dae.Node> FindNodes(Dae.Node root)
+        {
+            var nodes = new List<Dae.Node>(19);
+            FindNodesRecursive(root, nodes);
+            return nodes;
+        }
+
+        private static void FindNodesRecursive(Dae.Node node, List<Dae.Node> result)
+        {
+            result.Add(node);
+
+            foreach (var child in node.Nodes)
+                FindNodesRecursive(child, result);
+        }
+    }
+}
Index: /OniSplit/Totoro/AnimationDatReader.cs
===================================================================
--- /OniSplit/Totoro/AnimationDatReader.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationDatReader.cs	(revision 1114)
@@ -0,0 +1,402 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Totoro
+{
+    internal class AnimationDatReader
+    {
+        private readonly Animation animation = new Animation();
+        private readonly InstanceDescriptor tram;
+        private readonly BinaryReader dat;
+
+        #region private class DatExtent
+
+        private class DatExtent
+        {
+            public int Frame;
+            public readonly AttackExtent Extent = new AttackExtent();
+        }
+
+        #endregion
+        #region private class DatExtentInfo
+
+        private class DatExtentInfo
+        {
+            public float MaxHorizontal;
+            public float MinY = 1e09f;
+            public float MaxY = -1e09f;
+            public readonly DatExtentInfoFrame FirstExtent = new DatExtentInfoFrame();
+            public readonly DatExtentInfoFrame MaxExtent = new DatExtentInfoFrame();
+        }
+
+        #endregion
+        #region private class DatExtentInfoFrame
+
+        private class DatExtentInfoFrame
+        {
+            public int Frame = -1;
+            public int Attack;
+            public int AttackOffset;
+            public Vector2 Location;
+            public float Height;
+            public float Length;
+            public float MinY;
+            public float MaxY;
+            public float Angle;
+        }
+
+        #endregion
+
+        private AnimationDatReader(InstanceDescriptor tram, BinaryReader dat)
+        {
+            this.tram = tram;
+            this.dat = dat;
+        }
+
+        public static Animation Read(InstanceDescriptor tram)
+        {
+            using (var dat = tram.OpenRead())
+            {
+                var reader = new AnimationDatReader(tram, dat);
+                reader.ReadAnimation();
+                return reader.animation;
+            }
+        }
+
+        private void ReadAnimation()
+        {
+            dat.Skip(4);
+            int heightOffset = dat.ReadInt32();
+            int velocityOffset = dat.ReadInt32();
+            int attackOffset = dat.ReadInt32();
+            int damageOffset = dat.ReadInt32();
+            int motionBlurOffset = dat.ReadInt32();
+            int shortcutOffset = dat.ReadInt32();
+            ReadOptionalThrowInfo();
+            int footstepOffset = dat.ReadInt32();
+            int particleOffset = dat.ReadInt32();
+            int positionOffset = dat.ReadInt32();
+            int rotationOffset = dat.ReadInt32();
+            int soundOffset = dat.ReadInt32();
+            animation.Flags = (AnimationFlags)dat.ReadInt32();
+
+            var directAnimations = dat.ReadLinkArray(2);
+            for (int i = 0; i < directAnimations.Length; i++)
+                animation.DirectAnimations[i] = (directAnimations[i] != null ? directAnimations[i].FullName : null);
+
+            animation.OverlayUsedBones = (BoneMask)dat.ReadInt32();
+            animation.OverlayReplacedBones = (BoneMask)dat.ReadInt32();
+            animation.FinalRotation = dat.ReadSingle();
+            animation.Direction = (Direction)dat.ReadUInt16();
+            animation.Vocalization = dat.ReadUInt16();
+            var extents = ReadExtentInfo();
+            animation.Impact = dat.ReadString(16);
+            animation.HardPause = dat.ReadUInt16();
+            animation.SoftPause = dat.ReadUInt16();
+            int soundCount = dat.ReadInt32();
+            dat.Skip(6);
+            int fps = dat.ReadUInt16();
+            animation.FrameSize = dat.ReadUInt16();
+            animation.Type = (AnimationType)dat.ReadUInt16();
+            animation.AimingType = (AnimationType)dat.ReadUInt16();
+            animation.FromState = (AnimationState)dat.ReadUInt16();
+            animation.ToState = (AnimationState)dat.ReadUInt16();
+            int boneCount = dat.ReadUInt16();
+            int frameCount = dat.ReadUInt16();
+            int duration = dat.ReadInt16();
+            animation.Varient = (AnimationVarient)dat.ReadUInt16();
+            dat.Skip(2);
+            animation.AtomicStart = dat.ReadUInt16();
+            animation.AtomicEnd = dat.ReadUInt16();
+            animation.InterpolationEnd = dat.ReadUInt16();
+            animation.InterpolationMax = dat.ReadUInt16();
+            animation.ActionFrame = dat.ReadUInt16();
+            animation.FirstLevelAvailable = dat.ReadUInt16();
+            animation.InvulnerableStart = dat.ReadByte();
+            animation.InvulnerableEnd = dat.ReadByte();
+            int attackCount = dat.ReadByte();
+            int damageCount = dat.ReadByte();
+            int motionBlurCount = dat.ReadByte();
+            int shortcutCount = dat.ReadByte();
+            int footstepCount = dat.ReadByte();
+            int particleCount = dat.ReadByte();
+            ReadRawArray(heightOffset, frameCount, animation.Heights, r => r.ReadSingle());
+            ReadRawArray(velocityOffset, frameCount, animation.Velocities, r => r.ReadVector2());
+            ReadRotations(rotationOffset, boneCount, frameCount);
+            ReadRawArray(positionOffset, frameCount, animation.Positions, ReadPosition);
+            ReadRawArray(shortcutOffset, shortcutCount, animation.Shortcuts, ReadShortcut);
+            ReadRawArray(damageOffset, damageCount, animation.SelfDamage, ReadDamage);
+            ReadRawArray(particleOffset, particleCount, animation.Particles, ReadParticle);
+            ReadRawArray(footstepOffset, footstepCount, animation.Footsteps, ReadFootstep);
+            ReadRawArray(soundOffset, soundCount, animation.Sounds, ReadSound);
+            ReadRawArray(motionBlurOffset, motionBlurCount, animation.MotionBlur, ReadMotionBlur);
+            ReadRawArray(attackOffset, attackCount, animation.Attacks, ReadAttack);
+
+            foreach (var attack in animation.Attacks)
+            {
+                for (int i = attack.Start; i <= attack.End; i++)
+                {
+                    var extent = extents.FirstOrDefault(e => e.Frame == i);
+
+                    if (extent != null)
+                        attack.Extents.Add(extent.Extent);
+                }
+            }
+        }
+
+        private void ReadRotations(int offset, int boneCount, int frameCount)
+        {
+            using (var raw = tram.GetRawReader(offset))
+            {
+                int basePosition = raw.Position;
+                var boneOffsets = raw.ReadUInt16Array(boneCount);
+
+                foreach (int boneOffset in boneOffsets)
+                {
+                    raw.Position = basePosition + boneOffset;
+
+                    var keys = new List<KeyFrame>();
+                    int time = 0;
+
+                    do
+                    {
+                        var key = new KeyFrame();
+
+                        if (animation.FrameSize == 6)
+                        {
+                            key.Rotation.X = raw.ReadInt16() * 180.0f / 32767.5f;
+                            key.Rotation.Y = raw.ReadInt16() * 180.0f / 32767.5f;
+                            key.Rotation.Z = raw.ReadInt16() * 180.0f / 32767.5f;
+                        }
+                        else if (animation.FrameSize == 16)
+                        {
+                            key.Rotation = raw.ReadQuaternion().ToVector4();
+                        }
+
+                        if (time == frameCount - 1)
+                            key.Duration = 1;
+                        else
+                            key.Duration = raw.ReadByte();
+
+                        time += key.Duration;
+                        keys.Add(key);
+
+                    } while (time < frameCount);
+
+                    animation.Rotations.Add(keys);
+                }
+            }
+        }
+
+        private List<DatExtent> ReadExtentInfo()
+        {
+            var info = new DatExtentInfo
+            {
+                MaxHorizontal = dat.ReadSingle(),
+                MinY = dat.ReadSingle(),
+                MaxY = dat.ReadSingle()
+            };
+
+            for (int i = 0; i < animation.AttackRing.Length; i++)
+                animation.AttackRing[i] = dat.ReadSingle();
+
+            info.FirstExtent.Frame = dat.ReadUInt16();
+            info.FirstExtent.Attack = dat.ReadByte();
+            info.FirstExtent.AttackOffset = dat.ReadByte();
+            info.FirstExtent.Location = dat.ReadVector2();
+            info.FirstExtent.Height = dat.ReadSingle();
+            info.FirstExtent.Length = dat.ReadSingle();
+            info.FirstExtent.MinY = dat.ReadSingle();
+            info.FirstExtent.MaxY = dat.ReadSingle();
+            info.FirstExtent.Angle = dat.ReadSingle();
+
+            info.MaxExtent.Frame = dat.ReadUInt16();
+            info.MaxExtent.Attack = dat.ReadByte();
+            info.MaxExtent.AttackOffset = dat.ReadByte();
+            info.MaxExtent.Location = dat.ReadVector2();
+            info.MaxExtent.Height = dat.ReadSingle();
+            info.MaxExtent.Length = dat.ReadSingle();
+            info.MaxExtent.MinY = dat.ReadSingle();
+            info.MaxExtent.MaxY = dat.ReadSingle();
+            info.MaxExtent.Angle = dat.ReadSingle();
+
+            dat.Skip(4);
+
+            int extentCount = dat.ReadInt32();
+            int extentOffset = dat.ReadInt32();
+
+            var extents = new List<DatExtent>();
+            ReadRawArray(extentOffset, extentCount, extents, ReadExtent);
+
+            foreach (var datExtent in extents)
+            {
+                var attackExtent = datExtent.Extent;
+
+                if (datExtent.Frame == info.FirstExtent.Frame)
+                {
+                    attackExtent.Angle = MathHelper.ToDegrees(info.FirstExtent.Angle);
+                    attackExtent.Length = info.FirstExtent.Length;
+                    attackExtent.MinY = info.FirstExtent.MinY;
+                    attackExtent.MaxY = info.FirstExtent.MaxY;
+                }
+                else if (datExtent.Frame == info.MaxExtent.Frame)
+                {
+                    attackExtent.Angle = MathHelper.ToDegrees(info.MaxExtent.Angle);
+                    attackExtent.Length = info.MaxExtent.Length;
+                    attackExtent.MinY = info.MaxExtent.MinY;
+                    attackExtent.MaxY = info.MaxExtent.MaxY;
+                }
+
+                if (Math.Abs(attackExtent.MinY - info.MinY) < 0.01f)
+                    attackExtent.MinY = info.MinY;
+
+                if (Math.Abs(attackExtent.MaxY - info.MaxY) < 0.01f)
+                    attackExtent.MaxY = info.MaxY;
+            }
+
+            return extents;
+        }
+
+        private void ReadOptionalThrowInfo()
+        {
+            int offset = dat.ReadInt32();
+
+            if (offset != 0)
+            {
+                using (var raw = tram.GetRawReader(offset))
+                    animation.ThrowSource = ReadThrowInfo(raw);
+            }
+        }
+
+        private ThrowInfo ReadThrowInfo(BinaryReader raw)
+        {
+            return new ThrowInfo
+            {
+                Position = raw.ReadVector3(),
+                Angle = raw.ReadSingle(),
+                Distance = raw.ReadSingle(),
+                Type = (AnimationType)raw.ReadUInt16()
+            };
+        }
+
+        private Shortcut ReadShortcut(BinaryReader raw)
+        {
+            return new Shortcut
+            {
+                FromState = (AnimationState)raw.ReadUInt16(),
+                Length = raw.ReadUInt16(),
+                ReplaceAtomic = (raw.ReadInt32() != 0)
+            };
+        }
+
+        private Footstep ReadFootstep(BinaryReader raw)
+        {
+            return new Footstep
+            {
+                Frame = raw.ReadUInt16(),
+                Type = (FootstepType)raw.ReadUInt16()
+            };
+        }
+
+        private Sound ReadSound(BinaryReader raw)
+        {
+            return new Sound
+            {
+                Name = raw.ReadString(32),
+                Start = raw.ReadUInt16()
+            };
+        }
+
+        private MotionBlur ReadMotionBlur(BinaryReader raw)
+        {
+            var motionBlur = new MotionBlur
+            {
+                Bones = (BoneMask)raw.ReadInt32(),
+                Start = raw.ReadUInt16(),
+                End = raw.ReadUInt16(),
+                Lifetime = raw.ReadByte(),
+                Alpha = raw.ReadByte(),
+                Interval = raw.ReadByte()
+            };
+
+            raw.Skip(1);
+
+            return motionBlur;
+        }
+
+        private Particle ReadParticle(BinaryReader raw)
+        {
+            return new Particle
+            {
+                Start = raw.ReadUInt16(),
+                End = raw.ReadUInt16(),
+                Bone = (Bone)raw.ReadInt32(),
+                Name = raw.ReadString(16)
+            };
+        }
+
+        private Damage ReadDamage(BinaryReader raw)
+        {
+            return new Damage
+            {
+                Points = raw.ReadUInt16(),
+                Frame = raw.ReadUInt16()
+            };
+        }
+
+        private Position ReadPosition(BinaryReader raw)
+        {
+            return new Position
+            {
+                X = raw.ReadInt16() * 0.01f,
+                Z = raw.ReadInt16() * 0.01f,
+                Height = raw.ReadUInt16() * 0.01f,
+                YOffset = raw.ReadInt16() * 0.01f
+            };
+        }
+
+        private Attack ReadAttack(BinaryReader raw)
+        {
+            var attack = new Attack
+            {
+                Bones = (BoneMask)raw.ReadInt32(),
+                Knockback = raw.ReadSingle(),
+                Flags = (AttackFlags)raw.ReadInt32(),
+                HitPoints = raw.ReadInt16(),
+                Start = raw.ReadUInt16(),
+                End = raw.ReadUInt16(),
+                HitType = (AnimationType)raw.ReadUInt16(),
+                HitLength = raw.ReadUInt16(),
+                StunLength = raw.ReadUInt16(),
+                StaggerLength = raw.ReadUInt16()
+            };
+
+            raw.Skip(6);
+
+            return attack;
+        }
+
+        private DatExtent ReadExtent(BinaryReader raw)
+        {
+            var extent = new DatExtent();
+            extent.Frame = raw.ReadInt16();
+            extent.Extent.Angle = raw.ReadUInt16() * 360.0f / 65535.0f;
+            extent.Extent.Length = (raw.ReadUInt32() & 0xffffu) * 0.01f;
+            extent.Extent.MinY = raw.ReadInt16() * 0.01f;
+            extent.Extent.MaxY = raw.ReadInt16() * 0.01f;
+            return extent;
+        }
+
+        private void ReadRawArray<T>(int offset, int count, List<T> list, Func<BinaryReader, T> readElement)
+        {
+            if (offset == 0 || count == 0)
+                return;
+
+            using (var raw = tram.GetRawReader(offset))
+            {
+                for (int i = 0; i < count; i++)
+                    list.Add(readElement(raw));
+            }
+        }
+    }
+}
Index: /OniSplit/Totoro/AnimationDatWriter.cs
===================================================================
--- /OniSplit/Totoro/AnimationDatWriter.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationDatWriter.cs	(revision 1114)
@@ -0,0 +1,585 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Xml;
+using Oni.Metadata;
+
+namespace Oni.Totoro
+{
+    internal class AnimationDatWriter
+    {
+        private Animation animation;
+        private List<DatExtent> extents;
+        private DatExtentInfo extentInfo;
+        private Importer importer;
+        private BinaryWriter dat;
+        private BinaryWriter raw;
+
+        #region private class DatExtent
+
+        private class DatExtent
+        {
+            public readonly int Frame;
+            public readonly AttackExtent Extent;
+
+            public DatExtent(int frame, AttackExtent extent)
+            {
+                this.Frame = frame;
+                this.Extent = extent;
+            }
+        }
+
+        #endregion
+        #region private class DatExtentInfo
+
+        private class DatExtentInfo
+        {
+            public float MaxDistance;
+            public float MinY = 1e09f;
+            public float MaxY = -1e09f;
+            public readonly DatExtentInfoFrame FirstExtent = new DatExtentInfoFrame();
+            public readonly DatExtentInfoFrame MaxExtent = new DatExtentInfoFrame();
+        }
+
+        #endregion
+        #region private class DatExtentInfoFrame
+
+        private class DatExtentInfoFrame
+        {
+            public int Frame = -1;
+            public int Attack;
+            public int AttackOffset;
+            public Vector2 Location;
+            public float Height;
+            public float Length;
+            public float MinY;
+            public float MaxY;
+            public float Angle;
+        }
+
+        #endregion
+
+        private AnimationDatWriter()
+        {
+        }
+
+        public static void Write(Animation animation, Importer importer, BinaryWriter dat)
+        {
+            var writer = new AnimationDatWriter
+            {
+                animation = animation,
+                importer = importer,
+                dat = dat,
+                raw = importer.RawWriter
+            };
+
+            writer.WriteAnimation();
+        }
+
+        private void WriteAnimation()
+        {
+            extentInfo = new DatExtentInfo();
+            extents = new List<DatExtent>();
+
+            if (animation.Attacks.Count > 0)
+            {
+                if (animation.Attacks[0].Extents.Count == 0)
+                    GenerateExtentInfo();
+
+                foreach (var attack in animation.Attacks)
+                {
+                    int frame = attack.Start;
+
+                    foreach (var extent in attack.Extents)
+                        extents.Add(new DatExtent(frame++, extent));
+                }
+
+                GenerateExtentSummary();
+            }
+
+            var rotations = animation.Rotations;
+            int frameSize = animation.FrameSize;
+
+            if (frameSize == 16 && (animation.Flags & AnimationFlags.Overlay) == 0)
+            {
+                rotations = CompressFrames(rotations);
+                frameSize = 6;
+            }
+
+            dat.Write(0);
+            WriteRawArray(animation.Heights, x => raw.Write(x));
+            WriteRawArray(animation.Velocities, x => raw.Write(x));
+            WriteRawArray(animation.Attacks, Write);
+            WriteRawArray(animation.SelfDamage, Write);
+            WriteRawArray(animation.MotionBlur, Write);
+            WriteRawArray(animation.Shortcuts, Write);
+            WriteThrowInfo();
+            WriteRawArray(animation.Footsteps, Write);
+            WriteRawArray(animation.Particles, Write);
+            WriteRawArray(animation.Positions, Write);
+            WriteRotations(rotations, frameSize);
+            WriteRawArray(animation.Sounds, Write);
+            dat.Write((int)animation.Flags);
+
+            if (!string.IsNullOrEmpty(animation.DirectAnimations[0]))
+                dat.Write(importer.CreateInstance(TemplateTag.TRAM, animation.DirectAnimations[0]));
+            else
+                dat.Write(0);
+
+            if (!string.IsNullOrEmpty(animation.DirectAnimations[1]))
+                dat.Write(importer.CreateInstance(TemplateTag.TRAM, animation.DirectAnimations[1]));
+            else
+                dat.Write(0);
+
+            dat.Write((int)animation.OverlayUsedBones);
+            dat.Write((int)animation.OverlayReplacedBones);
+            dat.Write(animation.FinalRotation);
+            dat.Write((ushort)animation.Direction);
+            dat.WriteUInt16(animation.Vocalization);
+            WriteExtentInfo();
+            dat.Write(animation.Impact, 16);
+            dat.WriteUInt16(animation.HardPause);
+            dat.WriteUInt16(animation.SoftPause);
+            dat.Write(animation.Sounds.Count);
+            dat.Skip(6);
+            dat.WriteUInt16(60);
+            dat.WriteUInt16(frameSize);
+            dat.WriteUInt16((ushort)animation.Type);
+            dat.WriteUInt16((ushort)animation.AimingType);
+            dat.WriteUInt16((ushort)animation.FromState);
+            dat.WriteUInt16((ushort)animation.ToState);
+            dat.WriteUInt16(rotations.Count);
+            dat.WriteUInt16(animation.Velocities.Count);
+            dat.WriteUInt16(animation.Velocities.Count);
+            dat.WriteUInt16((ushort)animation.Varient);
+            dat.Skip(2);
+            dat.WriteUInt16(animation.AtomicStart);
+            dat.WriteUInt16(animation.AtomicEnd);
+            dat.WriteUInt16(animation.InterpolationEnd);
+            dat.WriteUInt16(animation.InterpolationMax);
+            dat.WriteUInt16(animation.ActionFrame);
+            dat.WriteUInt16(animation.FirstLevelAvailable);
+            dat.WriteByte(animation.InvulnerableStart);
+            dat.WriteByte(animation.InvulnerableEnd);
+            dat.WriteByte(animation.Attacks.Count);
+            dat.WriteByte(animation.SelfDamage.Count);
+            dat.WriteByte(animation.MotionBlur.Count);
+            dat.WriteByte(animation.Shortcuts.Count);
+            dat.WriteByte(animation.Footsteps.Count);
+            dat.WriteByte(animation.Particles.Count);
+        }
+
+        private void WriteRotations(List<List<KeyFrame>> rotations, int frameSize)
+        {
+            dat.Write(raw.Align32());
+
+            var offsets = new ushort[rotations.Count];
+
+            offsets[0] = (ushort)(rotations.Count * 2);
+
+            for (int i = 1; i < offsets.Length; i++)
+                offsets[i] = (ushort)(offsets[i - 1] + rotations[i - 1].Count * (frameSize + 1) - 1);
+
+            raw.Write(offsets);
+
+            foreach (var keys in rotations)
+            {
+                foreach (var key in keys)
+                {
+                    switch (frameSize)
+                    {
+                        case 6:
+                            raw.WriteInt16((short)(Math.Round(key.Rotation.X / 180.0f * 32767.5f)));
+                            raw.WriteInt16((short)(Math.Round(key.Rotation.Y / 180.0f * 32767.5f)));
+                            raw.WriteInt16((short)(Math.Round(key.Rotation.Z / 180.0f * 32767.5f)));
+                            break;
+
+                        case 16:
+                            raw.Write(new Quaternion(key.Rotation));
+                            break;
+                    }
+
+                    if (key != keys.Last())
+                        raw.WriteByte(key.Duration);
+                }
+            }
+        }
+
+        private void WriteThrowInfo()
+        {
+            if (animation.ThrowSource == null)
+            {
+                dat.Write(0);
+                return;
+            }
+
+            dat.Write(raw.Align32());
+
+            raw.Write(animation.ThrowSource.Position);
+            raw.Write(animation.ThrowSource.Angle);
+            raw.Write(animation.ThrowSource.Distance);
+            raw.WriteUInt16((ushort)animation.ThrowSource.Type);
+        }
+
+        private void WriteExtentInfo()
+        {
+            dat.Write(extentInfo.MaxDistance);
+            dat.Write(extentInfo.MinY);
+            dat.Write(extentInfo.MaxY);
+            dat.Write(animation.AttackRing);
+            Write(extentInfo.FirstExtent);
+            Write(extentInfo.MaxExtent);
+            dat.Write(0);
+            dat.Write(extents.Count);
+            WriteRawArray(extents, Write);
+        }
+
+        private void Write(DatExtentInfoFrame info)
+        {
+            dat.WriteInt16(info.Frame);
+            dat.WriteByte(info.Attack);
+            dat.WriteByte(info.AttackOffset);
+            dat.Write(info.Location);
+            dat.Write(info.Height);
+            dat.Write(info.Length);
+            dat.Write(info.MinY);
+            dat.Write(info.MaxY);
+            dat.Write(info.Angle);
+        }
+
+        private void Write(Position position)
+        {
+            raw.Write((short)Math.Round(position.X * 100.0f));
+            raw.Write((short)Math.Round(position.Z * 100.0f));
+            raw.Write((ushort)Math.Round(position.Height * 100.0f));
+            raw.Write((short)Math.Round(position.YOffset * 100.0f));
+        }
+
+        private void Write(Damage damage)
+        {
+            raw.WriteUInt16(damage.Points);
+            raw.WriteUInt16(damage.Frame);
+        }
+
+        private void Write(Shortcut shortcut)
+        {
+            raw.WriteUInt16((ushort)shortcut.FromState);
+            raw.WriteUInt16(shortcut.Length);
+            raw.Write(shortcut.ReplaceAtomic ? 1 : 0);
+        }
+
+        private void Write(Footstep footstep)
+        {
+            raw.WriteUInt16(footstep.Frame);
+            raw.WriteUInt16((ushort)footstep.Type);
+        }
+
+        private void Write(Sound sound)
+        {
+            raw.Write(sound.Name, 32);
+            raw.WriteUInt16(sound.Start);
+        }
+
+        private void Write(Particle particle)
+        {
+            raw.WriteUInt16(particle.Start);
+            raw.WriteUInt16(particle.End);
+            raw.Write((int)particle.Bone);
+            raw.Write(particle.Name, 16);
+        }
+
+        private void Write(MotionBlur m)
+        {
+            raw.Write((int)m.Bones);
+            raw.WriteUInt16(m.Start);
+            raw.WriteUInt16(m.End);
+            raw.WriteByte(m.Lifetime);
+            raw.WriteByte(m.Alpha);
+            raw.WriteByte(m.Interval);
+            raw.WriteByte(0);
+        }
+
+        private void Write(DatExtent extent)
+        {
+            raw.WriteInt16(extent.Frame);
+            raw.Write((short)Math.Round(extent.Extent.Angle * 65535.0f / 360.0f));
+            raw.Write((ushort)Math.Round(extent.Extent.Length * 100.0f));
+            raw.WriteInt16(0);
+            raw.Write((short)Math.Round(extent.Extent.MinY * 100.0f));
+            raw.Write((short)Math.Round(extent.Extent.MaxY * 100.0f));
+        }
+
+        private void Write(Attack attack)
+        {
+            raw.Write((int)attack.Bones);
+            raw.Write(attack.Knockback);
+            raw.Write((int)attack.Flags);
+            raw.WriteInt16(attack.HitPoints);
+            raw.WriteInt16(attack.Start);
+            raw.WriteInt16(attack.End);
+            raw.WriteInt16((short)attack.HitType);
+            raw.WriteInt16(attack.HitLength);
+            raw.WriteInt16(attack.StunLength);
+            raw.WriteInt16(attack.StaggerLength);
+            raw.WriteInt16(0);
+            raw.Write(0);
+        }
+
+        private void WriteRawArray<T>(List<T> list, Action<T> writeElement)
+        {
+            if (list.Count == 0)
+            {
+                dat.Write(0);
+                return;
+            }
+
+            dat.Write(raw.Align32());
+
+            foreach (T t in list)
+                writeElement(t);
+        }
+
+        private void GenerateExtentInfo()
+        {
+            float[] attackRing = animation.AttackRing;
+
+            Array.Clear(attackRing, 0, attackRing.Length);
+
+            foreach (var attack in animation.Attacks)
+            {
+                attack.Extents.Clear();
+
+                for (int frame = attack.Start; frame <= attack.End; frame++)
+                {
+                    var position = animation.Positions[frame].XZ;
+                    var framePoints = animation.AllPoints[frame];
+
+                    for (int j = 0; j < framePoints.Count / 8; j++)
+                    {
+                        if ((attack.Bones & (BoneMask)(1 << j)) == 0)
+                            continue;
+
+                        for (int k = j * 8; k < (j + 1) * 8; k++)
+                        {
+                            var point = framePoints[k];
+                            var delta = point.XZ - animation.Positions[0].XZ;
+
+                            float distance = delta.Length();
+                            float angle = FMath.Atan2(delta.X, delta.Y);
+
+                            if (angle < 0.0f)
+                                angle += MathHelper.TwoPi;
+
+                            for (int r = 0; r < attackRing.Length; r++)
+                            {
+                                float ringAngle = r * MathHelper.TwoPi / attackRing.Length;
+
+                                if (Math.Abs(ringAngle - angle) < MathHelper.ToRadians(30.0f))
+                                    attackRing[r] = Math.Max(attackRing[r], distance);
+                            }
+                        }
+                    }
+
+                    float minHeight = +1e09f;
+                    float maxHeight = -1e09f;
+                    float maxDistance = -1e09f;
+                    float maxAngle = 0.0f;
+
+                    for (int j = 0; j < framePoints.Count / 8; j++)
+                    {
+                        if ((attack.Bones & (BoneMask)(1 << j)) == 0)
+                            continue;
+
+                        for (int k = j * 8; k < (j + 1) * 8; k++)
+                        {
+                            var point = framePoints[k];
+                            var delta = point.XZ - position;
+
+                            float distance;
+
+                            switch (animation.Direction)
+                            {
+                                case Direction.Forward:
+                                    distance = delta.Y;
+                                    break;
+                                case Direction.Left:
+                                    distance = delta.X;
+                                    break;
+                                case Direction.Right:
+                                    distance = -delta.X;
+                                    break;
+                                case Direction.Backward:
+                                    distance = -delta.Y;
+                                    break;
+                                default:
+                                    distance = delta.Length();
+                                    break;
+                            }
+
+                            if (distance > maxDistance)
+                            {
+                                maxDistance = distance;
+                                maxAngle = FMath.Atan2(delta.X, delta.Y);
+                            }
+
+                            minHeight = Math.Min(minHeight, point.Y);
+                            maxHeight = Math.Max(maxHeight, point.Y);
+                        }
+                    }
+
+                    maxDistance = Math.Max(maxDistance, 0.0f);
+
+                    if (maxAngle < 0)
+                        maxAngle += MathHelper.TwoPi;
+
+                    attack.Extents.Add(new AttackExtent
+                    {
+                        Angle = MathHelper.ToDegrees(maxAngle),
+                        Length = maxDistance,
+                        MinY = minHeight,
+                        MaxY = maxHeight
+                    });
+                }
+            }
+        }
+
+        private void GenerateExtentSummary()
+        {
+            if (extents.Count == 0)
+                return;
+
+            var positions = animation.Positions;
+            var attacks = animation.Attacks;
+            var heights = animation.Heights;
+
+            float minY = float.MaxValue, maxY = float.MinValue;
+
+            foreach (var datExtent in extents)
+            {
+                minY = Math.Min(minY, datExtent.Extent.MinY);
+                maxY = Math.Max(maxY, datExtent.Extent.MaxY);
+            }
+
+            var firstExtent = extents[0];
+            var maxExtent = firstExtent;
+
+            foreach (var datExtent in extents)
+            {
+                if (datExtent.Extent.Length + positions[datExtent.Frame].Z > maxExtent.Extent.Length + positions[maxExtent.Frame].Z)
+                    maxExtent = datExtent;
+            }
+
+            int maxAttackIndex = 0, maxAttackOffset = 0;
+
+            for (int i = 0; i < attacks.Count; i++)
+            {
+                var attack = attacks[i];
+
+                if (attack.Start <= maxExtent.Frame && maxExtent.Frame <= attack.End)
+                {
+                    maxAttackIndex = i;
+                    maxAttackOffset = maxExtent.Frame - attack.Start;
+                    break;
+                }
+            }
+
+            extentInfo.MaxDistance = animation.AttackRing.Max();
+            extentInfo.MinY = minY;
+            extentInfo.MaxY = maxY;
+
+            extentInfo.FirstExtent.Frame = firstExtent.Frame;
+            extentInfo.FirstExtent.Attack = 0;
+            extentInfo.FirstExtent.AttackOffset = 0;
+            extentInfo.FirstExtent.Location.X = positions[firstExtent.Frame].X;
+            extentInfo.FirstExtent.Location.Y = -positions[firstExtent.Frame].Z;
+            extentInfo.FirstExtent.Height = heights[firstExtent.Frame];
+            extentInfo.FirstExtent.Angle = MathHelper.ToRadians(firstExtent.Extent.Angle);
+            extentInfo.FirstExtent.Length = firstExtent.Extent.Length;
+            extentInfo.FirstExtent.MinY = FMath.Round(firstExtent.Extent.MinY, 2);
+            extentInfo.FirstExtent.MaxY = firstExtent.Extent.MaxY;
+
+            if ((animation.Flags & AnimationFlags.ThrowTarget) == 0)
+            {
+                extentInfo.MaxExtent.Frame = maxExtent.Frame;
+                extentInfo.MaxExtent.Attack = maxAttackIndex;
+                extentInfo.MaxExtent.AttackOffset = maxAttackOffset;
+                extentInfo.MaxExtent.Location.X = positions[maxExtent.Frame].X;
+                extentInfo.MaxExtent.Location.Y = -positions[maxExtent.Frame].Z;
+                extentInfo.MaxExtent.Height = heights[maxExtent.Frame];
+                extentInfo.MaxExtent.Angle = MathHelper.ToRadians(maxExtent.Extent.Angle);
+                extentInfo.MaxExtent.Length = maxExtent.Extent.Length;
+                extentInfo.MaxExtent.MinY = maxExtent.Extent.MinY;
+                extentInfo.MaxExtent.MaxY = FMath.Round(maxExtent.Extent.MaxY, 2);
+            }
+        }
+
+        private List<List<KeyFrame>> CompressFrames(List<List<KeyFrame>> tracks)
+        {
+            float tolerance = 0.5f;
+            float cosTolerance = FMath.Cos(MathHelper.ToRadians(tolerance) * 0.5f);
+            var newTracks = new List<List<KeyFrame>>();
+
+            foreach (var keys in tracks)
+            {
+                var newFrames = new List<KeyFrame>(keys.Count);
+
+                for (int i = 0; i < keys.Count;)
+                {
+                    var key = keys[i];
+
+                    int duration = key.Duration;
+                    var q0 = new Quaternion(key.Rotation);
+
+                    if (duration == 1)
+                    {
+                        for (int j = i + 2; j < keys.Count; j++)
+                        {
+                            if (!IsLinearRange(keys, i, j, cosTolerance))
+                                break;
+
+                            duration = j - i;
+                        }
+                    }
+
+                    var eulerXYZ = q0.ToEulerXYZ();
+
+                    newFrames.Add(new KeyFrame
+                    {
+                        Duration = duration,
+                        Rotation = {
+                            X = eulerXYZ.X,
+                            Y = eulerXYZ.Y,
+                            Z = eulerXYZ.Z
+                        }
+                    });
+
+                    i += duration;
+                }
+
+                newTracks.Add(newFrames);
+            }
+
+            return newTracks;
+        }
+
+        private static bool IsLinearRange(List<KeyFrame> frames, int first, int last, float tolerance)
+        {
+            var q0 = new Quaternion(frames[first].Rotation);
+            var q1 = new Quaternion(frames[last].Rotation);
+            float length = last - first;
+
+            for (int i = first + 1; i < last; ++i)
+            {
+                float t = (i - first) / length;
+
+                var linear = Quaternion.Lerp(q0, q1, t);
+                var real = new Quaternion(frames[i].Rotation);
+                var error = Quaternion.Conjugate(linear) * real;
+
+                if (Math.Abs(error.W) < tolerance)
+                    return false;
+            }
+
+            return true;
+        }
+    }
+}
Index: /OniSplit/Totoro/AnimationFlags.cs
===================================================================
--- /OniSplit/Totoro/AnimationFlags.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationFlags.cs	(revision 1114)
@@ -0,0 +1,35 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    [Flags]
+    internal enum AnimationFlags
+    {
+        RuntimeLoaded = 0x00000001,
+        Invulnerable = 0x00000002,
+        BlockHigh = 0x00000004,
+        BlockLow = 0x00000008,
+        Attack = 0x00000010,
+        DropWeapon = 0x00000020,
+        InAir = 0x00000040,
+        Atomic = 0x00000080,
+
+        NoTurn = 0x00000100,
+        AttackForward = 0x00000200,
+        AttackLeft = 0x00000400,
+        AttackRight = 0x00000800,
+        AttackBackward = 0x00001000,
+        Overlay = 0x00002000,
+        DontInterpolateVelocity = 0x00004000,
+        ThrowSource = 0x00008000,
+
+        ThrowTarget = 0x00010000,
+        RealWorld = 0x00020000,
+        DoAim = 0x00040000,
+        DontAim = 0x00080000,
+        CanPickup = 0x00100000,
+        Aim360 = 0x00200000,
+        DisableShield = 0x00400000,
+        NoAIPickup = 0x00800000
+    }
+}
Index: /OniSplit/Totoro/AnimationState.cs
===================================================================
--- /OniSplit/Totoro/AnimationState.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationState.cs	(revision 1114)
@@ -0,0 +1,78 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    internal enum AnimationState
+    {
+        None,
+        Anything,
+        RunningLeftDown,
+        RunningRightDown,
+        Sliding,
+        WalkingLeftDown,
+        WalkingRightDown,
+        Standing,
+        RunStart,
+        RunAccel,
+        RunSidestepLeft,
+        RunSidestepRight,
+        RunSlide,
+        RunJump,
+        RunJumpLand,
+        RunBackStart,
+        RunningBackRightDown,
+        RunningBackLeftDown,
+        FallenBack,
+        Crouch,
+        RunningUpstairRightDown,
+        RunningUpstairLeftDown,
+        SidestepLeftLeftDown,
+        SidestepLeftRightDown,
+        SidestepRightLeftDown,
+        SidestepRightRightDown,
+        SidestepRightJump,
+        SidestepLeftJump,
+        JumpForward,
+        JumpUp,
+        RunBackSlide,
+        LieBack,
+        SsLtStart,
+        SsRtStart,
+        WalkingSidestepLeft,
+        CrouchWalk,
+        WalkingSidestepRight,
+        Flying,
+        Falling,
+        FlyingForward,
+        FallingForward,
+        FlyingBack,
+        FallingBack,
+        FlyingLeft,
+        FallingLeft,
+        FlyingRight,
+        FallingRight,
+        CrouchStart,
+        WalkingBackLeftDown,
+        WalkingBackRightDown,
+        FallenFront,
+        SidestepLeftStart,
+        SidestepRightStart,
+        Sit,
+        PunchLow,
+        StandSpecial,
+        Acting,
+        CrouchRunLeft,
+        CrouchRunRight,
+        CrouchRunBackLeft,
+        CrouchRunBackRight,
+        Blocking1,
+        Blocking2,
+        Blocking3,
+        CrouchBlocking1,
+        Gliding,
+        WatchIdle,
+        Stunned,
+        Powerup,
+        Thunderbolt
+    }
+}
Index: /OniSplit/Totoro/AnimationType.cs
===================================================================
--- /OniSplit/Totoro/AnimationType.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationType.cs	(revision 1114)
@@ -0,0 +1,241 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    internal enum AnimationType : ushort
+    {
+        None,
+        Anything,
+        Walk,
+        Run,
+        Slide,
+        Jump,
+        Stand,
+        StandingTurnLeft,
+        StandingTurnRight,
+        RunBackwards,
+        RunSidestepLeft,
+        RunSidestepRight,
+        Kick,
+        WalkSidestepLeft,
+        WalkSidestepRight,
+        WalkBackwards,
+        Stance,
+        Crouch,
+        JumpForward,
+        JumpBackward,
+        JumpLeft,
+        JumpRight,
+        Punch,
+        Block,
+        Land,
+        Fly,
+        KickForward,
+        KickLeft,
+        KickRight,
+        KickBack,
+        KickLow,
+        PunchForward,
+        PunchLeft,
+        PunchRight,
+        PunchBack,
+        PunchLow,
+        Kick2,
+        Kick3,
+        Punch2,
+        Punch3,
+        LandForward,
+        LandRight,
+        LandLeft,
+        LandBack,
+        PPK,
+        PKK,
+        PKP,
+        KPK,
+        KPP,
+        KKP,
+        PK,
+        KP,
+        PunchHeavy,
+        KickHeavy,
+        PunchForwardHeavy,
+        KickForwardHeavy,
+        AimingOverlay,
+        HitOverlay,
+        CrouchRun,
+        CrouchWalk,
+        CrouchRunBackwards,
+        CrouchWalkBackwards,
+        CrouchRunSidestepLeft,
+        CrouchRunSidestepRight,
+        CrouchWalkSidestepLeft,
+        CrouchWalkSidestepRight,
+        RunKick,
+        RunPunch,
+        RunBackPunch,
+        RunBackKick,
+        SidestepLeftKick,
+        SidestepLeftPunch,
+        SidestepRightKick,
+        SidestepRightPunch,
+        Prone,
+        Flip,
+        HitHead,
+        HitBody,
+        HitFoot,
+        KnockdownHead,
+        KnockdownBody,
+        KnockdownFoot,
+        HitCrouch,
+        KnockdownCrouch,
+        HitFallen,
+        HitHeadBehind,
+        HitBodyBehind,
+        HitFootBehind,
+        KnockdownHeadBehind,
+        KnockdownBodyBehind,
+        KnockdownFootBehind,
+        HitCrouchBehind,
+        KnockdownCrouchBehind,
+        Idle,
+        Taunt,
+        Throw,
+        Thrown1,
+        Thrown2,
+        Thrown3,
+        Thrown4,
+        Thrown5,
+        Thrown6,
+        Special1,
+        Special2,
+        Special3,
+        Special4,
+        ThrowForwardPunch,
+        ThrowForwardKick,
+        ThrowBackwardPunch,
+        ThrowBackwardKick,
+        RunThrowForwardPunch,
+        RunThrowBackwardPunch,
+        RunThrowForwardKick,
+        RunThrowBackwardKick,
+        Thrown7,
+        Thrown8,
+        Thrown9,
+        Thrown10,
+        Thrown11,
+        Thrown12,
+        StartleLeft,
+        StartleRight,
+        Sit,
+        StandSpecial,
+        Act,
+        Kick3Fw,
+        HitFootOuch,
+        HitJewels,
+        Thrown13,
+        Thrown14,
+        Thrown15,
+        Thrown16,
+        Thrown17,
+        PPKK,
+        PPKKK,
+        PPKKKK,
+        LandHard,
+        LandHardForward,
+        LandHardRight,
+        LandHardLeft,
+        LandHardBack,
+        LandDead,
+        CrouchTurnLeft,
+        CrouchTurnRight,
+        CrouchForward,
+        CrouchBack,
+        CrouchLeft,
+        CrouchRight,
+        GetupKickBack,
+        AutopistolRecoil,
+        PhaseRifleRecoil,
+        PhaseStreamRecoil,
+        SuperballRecoil,
+        VandegrafRecoil,
+        ScramCannonRecoil,
+        MercuryBowRecoil,
+        ScreamerRecoil,
+        PickupObject,
+        PickupPistol,
+        PickupRifle,
+        Holster,
+        DrawPistol,
+        DrawRifle,
+        Punch4,
+        ReloadPistol,
+        ReloadPhaseRifle,
+        ReloadPhaseStream,
+        ReloadSuperball,
+        ReloadVandegraf,
+        ReloadScramCannon,
+        ReloadMercuryBow,
+        ReloadScreamer,
+        PfPf,
+        PfPfPf,
+        PlPl,
+        PlPlPl,
+        PrPr,
+        PrPrPr,
+        PbPb,
+        PbPbPb,
+        PdPd,
+        PdPdPd,
+        KfKf,
+        KfKfKf,
+        KlKl,
+        KlKlKl,
+        KrKr,
+        KrKrKr,
+        KbKb,
+        KbKbKb,
+        KdKd,
+        KdKdKd,
+        StartleLt,
+        StartleRt,
+        StartleBk,
+        StartleFw,
+        Console,
+        ConsoleWalk,
+        Stagger,
+        Watch,
+        ActNo,
+        ActYes,
+        ActTalk,
+        ActShrug,
+        ActShout,
+        ActGive,
+        RunStop,
+        WalkStop,
+        RunStart,
+        WalkStart,
+        RunBackwardsStart,
+        WalkBackwardsStart,
+        Stun,
+        StaggerBehind,
+        Blownup,
+        BlownupBehind,
+        OneStepStop,
+        RunSidestepLeftStart,
+        RunSidestepRightStart,
+        Powerup,
+        FallingFlail,
+        ConsolePunch,
+        TeleportIn,
+        TeleportOut,
+        NinjaFireball,
+        NinjaInvisible,
+        PunchRifle,
+        PickupObjectMid,
+        PickupPistolMid,
+        PickupRifleMid,
+        Hail,
+        MuroThunderbolt,
+        HitOverlayAI
+    }
+}
Index: /OniSplit/Totoro/AnimationVarient.cs
===================================================================
--- /OniSplit/Totoro/AnimationVarient.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationVarient.cs	(revision 1114)
@@ -0,0 +1,17 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    [Flags]
+    internal enum AnimationVarient
+    {
+        None = 0x0000,
+        Sprint = 0x0100,
+        Combat = 0x0200,
+        RightPistol = 0x0800,
+        LeftPistol = 0x1000,
+        RightRifle = 0x2000,
+        LeftRifle = 0x4000,
+        Panic = 0x8000
+    }
+}
Index: /OniSplit/Totoro/AnimationXmlReader.cs
===================================================================
--- /OniSplit/Totoro/AnimationXmlReader.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationXmlReader.cs	(revision 1114)
@@ -0,0 +1,437 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Xml;
+
+namespace Oni.Totoro
+{
+    internal class AnimationXmlReader
+    {
+        private const string ns = "";
+        private static readonly char[] emptyChars = new char[0];
+        private XmlReader xml;
+        private string basePath;
+        private Animation animation;
+        private AnimationDaeReader daeReader;
+
+        private AnimationXmlReader()
+        {
+        }
+
+        public static Animation Read(XmlReader xml, string baseDir)
+        {
+            var reader = new AnimationXmlReader
+            {
+                xml = xml,
+                basePath = baseDir,
+                animation = new Animation()
+            };
+
+            var animation = reader.Read();
+            animation.ValidateFrames();
+            return animation;
+        }
+
+        private Animation Read()
+        {
+            animation.Name = xml.GetAttribute("Name");
+            xml.ReadStartElement("Animation", ns);
+
+            if (xml.IsStartElement("DaeImport") || xml.IsStartElement("Import"))
+                ImportDaeAnimation();
+
+            xml.ReadStartElement("Lookup");
+            animation.Type = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("Type", ns));
+            animation.AimingType = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("AimingType", ns));
+            animation.FromState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("FromState", ns));
+            animation.ToState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("ToState", ns));
+            animation.Varient = MetaEnum.Parse<AnimationVarient>(xml.ReadElementContentAsString("Varient", ns));
+            animation.FirstLevelAvailable = xml.ReadElementContentAsInt("FirstLevel", ns);
+            ReadRawArray("Shortcuts", animation.Shortcuts, Read);
+            xml.ReadEndElement();
+
+            animation.Flags = MetaEnum.Parse<AnimationFlags>(xml.ReadElementContentAsString("Flags", ns));
+            xml.ReadStartElement("Atomic", ns);
+            animation.AtomicStart = xml.ReadElementContentAsInt("Start", ns);
+            animation.AtomicEnd = xml.ReadElementContentAsInt("End", ns);
+            xml.ReadEndElement();
+            xml.ReadStartElement("Invulnerable", ns);
+            animation.InvulnerableStart = xml.ReadElementContentAsInt("Start", ns);
+            animation.InvulnerableEnd = xml.ReadElementContentAsInt("End", ns);
+            xml.ReadEndElement();
+            xml.ReadStartElement("Overlay", ns);
+            animation.OverlayUsedBones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("UsedBones", ns));
+            animation.OverlayReplacedBones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("ReplacedBones", ns));
+            xml.ReadEndElement();
+
+            xml.ReadStartElement("DirectAnimations", ns);
+            animation.DirectAnimations[0] = xml.ReadElementContentAsString("Link", ns);
+            animation.DirectAnimations[1] = xml.ReadElementContentAsString("Link", ns);
+            xml.ReadEndElement();
+            xml.ReadStartElement("Pause");
+            animation.HardPause = xml.ReadElementContentAsInt("Hard", ns);
+            animation.SoftPause = xml.ReadElementContentAsInt("Soft", ns);
+            xml.ReadEndElement();
+            xml.ReadStartElement("Interpolation", ns);
+            animation.InterpolationEnd = xml.ReadElementContentAsInt("End", ns);
+            animation.InterpolationMax = xml.ReadElementContentAsInt("Max", ns);
+            xml.ReadEndElement();
+
+            animation.FinalRotation = MathHelper.ToRadians(xml.ReadElementContentAsFloat("FinalRotation", ns));
+            animation.Direction = MetaEnum.Parse<Direction>(xml.ReadElementContentAsString("Direction", ns));
+            animation.Vocalization = xml.ReadElementContentAsInt("Vocalization", ns);
+            animation.ActionFrame = xml.ReadElementContentAsInt("ActionFrame", ns);
+            animation.Impact = xml.ReadElementContentAsString("Impact", ns);
+
+            ReadRawArray("Particle", animation.Particles, Read);
+            ReadRawArray("MotionBlur", animation.MotionBlur, Read);
+            ReadRawArray("Footsteps", animation.Footsteps, Read);
+            ReadRawArray("Sounds", animation.Sounds, Read);
+
+            if (daeReader == null)
+            {
+                ReadHeights();
+                ReadVelocities();
+                ReadRotations();
+                ReadPositions();
+            }
+
+            ReadThrowInfo();
+            ReadRawArray("SelfDamage", animation.SelfDamage, Read);
+
+            if (xml.IsStartElement("Attacks"))
+            {
+                ReadRawArray("Attacks", animation.Attacks, Read);
+                ReadAttackRing();
+            }
+
+            xml.ReadEndElement();
+
+            if (daeReader != null)
+            {
+                daeReader.Read(animation);
+            }
+
+            return animation;
+        }
+
+        private void ReadVelocities()
+        {
+            if (!xml.IsStartElement("Velocities"))
+                return;
+
+            if (xml.SkipEmpty())
+                return;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+                animation.Velocities.Add(xml.ReadElementContentAsVector2());
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadPositions()
+        {
+            var xz = new Vector2();
+
+            if (xml.IsStartElement("PositionOffset"))
+            {
+                xml.ReadStartElement();
+                xz.X = xml.ReadElementContentAsFloat("X", ns);
+                xz.Y = xml.ReadElementContentAsFloat("Z", ns);
+                xml.ReadEndElement();
+            }
+
+            ReadRawArray("Positions", animation.Positions, ReadPosition);
+
+            for (int i = 0; i < animation.Positions.Count; i++)
+            {
+                var position = animation.Positions[i];
+                position.X = xz.X;
+                position.Z = xz.Y;
+                xz += animation.Velocities[i];
+            }
+        }
+
+        private void ReadRotations()
+        {
+            var rotations = animation.Rotations;
+
+            xml.ReadStartElement("Rotations");
+
+            while (xml.IsStartElement())
+            {
+                xml.ReadStartElement("Bone");
+
+                var keys = new List<KeyFrame>();
+
+                int count = 0;
+
+                while (xml.IsStartElement())
+                {
+                    string name = xml.LocalName;
+                    string[] tokens = xml.ReadElementContentAsString().Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);
+
+                    var key = new KeyFrame();
+                    key.Duration = XmlConvert.ToByte(tokens[0]);
+
+                    switch (name)
+                    {
+                        case "EKey":
+                            animation.FrameSize = 6;
+                            key.Rotation.X = XmlConvert.ToSingle(tokens[1]);
+                            key.Rotation.Y = XmlConvert.ToSingle(tokens[2]);
+                            key.Rotation.Z = XmlConvert.ToSingle(tokens[3]);
+                            break;
+
+                        case "QKey":
+                            animation.FrameSize = 16;
+                            key.Rotation.X = XmlConvert.ToSingle(tokens[1]);
+                            key.Rotation.Y = XmlConvert.ToSingle(tokens[2]);
+                            key.Rotation.Z = XmlConvert.ToSingle(tokens[3]);
+                            key.Rotation.W = -XmlConvert.ToSingle(tokens[4]);
+                            break;
+
+                        default:
+                            throw new InvalidDataException(string.Format("Unknonw animation key type '{0}'", name));
+                    }
+
+                    count += key.Duration;
+                    keys.Add(key);
+                }
+
+                if (count != animation.Velocities.Count)
+                    throw new InvalidDataException("bad number of frames");
+
+                rotations.Add(keys);
+                xml.ReadEndElement();
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadHeights()
+        {
+            if (!xml.IsStartElement("Heights"))
+                return;
+
+            if (xml.SkipEmpty())
+                return;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+                animation.Heights.Add(xml.ReadElementContentAsFloat("Height", ns));
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadThrowInfo()
+        {
+            if (!xml.IsStartElement("ThrowSource"))
+                return;
+
+            if (xml.SkipEmpty())
+                return;
+
+            animation.ThrowSource = new ThrowInfo();
+            xml.ReadStartElement("ThrowSource");
+            xml.ReadStartElement("TargetAdjustment");
+            animation.ThrowSource.Position = xml.ReadElementContentAsVector3("Position");
+            animation.ThrowSource.Angle = xml.ReadElementContentAsFloat("Angle", ns);
+            xml.ReadEndElement();
+            animation.ThrowSource.Distance = xml.ReadElementContentAsFloat("Distance", ns);
+            animation.ThrowSource.Type = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("TargetType", ns));
+            xml.ReadEndElement();
+        }
+
+        private void ReadAttackRing()
+        {
+            if (!xml.IsStartElement("AttackRing") && !xml.IsStartElement("HorizontalExtents"))
+                return;
+
+            if (animation.Attacks.Count == 0)
+            {
+                Console.Error.WriteLine("Warning: AttackRing found but no attacks are present, ignoring");
+                xml.Skip();
+                return;
+            }
+
+            xml.ReadStartElement();
+
+            for (int i = 0; i < 36; i++)
+                animation.AttackRing[i] = xml.ReadElementContentAsFloat();
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadPosition(Position position)
+        {
+            xml.ReadStartElement("Position");
+            position.Height = xml.ReadElementContentAsFloat("Height", ns);
+            position.YOffset = xml.ReadElementContentAsFloat("YOffset", ns);
+            xml.ReadEndElement();
+        }
+
+        private void Read(Particle particle)
+        {
+            xml.ReadStartElement("Particle");
+            particle.Start = xml.ReadElementContentAsInt("Start", ns);
+            particle.End = xml.ReadElementContentAsInt("End", ns);
+            particle.Bone = MetaEnum.Parse<Bone>(xml.ReadElementContentAsString("Bone", ns));
+            particle.Name = xml.ReadElementContentAsString("Name", ns);
+            xml.ReadEndElement();
+        }
+
+        private void Read(Sound sound)
+        {
+            xml.ReadStartElement("Sound");
+            sound.Name = xml.ReadElementContentAsString("Name", ns);
+            sound.Start = xml.ReadElementContentAsInt("Start", ns);
+            xml.ReadEndElement();
+        }
+
+        private void Read(Shortcut shortcut)
+        {
+            xml.ReadStartElement("Shortcut");
+            shortcut.FromState = MetaEnum.Parse<AnimationState>(xml.ReadElementContentAsString("FromState", ns));
+            shortcut.Length = xml.ReadElementContentAsInt("Length", ns);
+            shortcut.ReplaceAtomic = (xml.ReadElementContentAsString("ReplaceAtomic", ns) == "yes");
+            xml.ReadEndElement();
+        }
+
+        private void Read(Footstep footstep)
+        {
+            xml.ReadStartElement("Footstep");
+
+            string frame = xml.GetAttribute("Frame");
+
+            if (frame != null)
+            {
+                footstep.Frame = XmlConvert.ToInt32(frame);
+                footstep.Type = MetaEnum.Parse<FootstepType>(xml.GetAttribute("Type"));
+            }
+            else
+            {
+                footstep.Frame = xml.ReadElementContentAsInt("Frame", ns);
+                footstep.Type = MetaEnum.Parse<FootstepType>(xml.ReadElementContentAsString("Type", ns));
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void Read(Damage damage)
+        {
+            xml.ReadStartElement("Damage");
+            damage.Points = xml.ReadElementContentAsInt("Points", ns);
+            damage.Frame = xml.ReadElementContentAsInt("Frame", ns);
+            xml.ReadEndElement();
+        }
+
+        private void Read(MotionBlur d)
+        {
+            xml.ReadStartElement("MotionBlur");
+            d.Bones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("Bones", ns));
+            d.Start = xml.ReadElementContentAsInt("Start", ns);
+            d.End = xml.ReadElementContentAsInt("End", ns);
+            d.Lifetime = xml.ReadElementContentAsInt("Lifetime", ns);
+            d.Alpha = xml.ReadElementContentAsInt("Alpha", ns);
+            d.Interval = xml.ReadElementContentAsInt("Interval", ns);
+            xml.ReadEndElement();
+        }
+
+        private void Read(Attack attack)
+        {
+            xml.ReadStartElement("Attack");
+            attack.Start = xml.ReadElementContentAsInt("Start", ns);
+            attack.End = xml.ReadElementContentAsInt("End", ns);
+            attack.Bones = MetaEnum.Parse<BoneMask>(xml.ReadElementContentAsString("Bones", ns));
+            attack.Flags = MetaEnum.Parse<AttackFlags>(xml.ReadElementContentAsString("Flags", ns));
+            attack.Knockback = xml.ReadElementContentAsFloat("Knockback", ns);
+            attack.HitPoints = xml.ReadElementContentAsInt("HitPoints", ns);
+            attack.HitType = MetaEnum.Parse<AnimationType>(xml.ReadElementContentAsString("HitType", ns));
+            attack.HitLength = xml.ReadElementContentAsInt("HitLength", ns);
+            attack.StunLength = xml.ReadElementContentAsInt("StunLength", ns);
+            attack.StaggerLength = xml.ReadElementContentAsInt("StaggerLength", ns);
+
+            if (xml.IsStartElement("Extents"))
+            {
+                ReadRawArray("Extents", attack.Extents, Read);
+
+                if (attack.Extents.Count != attack.End - attack.Start + 1)
+                    Console.Error.WriteLine("Error: Attack starting at frame {0} has an incorrect number of extents ({1})", attack.Start, attack.Extents.Count);
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void Read(AttackExtent extent)
+        {
+            xml.ReadStartElement("Extent");
+            extent.Angle = xml.ReadElementContentAsFloat("Angle", ns);
+            extent.Length = xml.ReadElementContentAsFloat("Length", ns);
+            extent.MinY = xml.ReadElementContentAsFloat("MinY", ns);
+            extent.MaxY = xml.ReadElementContentAsFloat("MaxY", ns);
+            xml.ReadEndElement();
+        }
+
+        private void ReadRawArray<T>(string name, List<T> list, Action<T> elementReader)
+            where T : new()
+        {
+            if (xml.SkipEmpty())
+                return;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                T t = new T();
+                elementReader(t);
+                list.Add(t);
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ImportDaeAnimation()
+        {
+            string filePath = xml.GetAttribute("Path");
+            bool empty = xml.SkipEmpty();
+
+            if (!empty)
+            {
+                xml.ReadStartElement();
+
+                if (filePath == null)
+                    filePath = xml.ReadElementContentAsString("Path", ns);
+            }
+
+            filePath = Path.Combine(basePath, filePath);
+
+            if (!File.Exists(filePath))
+            {
+                Console.Error.WriteLine("Could not find animation import source file '{0}'", filePath);
+                return;
+            }
+
+            Console.WriteLine("Importing {0}", filePath);
+
+            daeReader = new AnimationDaeReader();
+            daeReader.Scene = Dae.Reader.ReadFile(filePath);
+
+            if (!empty)
+            {
+                if (xml.IsStartElement("Start"))
+                    daeReader.StartFrame = xml.ReadElementContentAsInt("Start", ns);
+
+                if (xml.IsStartElement("End"))
+                    daeReader.EndFrame = xml.ReadElementContentAsInt("End", ns);
+
+                xml.ReadEndElement();
+            }
+        }
+    }
+}
Index: /OniSplit/Totoro/AnimationXmlWriter.cs
===================================================================
--- /OniSplit/Totoro/AnimationXmlWriter.cs	(revision 1114)
+++ /OniSplit/Totoro/AnimationXmlWriter.cs	(revision 1114)
@@ -0,0 +1,338 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Totoro
+{
+    internal class AnimationXmlWriter
+    {
+        private Animation animation;
+        private XmlWriter xml;
+        private string daeFileName;
+        private int startFrame;
+        private int endFrame;
+
+        private AnimationXmlWriter()
+        {
+        }
+
+        public static void Write(Animation animation, XmlWriter xml, string daeFileName, int startFrame, int endFrame)
+        {
+            var writer = new AnimationXmlWriter {
+                xml = xml,
+                animation = animation,
+                daeFileName = daeFileName,
+                startFrame = startFrame,
+                endFrame = endFrame
+            };
+
+            writer.Write();
+        }
+
+        private void Write()
+        {
+            xml.WriteStartElement("Animation");
+
+            if (daeFileName != null)
+            {
+                xml.WriteStartElement("Import");
+                xml.WriteAttributeString("Path", daeFileName);
+
+                if (startFrame > 0 || endFrame > 0)
+                    xml.WriteElementString("Start", XmlConvert.ToString(startFrame));
+
+                if (endFrame > 0)
+                    xml.WriteElementString("End", XmlConvert.ToString(endFrame));
+
+                xml.WriteEndElement();
+            }
+
+            xml.WriteStartElement("Lookup");
+            xml.WriteElementString("Type", animation.Type.ToString());
+            xml.WriteElementString("AimingType", animation.AimingType.ToString());
+            xml.WriteElementString("FromState", animation.FromState.ToString());
+            xml.WriteElementString("ToState", animation.ToState.ToString());
+            xml.WriteElementString("Varient", MetaEnum.ToString(animation.Varient));
+            xml.WriteElementString("FirstLevel", XmlConvert.ToString(animation.FirstLevelAvailable));
+            WriteRawArray("Shortcuts", animation.Shortcuts, Write);
+            xml.WriteEndElement();
+
+            xml.WriteElementString("Flags", MetaEnum.ToString(animation.Flags));
+
+            xml.WriteStartElement("Atomic");
+            xml.WriteElementString("Start", XmlConvert.ToString(animation.AtomicStart));
+            xml.WriteElementString("End", XmlConvert.ToString(animation.AtomicEnd));
+            xml.WriteEndElement();
+
+            xml.WriteStartElement("Invulnerable");
+            xml.WriteElementString("Start", XmlConvert.ToString(animation.InvulnerableStart));
+            xml.WriteElementString("End", XmlConvert.ToString(animation.InvulnerableEnd));
+            xml.WriteEndElement();
+
+            xml.WriteStartElement("Overlay");
+            xml.WriteElementString("UsedBones", MetaEnum.ToString(animation.OverlayUsedBones));
+            xml.WriteElementString("ReplacedBones", MetaEnum.ToString(animation.OverlayReplacedBones));
+            xml.WriteEndElement();
+
+            xml.WriteStartElement("DirectAnimations");
+            xml.WriteElementString("Link", animation.DirectAnimations[0]);
+            xml.WriteElementString("Link", animation.DirectAnimations[1]);
+            xml.WriteEndElement();
+            xml.WriteStartElement("Pause");
+            xml.WriteElementString("Hard", XmlConvert.ToString(animation.HardPause));
+            xml.WriteElementString("Soft", XmlConvert.ToString(animation.SoftPause));
+            xml.WriteEndElement();
+            xml.WriteStartElement("Interpolation");
+            xml.WriteElementString("End", XmlConvert.ToString(animation.InterpolationEnd));
+            xml.WriteElementString("Max", XmlConvert.ToString(animation.InterpolationMax));
+            xml.WriteEndElement();
+
+            xml.WriteElementString("FinalRotation", XmlConvert.ToString(MathHelper.ToDegrees(animation.FinalRotation)));
+            xml.WriteElementString("Direction", animation.Direction.ToString());
+            xml.WriteElementString("Vocalization", XmlConvert.ToString(animation.Vocalization));
+            xml.WriteElementString("ActionFrame", XmlConvert.ToString(animation.ActionFrame));
+            xml.WriteElementString("Impact", animation.Impact);
+
+            WriteRawArray("Particles", animation.Particles, Write);
+            WriteRawArray("MotionBlur", animation.MotionBlur, Write);
+            WriteRawArray("Footsteps", animation.Footsteps, Write);
+            WriteRawArray("Sounds", animation.Sounds, Write);
+
+            if (daeFileName == null)
+            {
+                WriteHeights();
+                WriteVelocities();
+                WriteRotations();
+                WritePositions();
+            }
+
+            WriteThrowInfo();
+            WriteRawArray("SelfDamage", animation.SelfDamage, Write);
+
+            if (animation.Attacks.Count > 0)
+            {
+                WriteRawArray("Attacks", animation.Attacks, Write);
+
+                if (daeFileName == null)
+                {
+                    WriteAttackRing();
+                }
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteRotations()
+        {
+            xml.WriteStartElement("Rotations");
+
+            foreach (var keys in animation.Rotations)
+            {
+                xml.WriteStartElement("Bone");
+
+                foreach (var key in keys)
+                {
+                    if (animation.FrameSize == 6)
+                    {
+                        xml.WriteElementString("EKey", string.Format("{0} {1} {2} {3}",
+                            XmlConvert.ToString(key.Duration),
+                            XmlConvert.ToString(key.Rotation.X),
+                            XmlConvert.ToString(key.Rotation.Y),
+                            XmlConvert.ToString(key.Rotation.Z)));
+                    }
+                    else if (animation.FrameSize == 16)
+                    {
+                        xml.WriteElementString("QKey", string.Format("{0} {1} {2} {3} {4}",
+                            XmlConvert.ToString(key.Duration),
+                            XmlConvert.ToString(key.Rotation.X),
+                            XmlConvert.ToString(key.Rotation.Y),
+                            XmlConvert.ToString(key.Rotation.Z),
+                            XmlConvert.ToString(-key.Rotation.W)));
+                    }
+                }
+
+                xml.WriteEndElement();
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WritePositions()
+        {
+            var positions = animation.Positions;
+
+            xml.WriteStartElement("PositionOffset");
+            xml.WriteElementString("X", XmlConvert.ToString(positions.Count == 0 ? 0.0f : positions[0].X));
+            xml.WriteElementString("Z", XmlConvert.ToString(positions.Count == 0 ? 0.0f : positions[0].Z));
+            xml.WriteEndElement();
+
+            WriteRawArray("Positions", animation.Positions, Write);
+        }
+
+        private void WriteHeights()
+        {
+            xml.WriteStartElement("Heights");
+
+            foreach (float height in animation.Heights)
+            {
+                xml.WriteElementString("Height", XmlConvert.ToString(height));
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteVelocities()
+        {
+            xml.WriteStartElement("Velocities");
+
+            foreach (Vector2 velocity in animation.Velocities)
+            {
+                xml.WriteElementString("Velocity", string.Format("{0} {1}",
+                    XmlConvert.ToString(velocity.X),
+                    XmlConvert.ToString(velocity.Y)));
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteAttackRing()
+        {
+            xml.WriteStartElement("AttackRing");
+
+            for (int i = 0; i < 36; i++)
+                xml.WriteElementString("Length", XmlConvert.ToString(animation.AttackRing[i]));
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteThrowInfo()
+        {
+            xml.WriteStartElement("ThrowSource");
+
+            ThrowInfo info = animation.ThrowSource;
+
+            if (info != null)
+            {
+                xml.WriteStartElement("TargetAdjustment");
+                xml.WriteElementString("Position", string.Format("{0} {1} {2}",
+                    XmlConvert.ToString(info.Position.X),
+                    XmlConvert.ToString(info.Position.Y),
+                    XmlConvert.ToString(info.Position.Z)));
+                xml.WriteElementString("Angle", XmlConvert.ToString(info.Angle));
+                xml.WriteEndElement();
+
+                xml.WriteElementString("Distance", XmlConvert.ToString(info.Distance));
+                xml.WriteElementString("TargetType", info.Type.ToString());
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void Write(Position position)
+        {
+            xml.WriteStartElement("Position");
+            xml.WriteElementString("Height", XmlConvert.ToString(position.Height));
+            xml.WriteElementString("YOffset", XmlConvert.ToString(position.YOffset));
+            xml.WriteEndElement();
+        }
+
+        private void Write(AttackExtent extent)
+        {
+            xml.WriteStartElement("Extent");
+            xml.WriteElementString("Angle", XmlConvert.ToString(extent.Angle));
+            xml.WriteElementString("Length", XmlConvert.ToString(extent.Length));
+            xml.WriteElementString("MinY", XmlConvert.ToString(extent.MinY));
+            xml.WriteElementString("MaxY", XmlConvert.ToString(extent.MaxY));
+            xml.WriteEndElement();
+        }
+
+        private void Write(Attack attack)
+        {
+            xml.WriteStartElement("Attack");
+            xml.WriteElementString("Start", XmlConvert.ToString(attack.Start));
+            xml.WriteElementString("End", XmlConvert.ToString(attack.End));
+            xml.WriteElementString("Bones", MetaEnum.ToString(attack.Bones));
+            xml.WriteElementString("Flags", MetaEnum.ToString(attack.Flags));
+            xml.WriteElementString("Knockback", XmlConvert.ToString(attack.Knockback));
+            xml.WriteElementString("HitPoints", XmlConvert.ToString(attack.HitPoints));
+            xml.WriteElementString("HitType", attack.HitType.ToString());
+            xml.WriteElementString("HitLength", XmlConvert.ToString(attack.HitLength));
+            xml.WriteElementString("StunLength", XmlConvert.ToString(attack.StunLength));
+            xml.WriteElementString("StaggerLength", XmlConvert.ToString(attack.StaggerLength));
+
+            if (daeFileName == null)
+            {
+                WriteRawArray("Extents", attack.Extents, Write);
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void Write(Particle particle)
+        {
+            xml.WriteStartElement("Particle");
+            xml.WriteElementString("Start", XmlConvert.ToString(particle.Start));
+            xml.WriteElementString("End", XmlConvert.ToString(particle.End));
+            xml.WriteElementString("Bone", particle.Bone.ToString());
+            xml.WriteElementString("Name", particle.Name);
+            xml.WriteEndElement();
+        }
+
+        private void Write(Damage damage)
+        {
+            xml.WriteStartElement("Damage");
+            xml.WriteElementString("Points", XmlConvert.ToString(damage.Points));
+            xml.WriteElementString("Frame", XmlConvert.ToString(damage.Frame));
+            xml.WriteEndElement();
+        }
+
+        private void Write(Sound sound)
+        {
+            xml.WriteStartElement("Sound");
+            xml.WriteElementString("Name", sound.Name);
+            xml.WriteElementString("Start", XmlConvert.ToString(sound.Start));
+            xml.WriteEndElement();
+        }
+
+        private void Write(Footstep footstep)
+        {
+            xml.WriteStartElement("Footstep");
+            xml.WriteElementString("Frame", XmlConvert.ToString(footstep.Frame));
+            xml.WriteElementString("Type", MetaEnum.ToString(footstep.Type));
+            xml.WriteEndElement();
+        }
+
+        private void Write(Shortcut shortcut)
+        {
+            xml.WriteStartElement("Shortcut");
+            xml.WriteElementString("FromState", shortcut.FromState.ToString());
+            xml.WriteElementString("Length", XmlConvert.ToString(shortcut.Length));
+            xml.WriteElementString("ReplaceAtomic", shortcut.ReplaceAtomic ? "yes" : "no");
+            xml.WriteEndElement();
+        }
+
+        private void Write(MotionBlur motionBlur)
+        {
+            xml.WriteStartElement("MotionBlur");
+            xml.WriteElementString("Bones", MetaEnum.ToString(motionBlur.Bones));
+            xml.WriteElementString("Start", XmlConvert.ToString(motionBlur.Start));
+            xml.WriteElementString("End", XmlConvert.ToString(motionBlur.End));
+            xml.WriteElementString("Lifetime", XmlConvert.ToString(motionBlur.Lifetime));
+            xml.WriteElementString("Alpha", XmlConvert.ToString(motionBlur.Alpha));
+            xml.WriteElementString("Interval", XmlConvert.ToString(motionBlur.Interval));
+            xml.WriteEndElement();
+        }
+
+        private void WriteRawArray<T>(string name, List<T> list, Action<T> elementWriter)
+        {
+            xml.WriteStartElement(name);
+
+            foreach (T t in list)
+                elementWriter(t);
+
+            xml.WriteEndElement();
+        }
+    }
+}
Index: /OniSplit/Totoro/Attack.cs
===================================================================
--- /OniSplit/Totoro/Attack.cs	(revision 1114)
+++ /OniSplit/Totoro/Attack.cs	(revision 1114)
@@ -0,0 +1,20 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Totoro
+{
+    internal class Attack
+    {
+        public int Start;
+        public int End;
+        public BoneMask Bones;
+        public AttackFlags Flags;
+        public float Knockback;
+        public int HitPoints;
+        public AnimationType HitType;
+        public int HitLength;
+        public int StunLength;
+        public int StaggerLength;
+        public readonly List<AttackExtent> Extents = new List<AttackExtent>();
+    }
+}
Index: /OniSplit/Totoro/AttackExtent.cs
===================================================================
--- /OniSplit/Totoro/AttackExtent.cs	(revision 1114)
+++ /OniSplit/Totoro/AttackExtent.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    internal class AttackExtent
+    {
+        public float Angle;
+        public float Length;
+        public float MinY;
+        public float MaxY;
+    }
+}
Index: /OniSplit/Totoro/AttackFlags.cs
===================================================================
--- /OniSplit/Totoro/AttackFlags.cs	(revision 1114)
+++ /OniSplit/Totoro/AttackFlags.cs	(revision 1114)
@@ -0,0 +1,14 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    [Flags]
+    internal enum AttackFlags
+    {
+        None = 0,
+        Unblockable = 1,
+        High = 2,
+        Low = 4,
+        HalfDamage = 8
+    }
+}
Index: /OniSplit/Totoro/Body.cs
===================================================================
--- /OniSplit/Totoro/Body.cs	(revision 1114)
+++ /OniSplit/Totoro/Body.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Totoro
+{
+    internal class Body
+    {
+        public readonly List<BodyNode> Nodes = new List<BodyNode>();
+
+        public BodyNode Root => Nodes[0];
+    }
+}
Index: /OniSplit/Totoro/BodyDaeImporter.cs
===================================================================
--- /OniSplit/Totoro/BodyDaeImporter.cs	(revision 1114)
+++ /OniSplit/Totoro/BodyDaeImporter.cs	(revision 1114)
@@ -0,0 +1,48 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Oni.Totoro
+{
+    internal class BodyDaeImporter
+    {
+        private readonly bool generateNormals;
+        private readonly bool flatNormals;
+        private readonly float shellOffset;
+
+        public BodyDaeImporter(string[] args)
+        {
+            foreach (string arg in args)
+            {
+                if (arg == "-normals")
+                {
+                    generateNormals = true;
+                }
+                else if (arg == "-flat")
+                {
+                    flatNormals = true;
+                }
+                else if (arg == "-cel" || arg.StartsWith("-cel:", StringComparison.Ordinal))
+                {
+                    int i = arg.IndexOf(':');
+
+                    if (i != -1)
+                        shellOffset = float.Parse(arg.Substring(i + 1), CultureInfo.InvariantCulture);
+                    else
+                        shellOffset = 0.07f;
+                }
+            }
+        }
+
+        public ImporterDescriptor Import(string filePath, ImporterFile importer)
+        {
+            var scene = Dae.Reader.ReadFile(filePath);
+
+            Dae.FaceConverter.Triangulate(scene);
+
+            var body = BodyDaeReader.Read(scene, generateNormals, flatNormals, shellOffset);
+
+            return BodyDatWriter.Write(body, importer);
+        }
+    }
+}
Index: /OniSplit/Totoro/BodyDaeReader.cs
===================================================================
--- /OniSplit/Totoro/BodyDaeReader.cs	(revision 1114)
+++ /OniSplit/Totoro/BodyDaeReader.cs	(revision 1114)
@@ -0,0 +1,117 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Oni.Totoro
+{
+    internal class BodyDaeReader
+    {
+        private Body body;
+        private float shellOffset = 0.07f;
+        private bool generateNormals;
+        private bool flatNormals;
+
+        private BodyDaeReader()
+        {
+        }
+
+        public static Body Read(Dae.Scene scene)
+        {
+            var reader = new BodyDaeReader
+            {
+                body = new Body()
+            };
+
+            reader.ReadBodyParts(scene);
+            return reader.body;
+        }
+
+        public static Body Read(Dae.Scene scene, bool generateNormals, bool flatNormals, float shellOffset)
+        {
+            var reader = new BodyDaeReader
+            {
+                body = new Body(),
+                flatNormals = flatNormals,
+                generateNormals = generateNormals,
+                shellOffset = shellOffset
+            };
+
+            reader.ReadBodyParts(scene);
+            return reader.body;
+        }
+
+        private void ReadBodyParts(Dae.Scene scene)
+        {
+            var rootBodyNode = FindRootNode(scene);
+
+            if (rootBodyNode == null)
+                throw new InvalidDataException("The scene does not contain any geometry nodes.");
+
+            //
+            // Make sure the pelvis is not translated from origin.
+            //
+
+            rootBodyNode.Translation = Vector3.Zero;
+
+            if (body.Nodes.Count != 19)
+                Console.Error.WriteLine("Non standard bone count: {0}", body.Nodes.Count);
+        }
+
+        private BodyNode FindRootNode(Dae.Node daeNode)
+        {
+            if (daeNode.GeometryInstances.Any())
+                return ReadNode(daeNode, null);
+
+            foreach (var childDaeNode in daeNode.Nodes)
+            {
+                var bodyNode = FindRootNode(childDaeNode);
+
+                if (bodyNode != null)
+                    return bodyNode;
+            }
+
+            return null;
+        }
+
+        private BodyNode ReadNode(Dae.Node daeNode, BodyNode parentNode)
+        {
+            var bodyNode = new BodyNode
+            {
+                DaeNode = daeNode,
+                Parent = parentNode,
+                Index = body.Nodes.Count
+            };
+
+            body.Nodes.Add(bodyNode);
+
+            //
+            // Find and read the geometry for this node
+            //
+
+            foreach (var geometryInstance in daeNode.GeometryInstances.Where(n => n.Target != null))
+            {
+                var daeGeometry = geometryInstance.Target;
+
+                if (bodyNode.Geometry != null)
+                    Console.Error.WriteLine("The node {0} contains more than one geometry. Only the first geometry will be used.", daeGeometry.Name);
+
+                bodyNode.Geometry = Motoko.GeometryDaeReader.Read(daeGeometry, generateNormals, flatNormals, shellOffset);
+            }
+
+            //
+            // Extract the translation part of this node's transform
+            //
+
+            bodyNode.Translation = daeNode.Transforms.ToMatrix().Translation;
+
+            //
+            // Read child nodes
+            //
+
+            foreach (var daeChildNode in daeNode.Nodes)
+                bodyNode.Nodes.Add(ReadNode(daeChildNode, parentNode));
+
+            return bodyNode;
+        }
+    }
+}
Index: /OniSplit/Totoro/BodyDaeWriter.cs
===================================================================
--- /OniSplit/Totoro/BodyDaeWriter.cs	(revision 1114)
+++ /OniSplit/Totoro/BodyDaeWriter.cs	(revision 1114)
@@ -0,0 +1,78 @@
+﻿using System;
+using Oni.Motoko;
+
+namespace Oni.Totoro
+{
+    internal class BodyDaeWriter
+    {
+        private static readonly Vector3[] defaultPose =
+        {
+            new Vector3(0.0f, 90.0f, 90.0f),
+
+            new Vector3(0.0f, 180.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+
+            new Vector3(0.0f, 180.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+
+            new Vector3(0.0f, 0.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+
+            new Vector3(90.0f, 90.0f, 90.0f),
+            new Vector3(0.0f, 90.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+            new Vector3(-90.0f, 0.0f, 0.0f),
+
+            new Vector3(-90.0f, -90.0f, 90.0f),
+            new Vector3(0.0f, -90.0f, 0.0f),
+            new Vector3(0.0f, 0.0f, 0.0f),
+            new Vector3(90.0f, 0.0f, 0.0f)
+        };
+
+        private readonly GeometryDaeWriter geometryWriter;
+
+        public BodyDaeWriter(GeometryDaeWriter geometryWriter)
+        {
+            this.geometryWriter = geometryWriter;
+        }
+
+        public Dae.Node Write(Body body, bool noAnimation, InstanceDescriptor[] textures)
+        {
+            return WriteNode(body.Root, noAnimation, textures);
+        }
+
+        private Dae.Node WriteNode(BodyNode bodyNode, bool useDefaultPose, InstanceDescriptor[] textures)
+        {
+            if (textures != null)
+                bodyNode.Geometry.Texture = textures[bodyNode.Index];
+
+            var daeNode = geometryWriter.WriteNode(bodyNode.Geometry, bodyNode.Name);
+
+            daeNode.Transforms.Translate("pos", bodyNode.Translation);
+
+            if (useDefaultPose)
+            {
+                var rot = defaultPose[bodyNode.Index];
+
+                daeNode.Transforms.Rotate("rotX", Vector3.Right, rot.X);
+                daeNode.Transforms.Rotate("rotY", Vector3.Up, rot.Y);
+                daeNode.Transforms.Rotate("rotZ", Vector3.Backward, rot.Z);
+            }
+            else
+            {
+                daeNode.Transforms.Rotate("rotX", Vector3.Right, 0.0f);
+                daeNode.Transforms.Rotate("rotY", Vector3.Up, 0.0f);
+                daeNode.Transforms.Rotate("rotZ", Vector3.Backward, 0.0f);
+            }
+
+            foreach (var childBodyNode in bodyNode.Nodes)
+                daeNode.Nodes.Add(WriteNode(childBodyNode, useDefaultPose, textures));
+
+            return daeNode;
+        }
+    }
+}
Index: /OniSplit/Totoro/BodyDatReader.cs
===================================================================
--- /OniSplit/Totoro/BodyDatReader.cs	(revision 1114)
+++ /OniSplit/Totoro/BodyDatReader.cs	(revision 1114)
@@ -0,0 +1,122 @@
+﻿using System;
+using Oni.Motoko;
+
+namespace Oni.Totoro
+{
+    internal static class BodyDatReader
+    {
+        public static Body Read(InstanceDescriptor source)
+        {
+            InstanceDescriptor trcm = ReadTRCM(source);
+
+            InstanceDescriptor trga;
+            InstanceDescriptor trta;
+            InstanceDescriptor tria;
+
+            using (var reader = trcm.OpenRead(84))
+            {
+                trga = reader.ReadInstance();
+                trta = reader.ReadInstance();
+                tria = reader.ReadInstance();
+            }
+
+            var geometries = ReadTRGA(trga);
+            var translations = ReadTRTA(trta);
+            var indices = ReadTRIA(tria);
+
+            var nodes = new BodyNode[geometries.Length];
+
+            for (int i = 0; i < nodes.Length; i++)
+            {
+                nodes[i] = new BodyNode
+                {
+                    Name = BodyNode.Names[i],
+                    Index = i,
+                    Geometry = geometries[i],
+                    Translation = translations[i],
+                };
+            }
+
+            for (int i = 0; i < nodes.Length; i++)
+            {
+                var node = nodes[i];
+
+                for (int j = indices[i].FirstChildIndex; j != 0; j = indices[j].SiblingIndex)
+                {
+                    nodes[j].Parent = node;
+                    node.Nodes.Add(nodes[j]);
+                }
+            }
+
+            var body = new Body();
+            body.Nodes.AddRange(nodes);
+            return body;
+        }
+
+        private static InstanceDescriptor ReadTRCM(InstanceDescriptor source)
+        {
+            if (source.Template.Tag == TemplateTag.TRCM)
+                return source;
+
+            if (source.Template.Tag == TemplateTag.ONCC)
+                source = Game.CharacterClass.Read(source).Body;
+
+            if (source.Template.Tag != TemplateTag.TRBS)
+                throw new InvalidOperationException(string.Format("Invalid body source type {0}", source.Template.Tag));
+
+            return ReadTRBS(source).Last();
+        }
+
+        private static InstanceDescriptor[] ReadTRBS(InstanceDescriptor trbs)
+        {
+            using (var reader = trbs.OpenRead())
+                return reader.ReadInstanceArray(5);
+        }
+
+        private static Geometry[] ReadTRGA(InstanceDescriptor trga)
+        {
+            InstanceDescriptor[] descriptors;
+
+            using (var reader = trga.OpenRead(22))
+                descriptors = reader.ReadInstanceArray(reader.ReadInt16());
+
+            var geometries = new Geometry[descriptors.Length];
+
+            for (int i = 0; i < descriptors.Length; i++)
+                geometries[i] = GeometryDatReader.Read(descriptors[i]);
+
+            return geometries;
+        }
+
+        private static Vector3[] ReadTRTA(InstanceDescriptor trta)
+        {
+            using (var reader = trta.OpenRead(22))
+                return reader.ReadVector3Array(reader.ReadInt16());
+        }
+
+        private struct NodeIndices
+        {
+            public byte ParentIndex;
+            public byte FirstChildIndex;
+            public byte SiblingIndex;
+        }
+
+        private static NodeIndices[] ReadTRIA(InstanceDescriptor tria)
+        {
+            using (var reader = tria.OpenRead(22))
+            {
+                var indices = new NodeIndices[reader.ReadInt16()];
+
+                for (int i = 0; i < indices.Length; i++)
+                {
+                    indices[i].ParentIndex = reader.ReadByte();
+                    indices[i].FirstChildIndex = reader.ReadByte();
+                    indices[i].SiblingIndex = reader.ReadByte();
+                    reader.Skip(1);
+                }
+
+                return indices;
+            }
+        }
+    }
+}
Index: /OniSplit/Totoro/BodyDatWriter.cs
===================================================================
--- /OniSplit/Totoro/BodyDatWriter.cs	(revision 1114)
+++ /OniSplit/Totoro/BodyDatWriter.cs	(revision 1114)
@@ -0,0 +1,110 @@
+﻿using System;
+using System.Collections.Generic;
+
+namespace Oni.Totoro
+{
+    internal static class BodyDatWriter
+    {
+        public static ImporterDescriptor Write(Body body, ImporterFile importer)
+        {
+            var trcm = importer.CreateInstance(TemplateTag.TRCM);
+            var trga = importer.CreateInstance(TemplateTag.TRGA);
+            var trta = importer.CreateInstance(TemplateTag.TRTA);
+            var tria = importer.CreateInstance(TemplateTag.TRIA);
+
+            var nodes = body.Nodes;
+            int nodeCount = nodes.Count;
+
+            var geometryDescriptors = new ImporterDescriptor[nodeCount];
+            var translations = new Vector3[nodeCount];
+            var indices = new NodeIndices[nodeCount];
+
+            foreach (var node in nodes)
+            {
+                int nodeIndex = node.Index;
+
+                geometryDescriptors[nodeIndex] = Motoko.GeometryDatWriter.Write(node.Geometry, importer);
+                translations[nodeIndex] = node.Translation;
+
+                int childCount = node.Nodes.Count;
+
+                if (childCount > 0)
+                {
+                    indices[nodeIndex].FirstChildIndex = (byte)node.Nodes[0].Index;
+
+                    int lastChildIndex = childCount - 1;
+
+                    for (int i = 0; i < childCount; i++)
+                    {
+                        int childIndex = node.Nodes[i].Index;
+
+                        if (i != lastChildIndex)
+                            indices[childIndex].SiblingIndex = (byte)node.Nodes[i + 1].Index;
+
+                        indices[childIndex].ParentIndex = (byte)nodeIndex;
+                    }
+                }
+            }
+
+            WriteTRCM(trcm, trga, trta, tria, nodeCount);
+            WriteTRGA(trga, geometryDescriptors);
+            WriteTRTA(trta, translations);
+            WriteTRIA(tria, indices);
+
+            return trcm;
+        }
+
+        private static void WriteTRCM(ImporterDescriptor trcm, ImporterDescriptor trga, ImporterDescriptor trta, ImporterDescriptor tria, int nodeCount)
+        {
+            using (var writer = trcm.OpenWrite(4))
+            {
+                writer.WriteInt16(nodeCount);
+                writer.Skip(78);
+                writer.Write(trga);
+                writer.Write(trta);
+                writer.Write(tria);
+            }
+        }
+
+        private static void WriteTRGA(ImporterDescriptor trga, ImporterDescriptor[] descriptors)
+        {
+            using (var writer = trga.OpenWrite(22))
+            {
+                writer.WriteInt16(descriptors.Length);
+                writer.Write(descriptors);
+            }
+        }
+
+        private static void WriteTRTA(ImporterDescriptor trta, Vector3[] translations)
+        {
+            using (var writer = trta.OpenWrite(22))
+            {
+                writer.WriteInt16(translations.Length);
+                writer.Write(translations);
+            }
+        }
+
+        private struct NodeIndices
+        {
+            public byte ParentIndex;
+            public byte FirstChildIndex;
+            public byte SiblingIndex;
+        }
+
+        private static void WriteTRIA(ImporterDescriptor tria, NodeIndices[] indices)
+        {
+            using (var writer = tria.OpenWrite(22))
+            {
+                writer.WriteInt16(indices.Length);
+
+                foreach (var node in indices)
+                {
+                    writer.WriteByte(node.ParentIndex);
+                    writer.WriteByte(node.FirstChildIndex);
+                    writer.WriteByte(node.SiblingIndex);
+                    writer.WriteByte(0);
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Totoro/BodyNode.cs
===================================================================
--- /OniSplit/Totoro/BodyNode.cs	(revision 1114)
+++ /OniSplit/Totoro/BodyNode.cs	(revision 1114)
@@ -0,0 +1,42 @@
+﻿using System;
+using System.Collections.Generic;
+using Oni.Motoko;
+
+namespace Oni.Totoro
+{
+    internal class BodyNode
+    {
+        public static readonly string[] Names =
+        {
+            "pelvis",
+            "left_thigh",
+            "left_calf",
+            "left_foot",
+            "right_thigh",
+            "right_calf",
+            "right_foot",
+            "mid",
+            "chest",
+            "neck",
+            "head",
+            "left_shoulder",
+            "left_biceps",
+            "left_wrist",
+            "left_handfist",
+            "right_shoulder",
+            "right_biceps",
+            "right_wrist",
+            "right_handfist",
+        };
+
+        public string Name;
+        public int Index;
+
+        public BodyNode Parent;
+        public readonly List<BodyNode> Nodes = new List<BodyNode>();
+        public Vector3 Translation;
+        public Geometry Geometry;
+
+        public Dae.Node DaeNode;
+    }
+}
Index: /OniSplit/Totoro/BodySetImporter.cs
===================================================================
--- /OniSplit/Totoro/BodySetImporter.cs	(revision 1114)
+++ /OniSplit/Totoro/BodySetImporter.cs	(revision 1114)
@@ -0,0 +1,41 @@
+﻿using System;
+using System.IO;
+
+namespace Oni.Totoro
+{
+    internal class BodySetImporter : Importer
+    {
+        private BodyDaeImporter bodyImporter;
+
+        public BodySetImporter(string[] args)
+        {
+            bodyImporter = new BodyDaeImporter(args);
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (name.StartsWith("ONCC", StringComparison.Ordinal))
+                name = name.Substring(4);
+
+            BeginImport();
+
+            var trbs = CreateInstance(TemplateTag.TRBS, name);
+            var trcm = bodyImporter.Import(filePath, ImporterFile);
+
+            WriteTRBS(trbs, trcm);
+
+            Write(outputDirPath);
+        }
+
+        private void WriteTRBS(ImporterDescriptor trbs, ImporterDescriptor trcm)
+        {
+            using (var writer = trbs.OpenWrite())
+            {
+                for (int i = 0; i < 5; i++)
+                    writer.Write(trcm);
+            }
+        }
+    }
+}
Index: /OniSplit/Totoro/Bone.cs
===================================================================
--- /OniSplit/Totoro/Bone.cs	(revision 1114)
+++ /OniSplit/Totoro/Bone.cs	(revision 1114)
@@ -0,0 +1,25 @@
+﻿namespace Oni.Totoro
+{
+    internal enum Bone
+    {
+        Pelvis,
+        LeftThigh,
+        LeftCalf,
+        LeftFoot,
+        RightThigh,
+        RightCalf,
+        RightFoot,
+        Mid,
+        Chest,
+        Neck,
+        Head,
+        LeftShoulder,
+        LeftArm,
+        LeftWrist,
+        LeftFist,
+        RightShoulder,
+        RightArm,
+        RightWrist,
+        RightFist
+    }
+}
Index: /OniSplit/Totoro/BoneMask.cs
===================================================================
--- /OniSplit/Totoro/BoneMask.cs	(revision 1114)
+++ /OniSplit/Totoro/BoneMask.cs	(revision 1114)
@@ -0,0 +1,29 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    [Flags]
+    internal enum BoneMask : uint
+    {
+        None = 0x0000,
+        Pelvis = (1u << Bone.Pelvis),
+        LeftThigh = (1u << Bone.LeftThigh),
+        LeftCalf = (1u << Bone.LeftCalf),
+        LeftFoot = (1u << Bone.LeftFoot),
+        RightThigh = (1u << Bone.RightThigh),
+        RightCalf = (1u << Bone.RightCalf),
+        RightFoot = (1u << Bone.RightFoot),
+        Mid = (1u << Bone.Mid),
+        Chest = (1u << Bone.Chest),
+        Neck = (1u << Bone.Neck),
+        Head = (1u << Bone.Head),
+        LeftShoulder = (1u << Bone.LeftShoulder),
+        LeftArm = (1u << Bone.LeftArm),
+        LeftWrist = (1u << Bone.LeftWrist),
+        LeftFist = (1u << Bone.LeftFist),
+        RightShoulder = (1u << Bone.RightShoulder),
+        RightArm = (1u << Bone.RightArm),
+        RightWrist = (1u << Bone.RightWrist),
+        RightFist = (1u << Bone.RightFist)
+    }
+}
Index: /OniSplit/Totoro/Damage.cs
===================================================================
--- /OniSplit/Totoro/Damage.cs	(revision 1114)
+++ /OniSplit/Totoro/Damage.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Totoro
+{
+    internal class Damage
+    {
+        public int Frame;
+        public int Points;
+    }
+}
Index: /OniSplit/Totoro/Direction.cs
===================================================================
--- /OniSplit/Totoro/Direction.cs	(revision 1114)
+++ /OniSplit/Totoro/Direction.cs	(revision 1114)
@@ -0,0 +1,11 @@
+﻿namespace Oni.Totoro
+{
+    internal enum Direction
+    {
+        None,
+        Forward,
+        Backward,
+        Left,
+        Right
+    }
+}
Index: /OniSplit/Totoro/Footstep.cs
===================================================================
--- /OniSplit/Totoro/Footstep.cs	(revision 1114)
+++ /OniSplit/Totoro/Footstep.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Totoro
+{
+    internal class Footstep
+    {
+        public int Frame;
+        public FootstepType Type;
+    }
+}
Index: /OniSplit/Totoro/FootstepType.cs
===================================================================
--- /OniSplit/Totoro/FootstepType.cs	(revision 1114)
+++ /OniSplit/Totoro/FootstepType.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿using System;
+
+namespace Oni.Totoro
+{
+    [Flags]
+    internal enum FootstepType
+    {
+        None = 0,
+        Left = 1,
+        Right = 2
+    }
+}
Index: /OniSplit/Totoro/KeyFrame.cs
===================================================================
--- /OniSplit/Totoro/KeyFrame.cs	(revision 1114)
+++ /OniSplit/Totoro/KeyFrame.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Totoro
+{
+    internal class KeyFrame
+    {
+        public int Duration;
+        public Vector4 Rotation;
+    }
+}
Index: /OniSplit/Totoro/MotionBlur.cs
===================================================================
--- /OniSplit/Totoro/MotionBlur.cs	(revision 1114)
+++ /OniSplit/Totoro/MotionBlur.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Totoro
+{
+    internal class MotionBlur
+    {
+        public BoneMask Bones;
+        public int Start;
+        public int End;
+        public int Lifetime;
+        public int Alpha;
+        public int Interval;
+    }
+}
Index: /OniSplit/Totoro/Particle.cs
===================================================================
--- /OniSplit/Totoro/Particle.cs	(revision 1114)
+++ /OniSplit/Totoro/Particle.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿namespace Oni.Totoro
+{
+    internal class Particle
+    {
+        public int Start;
+        public int End;
+        public Bone Bone;
+        public string Name;
+    }
+}
Index: /OniSplit/Totoro/Position.cs
===================================================================
--- /OniSplit/Totoro/Position.cs	(revision 1114)
+++ /OniSplit/Totoro/Position.cs	(revision 1114)
@@ -0,0 +1,12 @@
+﻿namespace Oni.Totoro
+{
+    internal class Position
+    {
+        public float X;
+        public float Z;
+        public float Height;
+        public float YOffset;
+
+        public Vector2 XZ => new Vector2(X, Z);
+    }
+}
Index: /OniSplit/Totoro/Shortcut.cs
===================================================================
--- /OniSplit/Totoro/Shortcut.cs	(revision 1114)
+++ /OniSplit/Totoro/Shortcut.cs	(revision 1114)
@@ -0,0 +1,9 @@
+﻿namespace Oni.Totoro
+{
+    internal class Shortcut
+    {
+        public AnimationState FromState;
+        public int Length;
+        public bool ReplaceAtomic;
+    }
+}
Index: /OniSplit/Totoro/Sound.cs
===================================================================
--- /OniSplit/Totoro/Sound.cs	(revision 1114)
+++ /OniSplit/Totoro/Sound.cs	(revision 1114)
@@ -0,0 +1,8 @@
+﻿namespace Oni.Totoro
+{
+    internal class Sound
+    {
+        public int Start;
+        public string Name;
+    }
+}
Index: /OniSplit/Totoro/ThrowInfo.cs
===================================================================
--- /OniSplit/Totoro/ThrowInfo.cs	(revision 1114)
+++ /OniSplit/Totoro/ThrowInfo.cs	(revision 1114)
@@ -0,0 +1,10 @@
+﻿namespace Oni.Totoro
+{
+    internal class ThrowInfo
+    {
+        public Vector3 Position;
+        public float Angle;
+        public float Distance;
+        public AnimationType Type;
+    }
+}
Index: /OniSplit/Utils.cs
===================================================================
--- /OniSplit/Utils.cs	(revision 1114)
+++ /OniSplit/Utils.cs	(revision 1114)
@@ -0,0 +1,687 @@
+﻿using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Xml;
+using System.Reflection;
+
+namespace Oni
+{
+#if !NETFX4
+    internal delegate void Action();
+    internal delegate void Action<T>(T arg1);
+    internal delegate void Action<T1, T2>(T1 arg1, T2 args);
+    internal delegate TResult Func<T1, TResult>(T1 arg1);
+    internal delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
+    internal delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
+#endif
+
+    internal static class Utils
+    {
+        private static string version;
+
+        public static string Version
+        {
+            get
+            {
+                if (version == null)
+                {
+#if NETCORE
+                    version = typeof(Utils).GetTypeInfo().Assembly.GetName().Version.ToString();
+#else
+                    version = typeof(Utils).Assembly.GetName().Version.ToString();
+#endif
+                }
+
+                return version;
+            }
+        }
+
+        public static string TagToString(int tag)
+        {
+            var chars = new char[4];
+
+            chars[0] = (char)((tag >> 00) & 0xff);
+            chars[1] = (char)((tag >> 08) & 0xff);
+            chars[2] = (char)((tag >> 16) & 0xff);
+            chars[3] = (char)((tag >> 24) & 0xff);
+
+            return new string(chars);
+        }
+
+        public static int Align4(int value) => (value + 0x03) & ~0x03;
+        public static int Align32(int value) => (value + 0x1f) & ~0x1f;
+
+        public static short ByteSwap(short value)
+        {
+            return (short)((value >> 8) | (value << 8));
+        }
+
+        public static int ByteSwap(int value)
+        {
+            value = (value >> 16) | (value << 16);
+            return ((value >> 8) & 0x00ff00ff) | ((value & 0x00ff00ff) << 8);
+        }
+
+        public static bool ArrayEquals<T>(T[] a1, T[] a2)
+        {
+            if (a1 == a2)
+                return true;
+
+            if (a1 == null || a2 == null)
+                return false;
+
+            if (a1.Length != a2.Length)
+                return false;
+
+            var comparer = EqualityComparer<T>.Default;
+
+            for (int i = 0; i < a1.Length; i++)
+            {
+                if (!comparer.Equals(a1[i], a2[i]))
+                    return false;
+            }
+
+            return true;
+        }
+
+        public static string CleanupTextureName(string name)
+        {
+            name = name.Replace('/', '_');
+
+            if (name == "<none>")
+                name = "none";
+
+            return name;
+        }
+
+        private static readonly char[] wildcards = { '*', '?', '.' };
+
+        private static void WildcardToRegex(string wexp, StringBuilder regexp)
+        {
+            for (int startIndex = 0; startIndex < wexp.Length;)
+            {
+                int i = wexp.IndexOfAny(wildcards, startIndex);
+
+                if (i == -1)
+                {
+                    regexp.Append(wexp, startIndex, wexp.Length - startIndex);
+                    break;
+                }
+
+                regexp.Append(wexp, startIndex, i - startIndex);
+
+                if (wexp[i] == '.')
+                    regexp.Append("\\.");
+                if (wexp[i] == '*')
+                    regexp.Append(".*");
+                else if (wexp[i] == '?')
+                    regexp.Append('.');
+
+                startIndex = i + 1;
+            }
+        }
+
+        public static Regex WildcardToRegex(string wexp)
+        {
+            if (string.IsNullOrEmpty(wexp))
+                return null;
+
+            var regexp = new StringBuilder();
+            WildcardToRegex(wexp, regexp);
+            return new Regex(regexp.ToString(), RegexOptions.Singleline);
+        }
+
+        public static Regex WildcardToRegex(List<string> wexps)
+        {
+            if (wexps.Count == 0)
+                return null;
+
+            var regex = new StringBuilder();
+
+            foreach (var wexp in wexps)
+            {
+                if (regex.Length != 0)
+                    regex.Append('|');
+
+                regex.Append('(');
+                WildcardToRegex(wexp, regex);
+                regex.Append(')');
+            }
+
+            Console.WriteLine(regex.ToString());
+
+            return new Regex(regex.ToString(), RegexOptions.Singleline);
+        }
+
+        private static byte[] buffer1;
+        private static byte[] buffer2;
+
+        public static bool AreFilesEqual(string filePath1, string filePath2)
+        {
+            if (buffer1 == null)
+            {
+                buffer1 = new byte[32768];
+                buffer2 = new byte[32768];
+            }
+
+            using (var s1 = File.OpenRead(filePath1))
+            using (var s2 = File.OpenRead(filePath2))
+            {
+                if (s1.Length != s2.Length)
+                    return false;
+
+                while (true)
+                {
+                    int r1 = s1.Read(buffer1, 0, buffer1.Length);
+                    int r2 = s2.Read(buffer2, 0, buffer2.Length);
+
+                    if (r1 != r2)
+                        return false;
+
+                    if (r1 == 0)
+                        return true;
+
+                    for (int i = 0; i < r1; i++)
+                    {
+                        if (buffer1[i] != buffer2[i])
+                            return false;
+                    }
+                }
+            }
+        }
+
+        
+        public static bool IsFlagsEnum(Type enumType)
+        {
+#if NETCORE
+            return (enumType.GetTypeInfo().GetCustomAttributes(typeof(FlagsAttribute), false).Any());
+#else
+            return (enumType.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0);
+#endif
+        }
+
+        public static void WriteEnum(Type enumType)
+        {
+            bool isFlags = IsFlagsEnum(enumType);
+            Type underlyingType = Enum.GetUnderlyingType(enumType);
+
+            if (isFlags)
+                Console.WriteLine("flags {0}", enumType.Name);
+            else
+                Console.WriteLine("enum {0}", enumType.Name);
+
+            foreach (string name in Enum.GetNames(enumType))
+            {
+                object value = Enum.Parse(enumType, name);
+
+                if (isFlags)
+                {
+                    if (underlyingType == typeof(ulong))
+                        Console.WriteLine("\t{0} = 0x{1:X16}", name, Convert.ToUInt64(value));
+                    else
+                        Console.WriteLine("\t{0} = 0x{1:X8}", name, Convert.ToUInt32(value));
+                }
+                else
+                {
+                    if (underlyingType == typeof(ulong))
+                        Console.WriteLine("\t{0} = {1}", name, Convert.ToUInt64(value));
+                    else
+                        Console.WriteLine("\t{0} = {1}", name, Convert.ToInt32(value));
+                }
+            }
+
+            Console.WriteLine();
+        }
+
+        public static IEnumerable<T> Reverse<T>(this IList<T> list)
+        {
+            for (int i = list.Count - 1; i >= 0; i--)
+                yield return list[i];
+        }
+
+        public static T First<T>(this IList<T> list) => list[0];
+
+        public static T Last<T>(this IList<T> list) => list[list.Count - 1];
+
+        public static T LastOrDefault<T>(this List<T> list)
+        {
+            if (list.Count == 0)
+                return default(T);
+
+            return list[list.Count - 1];
+        }
+
+        public static float[] Negate(this float[] values)
+        {
+            float[] result = new float[values.Length];
+
+            for (int i = 0; i < values.Length; i++)
+                result[i] = -values[i];
+
+            return result;
+        }
+
+        public static string CommonPrefix(List<string> strings)
+        {
+            string first = strings[0];
+
+            for (int prefixLength = 0; prefixLength < first.Length; prefixLength++)
+            {
+                for (int i = 1; i < strings.Count; i++)
+                {
+                    string s = strings[i];
+
+                    if (prefixLength >= s.Length || first[prefixLength] != s[prefixLength])
+                        return first.Substring(0, prefixLength);
+                }
+            }
+
+            return first;
+        }
+
+        public static void SkipSequence(this XmlReader xml, string name)
+        {
+            while (xml.IsStartElement(name))
+                xml.Skip();
+        }
+
+        public static bool SkipEmpty(this XmlReader xml)
+        {
+            if (!xml.IsEmptyElement)
+                return false;
+
+            xml.Skip();
+            return true;
+        }
+
+        //public static int CommonPrefixLength(string s1, string s2)
+        //{
+        //    int length = Math.Min(s1.Length, s2.Length);
+
+        //    for (int i = 0; i < length; i++)
+        //    {
+        //        if (s1[i] != s2[i])
+        //            return i;
+        //    }
+
+        //    return length;
+        //}
+    }
+
+    /// <summary>
+    /// A couple of IEnumerable extensions so we can run even on .NET 2.0
+    /// </summary>
+    internal static class Enumerable
+    {
+        public static bool Any<T>(this IEnumerable<T> source)
+        {
+            foreach (var t in source)
+                return true;
+
+            return false;
+        }
+
+        public static bool Any<T>(this IEnumerable<T> source, Func<T, bool> predicate)
+        {
+            foreach (var t in source)
+            {
+                if (predicate(t))
+                    return true;
+            }
+
+            return false;
+        }
+
+        public static bool All<T>(this IEnumerable<T> source, Func<T, bool> predicate)
+        {
+            foreach (var t in source)
+            {
+                if (!predicate(t))
+                    return false;
+            }
+
+            return true;
+        }
+
+        public static IEnumerable<T> OfType<T>(this System.Collections.IEnumerable source)
+            where T : class
+        {
+            foreach (var obj in source)
+            {
+                var t = obj as T;
+
+                if (t != null)
+                    yield return t;
+            }
+        }
+
+        public static IEnumerable<T> Distinct<T>(this IEnumerable<T> source)
+            where T : class
+        {
+            var set = new Dictionary<T, bool>();
+            bool hasNull = false;
+
+            foreach (var t in source)
+            {
+                if (t == null)
+                {
+                    if (!hasNull)
+                    {
+                        hasNull = true;
+                        yield return null;
+                    }
+                }
+                else if (!set.ContainsKey(t))
+                {
+                    set.Add(t, true);
+                    yield return t;
+                }
+            }
+        }
+
+        public static int Count<T>(this IEnumerable<T> source, Func<T, bool> predicate)
+        {
+            var count = 0;
+
+            foreach (var t in source)
+                if (predicate(t))
+                    count++;
+
+            return count;
+        }
+
+        public static IEnumerable<T> Concatenate<T>(this IEnumerable<T> first, IEnumerable<T> second)
+        {
+            foreach (var t in first)
+                yield return t;
+
+            foreach (var t in second)
+                yield return t;
+        }
+
+        public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
+        {
+            foreach (var t in source)
+            {
+                if (predicate(t))
+                    yield return t;
+            }
+        }
+
+        public static bool IsEmpty<T>(this IEnumerable<T> source)
+        {
+            foreach (var t in source)
+                return true;
+
+            return false;
+        }
+
+        public static T First<T>(this IEnumerable<T> source)
+        {
+            foreach (var t in source)
+                return t;
+
+            throw new InvalidOperationException();
+        }
+
+        public static T First<T>(this IEnumerable<T> source, Func<T, bool> predicate)
+        {
+            foreach (var t in source)
+            {
+                if (predicate(t))
+                    return t;
+            }
+
+            throw new InvalidOperationException();
+        }
+
+        public static T FirstOrDefault<T>(this IEnumerable<T> source)
+        {
+            foreach (var t in source)
+                return t;
+
+            return default(T);
+        }
+
+        public static T FirstOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate)
+        {
+            foreach (var t in source)
+            {
+                if (predicate(t))
+                    return t;
+            }
+
+            return default(T);
+        }
+
+        public static IEnumerable<TOut> Select<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, TOut> selector)
+        {
+            foreach (var t in source)
+                yield return selector(t);
+        }
+
+        public static IEnumerable<TOut> SelectMany<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, IEnumerable<TOut>> selector)
+        {
+            foreach (var tin in source)
+            {
+                foreach (var tout in selector(tin))
+                    yield return tout;
+            }
+        }
+
+        public static float Max(this IEnumerable<float> source)
+        {
+            var max = float.MinValue;
+
+            foreach (var value in source)
+                max = Math.Max(max, value);
+
+            return max;
+        }
+
+        public static float Min<T>(this IEnumerable<T> source, Func<T, float> selector)
+        {
+            var min = float.MaxValue;
+
+            foreach (var value in source)
+                min = Math.Min(min, selector(value));
+
+            return min;
+        }
+
+        public static int Min<T>(this IEnumerable<T> source, Func<T, int> selector)
+        {
+            var min = int.MaxValue;
+
+            foreach (var value in source)
+                min = Math.Min(min, selector(value));
+
+            return min;
+        }
+
+        public static float Min(this IEnumerable<float> source)
+        {
+            var min = float.MaxValue;
+
+            foreach (var value in source)
+                min = Math.Min(min, value);
+
+            return min;
+        }
+
+        public static float Max<T>(this IEnumerable<T> source, Func<T, float> selector)
+        {
+            var max = float.MinValue;
+
+            foreach (var value in source)
+                max = Math.Max(max, selector(value));
+
+            return max;
+        }
+
+        public static int Max(this IEnumerable<int> source)
+        {
+            int max = int.MinValue;
+
+            foreach (var value in source)
+            {
+                if (value > max)
+                    max = value;
+            }
+
+            return max;
+        }
+
+        public static int Max<T>(this IEnumerable<T> source, Func<T, int> selector)
+        {
+            int max = int.MinValue;
+
+            foreach (var item in source)
+            {
+                var value = selector(item);
+
+                if (value > max)
+                    max = value;
+            }
+
+            return max;
+        }
+
+        public static TOutput[] ConvertAll<TInput, TOutput>(this TInput[] input, Func<TInput, TOutput> converter)
+        {
+            var output = new TOutput[input.Length];
+
+            for (int i = 0; i < output.Length; i++)
+                output[i] = converter(input[i]);
+
+            return output;
+        }
+
+        public static int Sum<T>(this IEnumerable<T> source, Func<T, int> selector)
+        {
+            int sum = 0;
+
+            foreach (var value in source)
+                sum += selector(value);
+
+            return sum;
+        }
+
+        public static IEnumerable<T> Repeat<T>(T value, int count)
+        {
+            for (int i = 0; i < count; i++)
+                yield return value;
+        }
+
+        public static T[] ToArray<T>(this IEnumerable<T> source)
+        {
+            var collection = source as ICollection<T>;
+
+            if (collection != null)
+            {
+                var result = new T[collection.Count];
+                collection.CopyTo(result, 0);
+                return result;
+            }
+
+            return new List<T>(source).ToArray();
+        }
+
+        public static List<T> ToList<T>(this IEnumerable<T> source) => new List<T>(source);
+
+        public static IEnumerable<T> Ring<T>(this IEnumerable<T> source)
+        {
+            foreach (T t in source)
+            {
+                yield return t;
+            }
+
+            foreach (T t in source)
+            {
+                yield return t;
+                break;
+            }
+        }
+
+        public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, int count)
+        {
+            foreach (T t in source)
+            {
+                if (count <= 0)
+                    yield return t;
+
+                count--;
+            }
+        }
+    }
+
+    internal struct EmptyArray<T>
+    {
+        public static readonly T[] Value = new T[0];
+    }
+
+    internal struct ReadOnlyArray<T>
+    {
+        private readonly T[] array;
+
+        public ReadOnlyArray(T[] array)
+        {
+            this.array = array;
+        }
+
+        public int Length => array.Length;
+        public T this[int index] => array[index];
+    }
+
+    internal class TreeIterator<T> : IEnumerable<T>
+    {
+        private readonly IEnumerable<T> roots;
+        private readonly Func<T, IEnumerable<T>> children;
+
+        public TreeIterator(IEnumerable<T> roots, Func<T, IEnumerable<T>> children)
+        {
+            this.roots = roots;
+            this.children = children;
+        }
+
+        public class Enumerator : IEnumerator<T>
+        {
+            public bool MoveNext()
+            {
+                throw new NotImplementedException();
+            }
+
+            public T Current
+            {
+                get { throw new NotImplementedException(); }
+            }
+
+            object IEnumerator.Current => Current;
+
+            public void Dispose()
+            {
+            }
+
+            public void Reset()
+            {
+                throw new NotSupportedException();
+            }
+        }
+
+        public IEnumerator<T> GetEnumerator() => new Enumerator();
+        IEnumerator IEnumerable.GetEnumerator() => new Enumerator();
+    }
+}
+
+#if !NETFX4
+namespace System.Runtime.CompilerServices
+{
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+    internal sealed class ExtensionAttribute : Attribute
+    {
+    }
+}
+#endif
Index: /OniSplit/Xml/FilmToXmlConverter.cs
===================================================================
--- /OniSplit/Xml/FilmToXmlConverter.cs	(revision 1114)
+++ /OniSplit/Xml/FilmToXmlConverter.cs	(revision 1114)
@@ -0,0 +1,89 @@
+﻿using System;
+using System.IO;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    internal class FilmToXmlConverter : RawXmlExporter
+    {
+        public FilmToXmlConverter(BinaryReader reader, XmlWriter writer)
+            : base(reader, writer)
+        {
+        }
+
+        private static readonly MetaType filmHeader = new MetaStruct("FilmHeader",
+            new Field(MetaType.Vector3, "Position"),
+            new Field(MetaType.Float, "Facing"),
+            new Field(MetaType.Float, "DesiredFacing"),
+            new Field(MetaType.Float, "HeadFacing"),
+            new Field(MetaType.Float, "HeadPitch"),
+            new Field(MetaType.Int32, "FrameCount"),
+            new Field(MetaType.Padding(28)));
+
+        private static readonly MetaType filmAnimations = new MetaStruct("FilmAnimations",
+            new Field(MetaType.Array(2, MetaType.String128), "Animations"));
+
+        private static readonly MetaType filmFrames = new MetaStruct("FilmFrames",
+            new Field(MetaType.VarArray(new MetaStruct("Frame",
+                new Field(MetaType.Vector2, "MouseDelta"),
+                new Field(MetaType.Enum<InstanceMetadata.FILMKeys>(), "Keys"),
+                new Field(MetaType.Int32, "Frame"),
+                new Field(MetaType.Padding(4))
+            )), "Frames")
+        );
+
+        public static void Convert(string filePath, string outputDirPath)
+        {
+            using (var reader = new BinaryReader(filePath, true))
+            using (var writer = CreateXmlWriter(Path.Combine(outputDirPath, Path.GetFileNameWithoutExtension(filePath) + ".xml")))
+            {
+                writer.WriteStartElement("Instance");
+                writer.WriteAttributeString("id", "0");
+                writer.WriteAttributeString("type", "FILM");
+
+                var converter = new FilmToXmlConverter(reader, writer);
+
+                reader.Position = filmAnimations.Size;
+                filmHeader.Accept(converter);
+
+                reader.Position = 0;
+                filmAnimations.Accept(converter);
+
+                reader.Position = filmAnimations.Size + filmHeader.Size;
+                filmFrames.Accept(converter);
+
+                writer.WriteEndElement();
+            }
+        }
+
+        private static XmlWriter CreateXmlWriter(string filePath)
+        {
+            var settings = new XmlWriterSettings
+            {
+                CloseOutput = true,
+                Indent = true,
+                IndentChars = "    "
+            };
+
+            var stream = File.Create(filePath);
+            var writer = XmlWriter.Create(stream, settings);
+
+            try
+            {
+                writer.WriteStartElement("Oni");
+            }
+            catch
+            {
+#if NETCORE
+                writer.Dispose();
+#else
+                writer.Close();
+#endif
+                throw;
+            }
+
+            return writer;
+        }
+    }
+}
Index: /OniSplit/Xml/GenericXmlWriter.cs
===================================================================
--- /OniSplit/Xml/GenericXmlWriter.cs	(revision 1114)
+++ /OniSplit/Xml/GenericXmlWriter.cs	(revision 1114)
@@ -0,0 +1,254 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    internal class GenericXmlWriter : IMetaTypeVisitor
+    {
+        private readonly Action<InstanceDescriptor> exportChild;
+        private readonly XmlWriter xml;
+        private readonly Stack<int> startOffsetStack;
+        private InstanceFile instanceFile;
+        private BinaryReader reader;
+
+        public static void Write(XmlWriter xml, Action<InstanceDescriptor> exportChild, InstanceDescriptor descriptor)
+        {
+            var writer = new GenericXmlWriter(xml, exportChild);
+            writer.WriteInstance(descriptor);
+        }
+
+        private GenericXmlWriter(XmlWriter xml, Action<InstanceDescriptor> exportChild)
+        {
+            this.exportChild = exportChild;
+            this.xml = xml;
+            this.startOffsetStack = new Stack<int>();
+        }
+
+        private void WriteInstance(InstanceDescriptor descriptor)
+        {
+            using (reader = descriptor.OpenRead())
+            {
+                BeginXmlInstance(descriptor);
+                descriptor.Template.Type.Accept(this);
+                EndXmlInstance();
+            }
+        }
+
+        private void BeginXmlInstance(InstanceDescriptor descriptor)
+        {
+            instanceFile = descriptor.File;
+            startOffsetStack.Push(reader.Position - 8);
+
+            string tag = descriptor.Template.Tag.ToString();
+
+            xml.WriteStartElement(tag);
+            xml.WriteAttributeString("id", XmlConvert.ToString(descriptor.Index));
+        }
+
+        private void EndXmlInstance()
+        {
+            startOffsetStack.Pop();
+            xml.WriteEndElement();
+        }
+
+        #region IMetaTypeVisitor Members
+
+        void IMetaTypeVisitor.VisitEnum(MetaEnum type)
+        {
+            type.BinaryToXml(reader, xml);
+        }
+
+        void IMetaTypeVisitor.VisitByte(MetaByte type)
+        {
+            xml.WriteValue(reader.ReadByte());
+        }
+
+        void IMetaTypeVisitor.VisitInt16(MetaInt16 type)
+        {
+            xml.WriteValue(reader.ReadInt16());
+        }
+
+        void IMetaTypeVisitor.VisitUInt16(MetaUInt16 type)
+        {
+            xml.WriteValue(reader.ReadUInt16());
+        }
+
+        void IMetaTypeVisitor.VisitInt32(MetaInt32 type)
+        {
+            xml.WriteValue(reader.ReadInt32());
+        }
+
+        void IMetaTypeVisitor.VisitUInt32(MetaUInt32 type)
+        {
+            xml.WriteValue(reader.ReadUInt32());
+        }
+
+        void IMetaTypeVisitor.VisitInt64(MetaInt64 type)
+        {
+            xml.WriteValue(reader.ReadInt64());
+        }
+
+        void IMetaTypeVisitor.VisitUInt64(MetaUInt64 type)
+        {
+            xml.WriteValue(XmlConvert.ToString(reader.ReadUInt64()));
+        }
+
+        void IMetaTypeVisitor.VisitFloat(MetaFloat type)
+        {
+            xml.WriteValue(reader.ReadSingle());
+        }
+
+        void IMetaTypeVisitor.VisitVector2(MetaVector2 type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(2));
+        }
+
+        void IMetaTypeVisitor.VisitVector3(MetaVector3 type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(3));
+        }
+
+        void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(12));
+        }
+
+        void IMetaTypeVisitor.VisitPlane(MetaPlane type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(4));
+        }
+
+        void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(4));
+        }
+
+        void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type)
+        {
+            WriteFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type)
+        {
+            WriteFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitColor(MetaColor type)
+        {
+            Color color = reader.ReadColor();
+
+            if (color.A == 255)
+                xml.WriteValue(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", color.R, color.G, color.B));
+            else
+                xml.WriteValue(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}", color.R, color.G, color.B, color.A));
+        }
+
+        void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type)
+        {
+            xml.WriteValue(reader.ReadInt32());
+        }
+
+        void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type)
+        {
+            xml.WriteValue(reader.ReadInt32());
+        }
+
+        void IMetaTypeVisitor.VisitPointer(MetaPointer type)
+        {
+            int id = reader.ReadInt32();
+
+            if (id == 0)
+            {
+                xml.WriteString(string.Empty);
+                return;
+            }
+
+            exportChild(instanceFile.GetDescriptor(id));
+        }
+
+        void IMetaTypeVisitor.VisitString(MetaString type)
+        {
+            xml.WriteValue(reader.ReadString(type.Count));
+        }
+
+        void IMetaTypeVisitor.VisitPadding(MetaPadding type)
+        {
+            reader.Skip(type.Count);
+        }
+
+        void IMetaTypeVisitor.VisitStruct(MetaStruct type)
+        {
+            WriteFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitArray(MetaArray type)
+        {
+            WriteArray(type.ElementType, type.Count);
+        }
+
+        void IMetaTypeVisitor.VisitVarArray(MetaVarArray type)
+        {
+            int count;
+
+            if (type.CountField.Type == MetaType.Int16)
+                count = reader.ReadInt16();
+            else
+                count = reader.ReadInt32();
+
+            WriteArray(type.ElementType, count);
+        }
+
+        #endregion
+
+        private void WriteFields(IEnumerable<Field> fields)
+        {
+            foreach (var field in fields)
+            {
+                if (field.Type is MetaPadding)
+                {
+                    field.Type.Accept(this);
+                    continue;
+                }
+
+                string name = field.Name;
+
+                if (string.IsNullOrEmpty(name))
+                {
+                    int fieldOffset = reader.Position - startOffsetStack.Peek();
+                    name = string.Format(CultureInfo.InvariantCulture, "Offset_{0:X4}", fieldOffset);
+                }
+
+                xml.WriteStartElement(name);
+                field.Type.Accept(this);
+                xml.WriteEndElement();
+            }
+        }
+
+        private void WriteArray(MetaType elementType, int count)
+        {
+            bool simpleArray = elementType.IsBlittable;
+            simpleArray = false;
+
+            for (int i = 0; i < count; i++)
+            {
+                startOffsetStack.Push(reader.Position);
+
+                if (!simpleArray)
+                    xml.WriteStartElement(elementType.Name);
+
+                elementType.Accept(this);
+
+                if (!simpleArray)
+                    xml.WriteEndElement();
+                else if (i != count - 1)
+                    xml.WriteWhitespace(" \n");
+
+                startOffsetStack.Pop();
+            }
+        }
+    }
+}
Index: /OniSplit/Xml/ObjcXmlExporter.cs
===================================================================
--- /OniSplit/Xml/ObjcXmlExporter.cs	(revision 1114)
+++ /OniSplit/Xml/ObjcXmlExporter.cs	(revision 1114)
@@ -0,0 +1,456 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    internal class ObjcXmlExporter : RawXmlExporter
+    {
+        private readonly Dictionary<ObjectMetadata.TypeTag, Action> typeWriters = new Dictionary<ObjectMetadata.TypeTag, Action>();
+        private int objectEndPosition;
+
+        private ObjcXmlExporter(BinaryReader reader, XmlWriter xml)
+            : base(reader, xml)
+        {
+            InitTypeWriters(typeWriters);
+        }
+
+        public static void Export(BinaryReader reader, XmlWriter xml)
+        {
+            var exporter = new ObjcXmlExporter(reader, xml);
+            exporter.Export();
+        }
+
+        private void Export()
+        {
+            int size = Reader.ReadInt32();
+            int version = Reader.ReadInt32();
+
+            Xml.WriteStartElement("Objects");
+
+            while (true)
+            {
+                int objectSize = Reader.ReadInt32();
+
+                if (objectSize == 0)
+                    break;
+
+                int readerPosition = Reader.Position;
+                objectEndPosition = readerPosition + objectSize;
+
+                BeginStruct(Reader.Position);
+
+                var objectType = (ObjectMetadata.TypeTag)Reader.ReadInt32();
+                int objectId = Reader.ReadInt32();
+
+                Xml.WriteStartElement(objectType.ToString());
+                Xml.WriteAttributeString("Id", XmlConvert.ToString(objectId));
+
+                Xml.WriteStartElement("Header");
+                ObjectMetadata.Header.Accept(this);
+                Xml.WriteEndElement();
+
+                Xml.WriteStartElement("OSD");
+                typeWriters[objectType]();
+                Xml.WriteEndElement();
+
+                Xml.WriteEndElement();
+
+                Reader.Position = objectEndPosition;
+            }
+
+            Xml.WriteEndElement();
+        }
+
+        private void WriteCharacter()
+        {
+            ObjectMetadata.Character.Accept(this);
+        }
+
+        private void WriteCombatProfile()
+        {
+            ObjectMetadata.CombatProfile.Accept(this);
+        }
+
+        private void WriteConsole()
+        {
+            ObjectMetadata.Console.Accept(this);
+            WriteEventList();
+        }
+
+        private void WriteDoor()
+        {
+            ObjectMetadata.Door.Accept(this);
+            WriteEventList();
+        }
+
+        private void WriteFlag()
+        {
+            ObjectMetadata.Flag.Accept(this);
+        }
+
+        private void WriteFurniture()
+        {
+            ObjectMetadata.Furniture.Accept(this);
+        }
+
+        private void WriteMeleeProfile()
+        {
+            ObjectMetadata.MeleeProfile.Accept(this);
+
+            int attackCount = Reader.ReadInt32();
+            int evadeCount = Reader.ReadInt32();
+            int maneuverCount = Reader.ReadInt32();
+            int moveCount = Reader.ReadInt32();
+
+            int moveTablePosition = Reader.Position + (attackCount + evadeCount + maneuverCount) * 88;
+
+            Xml.WriteStartElement("Attacks");
+            for (int i = 0; i < attackCount; i++)
+                WriteMeleeTechnique(moveTablePosition);
+            Xml.WriteEndElement();
+
+            Xml.WriteStartElement("Evades");
+            for (int i = 0; i < evadeCount; i++)
+                WriteMeleeTechnique(moveTablePosition);
+            Xml.WriteEndElement();
+
+            Xml.WriteStartElement("Maneuvers");
+            for (int i = 0; i < maneuverCount; i++)
+                WriteMeleeTechnique(moveTablePosition);
+            Xml.WriteEndElement();
+        }
+
+        private void WriteMeleeTechnique(int moveTablePosition)
+        {
+            Xml.WriteStartElement("Technique");
+
+            ObjectMetadata.MeleeTechnique.Accept(this);
+
+            int moveCount = Reader.ReadInt32();
+            int moveStart = Reader.ReadInt32();
+
+            int oldPosition = Reader.Position;
+            Reader.Position = moveTablePosition + moveStart * 16;
+
+            Xml.WriteStartElement("Moves");
+
+            for (int j = 0; j < moveCount; j++)
+                WriteMeleeMove();
+
+            Xml.WriteEndElement();
+            Xml.WriteEndElement();
+
+            Reader.Position = oldPosition;
+        }
+
+        private void WriteMeleeMove()
+        {
+            int categoryType = Reader.ReadInt32();
+            float[] moveParams = Reader.ReadSingleArray(3);
+
+            var category = (ObjectMetadata.MeleeMoveCategory)(categoryType >> 24);
+            Xml.WriteStartElement(category.ToString());
+
+            switch (category)
+            {
+                default:
+                case ObjectMetadata.MeleeMoveCategory.Attack:
+                    Xml.WriteAttributeString("Type", ((ObjectMetadata.MeleeMoveAttackType)(categoryType & 0xffffff)).ToString());
+                    break;
+
+                case ObjectMetadata.MeleeMoveCategory.Evade:
+                    Xml.WriteAttributeString("Type", ((ObjectMetadata.MeleeMoveEvadeType)(categoryType & 0xffffff)).ToString());
+                    break;
+
+                case ObjectMetadata.MeleeMoveCategory.Throw:
+                    Xml.WriteAttributeString("Type", ((ObjectMetadata.MeleeMoveThrowType)(categoryType & 0xffffff)).ToString());
+                    break;
+
+                case ObjectMetadata.MeleeMoveCategory.Maneuver:
+                    ObjectMetadata.MeleeMoveTypeInfo typeInfo = ObjectMetadata.MeleeMoveManeuverTypeInfo[categoryType & 0xffffff];
+                    Xml.WriteAttributeString("Type", typeInfo.Type.ToString());
+
+                    for (int k = 0; k < typeInfo.ParamNames.Length; k++)
+                        Xml.WriteAttributeString(typeInfo.ParamNames[k], XmlConvert.ToString(moveParams[k]));
+
+                    break;
+
+                case ObjectMetadata.MeleeMoveCategory.Position:
+                    ObjectMetadata.MeleeMovePositionType moveType = (ObjectMetadata.MeleeMovePositionType)(categoryType & 0xffffff);
+                    Xml.WriteAttributeString("Type", moveType.ToString());
+
+                    if ((ObjectMetadata.MeleeMovePositionType.RunForward <= moveType && moveType <= ObjectMetadata.MeleeMovePositionType.RunBack)
+                        || ObjectMetadata.MeleeMovePositionType.CloseForward <= moveType)
+                    {
+                        Xml.WriteAttributeString("MinRunInDist", XmlConvert.ToString(moveParams[0]));
+                        Xml.WriteAttributeString("MaxRunInDist", XmlConvert.ToString(moveParams[1]));
+                        Xml.WriteAttributeString("ToleranceRange", XmlConvert.ToString(moveParams[2]));
+                    }
+
+                    break;
+            }
+
+            Xml.WriteEndElement();
+        }
+
+        private void WriteNeutralBehavior()
+        {
+            ObjectMetadata.NeutralBehavior.Accept(this);
+            int lineCount = Reader.ReadInt16();
+            ObjectMetadata.NeutralBehaviorParams.Accept(this);
+
+            Xml.WriteStartElement("DialogLines");
+            MetaType.Array(lineCount, ObjectMetadata.NeutralBehaviorDialogLine).Accept(this);
+            Xml.WriteEndElement();
+        }
+
+        private void WriteParticle()
+        {
+            ObjectMetadata.Particle.Accept(this);
+        }
+
+        private void WritePatrolPath()
+        {
+            ObjectMetadata.PatrolPath.Accept(this);
+            int length = Reader.ReadInt32();
+            ObjectMetadata.PatrolPathInfo.Accept(this);
+
+            int startPosition = Reader.Position;
+            int loopStartPoint = -1;
+
+            var pointType = (ObjectMetadata.PatrolPathPointType)Reader.ReadInt32();
+
+            for (int i = 0; i < length; i++)
+            {
+                if (pointType == ObjectMetadata.PatrolPathPointType.Loop)
+                    loopStartPoint = 0;
+                else if (pointType == ObjectMetadata.PatrolPathPointType.LoopFrom)
+                    loopStartPoint = Reader.ReadInt32();
+                else
+                    Reader.Position += ObjectMetadata.GetPatrolPathPointSize(pointType);
+
+                pointType = (ObjectMetadata.PatrolPathPointType)Reader.ReadInt32();
+            }
+
+            Reader.Position = startPosition;
+
+            Xml.WriteStartElement("Points");
+
+            for (int i = 0; i < length; i++)
+            {
+                if (loopStartPoint == i)
+                    Xml.WriteStartElement("Loop");
+
+                WritePatrolPathPoint();
+            }
+
+            if (loopStartPoint != -1)
+                Xml.WriteEndElement();
+
+            Xml.WriteEndElement();
+        }
+
+        private void WritePatrolPathPoint()
+        {
+            var pointType = (ObjectMetadata.PatrolPathPointType)Reader.ReadInt32();
+
+            switch (pointType)
+            {
+                case ObjectMetadata.PatrolPathPointType.Loop:
+                    return;
+
+                case ObjectMetadata.PatrolPathPointType.LoopFrom:
+                    Reader.Skip(4);
+                    return;
+            }
+
+            Xml.WriteStartElement(pointType.ToString());
+
+            switch (pointType)
+            {
+                case ObjectMetadata.PatrolPathPointType.Stop:
+                case ObjectMetadata.PatrolPathPointType.StopLooking:
+                case ObjectMetadata.PatrolPathPointType.StopScanning:
+                case ObjectMetadata.PatrolPathPointType.FreeFacing:
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.IgnorePlayer:
+                    Xml.WriteAttributeString("Value", Reader.ReadBoolean() ? "Yes" : "No");
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveToFlag:
+                case ObjectMetadata.PatrolPathPointType.LookAtFlag:
+                case ObjectMetadata.PatrolPathPointType.MoveAndFaceFlag:
+                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.ForkScript:
+                case ObjectMetadata.PatrolPathPointType.CallScript:
+                    Xml.WriteAttributeString("ScriptId", XmlConvert.ToString(Reader.ReadInt16()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.Pause:
+                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt32()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MovementMode:
+                    Xml.WriteAttributeString("Mode", ((ObjectMetadata.PatrolPathMovementMode)Reader.ReadInt32()).ToString());
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.LockFacing:
+                    Xml.WriteAttributeString("Facing", ((ObjectMetadata.PatrolPathFacing)Reader.ReadInt32()).ToString());
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveThroughFlag:
+                case ObjectMetadata.PatrolPathPointType.MoveNearFlag:
+                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
+                    Xml.WriteAttributeString("Distance", XmlConvert.ToString(Reader.ReadSingle()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.GlanceAtFlagFor:
+                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
+                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt32()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.Scan:
+                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt16()));
+                    Xml.WriteAttributeString("Rotation", XmlConvert.ToString(Reader.ReadSingle()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveToFlagLookAndWait:
+                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt16()));
+                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
+                    Xml.WriteAttributeString("Rotation", XmlConvert.ToString(Reader.ReadSingle()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.FaceToFlagAndFire:
+                    Xml.WriteAttributeString("FlagId", XmlConvert.ToString(Reader.ReadInt16()));
+                    Xml.WriteAttributeString("Frames", XmlConvert.ToString(Reader.ReadInt16()));
+                    Xml.WriteAttributeString("Spread", XmlConvert.ToString(Reader.ReadSingle()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.LookAtPoint:
+                case ObjectMetadata.PatrolPathPointType.MoveToPoint:
+                    Xml.WriteAttributeString("X", XmlConvert.ToString(Reader.ReadSingle()));
+                    Xml.WriteAttributeString("Y", XmlConvert.ToString(Reader.ReadSingle()));
+                    Xml.WriteAttributeString("Z", XmlConvert.ToString(Reader.ReadSingle()));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveThroughPoint:
+                    Xml.WriteAttributeString("X", XmlConvert.ToString(Reader.ReadSingle()));
+                    Xml.WriteAttributeString("Y", XmlConvert.ToString(Reader.ReadSingle()));
+                    Xml.WriteAttributeString("Z", XmlConvert.ToString(Reader.ReadSingle()));
+                    Xml.WriteAttributeString("Distance", XmlConvert.ToString(Reader.ReadSingle()));
+                    break;
+
+                default:
+                    throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Unsupported path point type {0}", pointType));
+            }
+
+            Xml.WriteEndElement();
+        }
+
+        private void WritePowerUp()
+        {
+            ObjectMetadata.PowerUp.Accept(this);
+        }
+
+        private void WriteSound()
+        {
+            ObjectMetadata.Sound.Accept(this);
+
+            var volumeType = (ObjectMetadata.SoundVolumeType)Reader.ReadInt32();
+
+            switch (volumeType)
+            {
+                case ObjectMetadata.SoundVolumeType.Box:
+                    Xml.WriteStartElement("Box");
+                    MetaType.BoundingBox.Accept(this);
+                    Xml.WriteEndElement();
+                    break;
+
+                case ObjectMetadata.SoundVolumeType.Sphere:
+                    Xml.WriteStartElement("Sphere");
+                    ObjectMetadata.SoundSphere.Accept(this);
+                    Xml.WriteEndElement();
+                    break;
+            }
+
+            ObjectMetadata.SoundParams.Accept(this);
+        }
+
+        private void WriteTriggerVolume()
+        {
+            ObjectMetadata.TriggerVolume.Accept(this);
+        }
+
+        private void WriteTrigger()
+        {
+            ObjectMetadata.Trigger.Accept(this);
+            WriteEventList();
+        }
+
+        private void WriteTurret()
+        {
+            ObjectMetadata.Turret.Accept(this);
+        }
+
+        private void WriteWeapon()
+        {
+            ObjectMetadata.Weapon.Accept(this);
+        }
+
+        private void WriteEventList()
+        {
+            Xml.WriteStartElement("Events");
+
+            int count = Reader.ReadInt16();
+
+            for (int i = 0; i < count; i++)
+            {
+                var eventType = (ObjectMetadata.EventType)Reader.ReadInt16();
+
+                Xml.WriteStartElement(eventType.ToString());
+
+                switch (eventType)
+                {
+                    case ObjectMetadata.EventType.None:
+                        break;
+                    case ObjectMetadata.EventType.Script:
+                        Xml.WriteAttributeString("Function", Reader.ReadString(32));
+                        break;
+                    default:
+                        Xml.WriteAttributeString("TargetId", XmlConvert.ToString(Reader.ReadInt16()));
+                        break;
+                }
+
+                Xml.WriteEndElement();
+            }
+
+            Xml.WriteEndElement();
+        }
+
+        private void InitTypeWriters(Dictionary<ObjectMetadata.TypeTag, Action> typeWriters)
+        {
+            typeWriters.Add(ObjectMetadata.TypeTag.CHAR, WriteCharacter);
+            typeWriters.Add(ObjectMetadata.TypeTag.CMBT, WriteCombatProfile);
+            typeWriters.Add(ObjectMetadata.TypeTag.CONS, WriteConsole);
+            typeWriters.Add(ObjectMetadata.TypeTag.DOOR, WriteDoor);
+            typeWriters.Add(ObjectMetadata.TypeTag.FLAG, WriteFlag);
+            typeWriters.Add(ObjectMetadata.TypeTag.FURN, WriteFurniture);
+            typeWriters.Add(ObjectMetadata.TypeTag.MELE, WriteMeleeProfile);
+            typeWriters.Add(ObjectMetadata.TypeTag.NEUT, WriteNeutralBehavior);
+            typeWriters.Add(ObjectMetadata.TypeTag.PART, WriteParticle);
+            typeWriters.Add(ObjectMetadata.TypeTag.PATR, WritePatrolPath);
+            typeWriters.Add(ObjectMetadata.TypeTag.PWRU, WritePowerUp);
+            typeWriters.Add(ObjectMetadata.TypeTag.SNDG, WriteSound);
+            typeWriters.Add(ObjectMetadata.TypeTag.TRGV, WriteTriggerVolume);
+            typeWriters.Add(ObjectMetadata.TypeTag.TRIG, WriteTrigger);
+            typeWriters.Add(ObjectMetadata.TypeTag.TURR, WriteTurret);
+            typeWriters.Add(ObjectMetadata.TypeTag.WEAP, WriteWeapon);
+        }
+    }
+}
Index: /OniSplit/Xml/ObjcXmlImporter.cs
===================================================================
--- /OniSplit/Xml/ObjcXmlImporter.cs	(revision 1114)
+++ /OniSplit/Xml/ObjcXmlImporter.cs	(revision 1114)
@@ -0,0 +1,573 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    internal class ObjcXmlImporter : RawXmlImporter
+    {
+        private readonly Dictionary<ObjectMetadata.TypeTag, Action> typeReaders = new Dictionary<ObjectMetadata.TypeTag, Action>();
+        private int nextId;
+
+        private ObjcXmlImporter(XmlReader xml, BinaryWriter writer)
+            : base(xml, writer)
+        {
+            InitTypeReaders(typeReaders);
+        }
+
+        public static void Import(XmlReader xml, BinaryWriter writer)
+        {
+            var importer = new ObjcXmlImporter(xml, writer);
+            importer.Import();
+        }
+
+        private void Import()
+        {
+            Writer.Write(39);
+
+            nextId = 1;
+
+            while (Xml.IsStartElement())
+            {
+                int objectStartPosition = Writer.Position;
+                Writer.Write(0);
+
+                BeginStruct(objectStartPosition);
+
+                ReadObject();
+
+                Writer.Position = Utils.Align4(Writer.Position);
+
+                int objectSize = Writer.Position - objectStartPosition - 4;
+                Writer.WriteAt(objectStartPosition, objectSize);
+            }
+
+            Writer.Write(0);
+        }
+
+        private ObjectMetadata.TypeTag ReadObject()
+        {
+            var id = Xml.GetAttribute("Id");
+            int objectId = string.IsNullOrEmpty(id) ? nextId++ : XmlConvert.ToInt32(id);
+            var objectTag = Xml.GetAttribute("Type");
+
+            if (objectTag == null)
+                objectTag = Xml.LocalName;
+
+            var objectType = MetaEnum.Parse<ObjectMetadata.TypeTag>(objectTag);
+
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            Writer.Write((int)objectType);
+            Writer.Write(objectId);
+            ObjectMetadata.Header.Accept(this);
+            typeReaders[objectType]();
+
+            Xml.ReadEndElement();
+
+            return objectType;
+        }
+
+        private void ReadCharacter()
+        {
+            ObjectMetadata.Character.Accept(this);
+        }
+
+        private void ReadCombatProfile()
+        {
+            ObjectMetadata.CombatProfile.Accept(this);
+        }
+
+        private void ReadConsole()
+        {
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            ReadStruct(ObjectMetadata.Console);
+            ReadEventList();
+
+            Xml.ReadEndElement();
+        }
+
+        private void ReadDoor()
+        {
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            ReadStruct(ObjectMetadata.Door);
+            ReadEventList();
+
+            Xml.ReadEndElement();
+        }
+
+        private void ReadFlag()
+        {
+            ObjectMetadata.Flag.Accept(this);
+        }
+
+        private void ReadFurniture()
+        {
+            ObjectMetadata.Furniture.Accept(this);
+        }
+
+        private struct MeleeMove
+        {
+            public int Type;
+            public float[] Params;
+        }
+
+        private void ReadMeleeProfile()
+        {
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            ReadStruct(ObjectMetadata.MeleeProfile);
+
+            int countFieldsPosition = Writer.Position;
+            Writer.Write(0);
+            Writer.Write(0);
+            Writer.Write(0);
+            Writer.Write(0);
+
+            int attackCount = 0;
+            int evadeCount = 0;
+            int maneuverCount = 0;
+            var moves = new List<MeleeMove>();
+
+            attackCount = ReadMeleeTechniques("Attacks", moves);
+            evadeCount = ReadMeleeTechniques("Evades", moves);
+            maneuverCount = ReadMeleeTechniques("Maneuvers", moves);
+
+            foreach (var move in moves)
+            {
+                Writer.Write(move.Type);
+                Writer.Write(move.Params);
+            }
+
+            int oldPosition = Writer.Position;
+            Writer.Position = countFieldsPosition;
+            Writer.Write(attackCount);
+            Writer.Write(evadeCount);
+            Writer.Write(maneuverCount);
+            Writer.Write(moves.Count);
+            Writer.Position = oldPosition;
+
+            Xml.ReadEndElement();
+        }
+
+        private int ReadMeleeTechniques(string xmlName, List<MeleeMove> moves)
+        {
+            if (Xml.IsStartElement(xmlName) && Xml.IsEmptyElement)
+            {
+                Xml.Skip();
+                return 0;
+            }
+
+            Xml.ReadStartElement(xmlName);
+            Xml.MoveToContent();
+
+            int techniqueCount = 0;
+
+            for (; Xml.IsStartElement("Technique"); techniqueCount++)
+            {
+                Xml.ReadStartElement();
+                Xml.MoveToContent();
+
+                ReadStruct(ObjectMetadata.MeleeTechnique);
+                int moveCountPosition = Writer.Position;
+                Writer.Write(0);
+                Writer.Write(moves.Count);
+
+                int moveCount = 0;
+
+                if (Xml.IsStartElement("Moves") && Xml.IsEmptyElement)
+                {
+                    Xml.Skip();
+                }
+                else
+                {
+                    Xml.ReadStartElement("Moves");
+                    Xml.MoveToContent();
+
+                    for (; Xml.IsStartElement(); moveCount++)
+                    {
+                        ReadMeleeMove(moves);
+                    }
+
+                    Xml.ReadEndElement();
+                }
+
+                Xml.ReadEndElement();
+
+                Writer.WriteAt(moveCountPosition, moveCount);
+            }
+
+            Xml.ReadEndElement();
+
+            return techniqueCount;
+        }
+
+        private void ReadMeleeMove(List<MeleeMove> moves)
+        {
+            var category = (ObjectMetadata.MeleeMoveCategory)Enum.Parse(typeof(ObjectMetadata.MeleeMoveCategory), Xml.LocalName);
+
+            var typeText = Xml.GetAttribute("Type");
+            int type;
+            var moveParams = new float[3];
+
+            switch (category)
+            {
+                default:
+                case ObjectMetadata.MeleeMoveCategory.Attack:
+                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveAttackType>(typeText));
+                    break;
+                case ObjectMetadata.MeleeMoveCategory.Evade:
+                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveEvadeType>(typeText));
+                    break;
+                case ObjectMetadata.MeleeMoveCategory.Throw:
+                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveThrowType>(typeText));
+                    break;
+
+                case ObjectMetadata.MeleeMoveCategory.Position:
+                    ObjectMetadata.MeleeMovePositionType positionType = MetaEnum.Parse<ObjectMetadata.MeleeMovePositionType>(typeText);
+
+                    if ((ObjectMetadata.MeleeMovePositionType.RunForward <= positionType
+                        && positionType <= ObjectMetadata.MeleeMovePositionType.RunBack)
+                            || ObjectMetadata.MeleeMovePositionType.CloseForward <= positionType)
+                    {
+                        moveParams[0] = XmlConvert.ToSingle(Xml.GetAttribute("MinRunInDist"));
+                        moveParams[1] = XmlConvert.ToSingle(Xml.GetAttribute("MaxRunInDist"));
+                        moveParams[2] = XmlConvert.ToSingle(Xml.GetAttribute("ToleranceRange"));
+                    }
+
+                    type = Convert.ToInt32(positionType);
+                    break;
+
+                case ObjectMetadata.MeleeMoveCategory.Maneuver:
+                    type = Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.MeleeMoveManeuverType>(typeText));
+                    ObjectMetadata.MeleeMoveTypeInfo typeInfo = ObjectMetadata.MeleeMoveManeuverTypeInfo[type];
+
+                    for (int k = 0; k < typeInfo.ParamNames.Length; k++)
+                        moveParams[k] = XmlConvert.ToSingle(Xml.GetAttribute(typeInfo.ParamNames[k]));
+
+                    break;
+            }
+
+            moves.Add(new MeleeMove()
+            {
+                Type = (((int)category) << 24) | (type & 0xffffff),
+                Params = moveParams
+            });
+
+            Xml.Skip();
+        }
+
+        private void ReadNeutralBehavior()
+        {
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            ReadStruct(ObjectMetadata.NeutralBehavior);
+            int countFieldPosition = Writer.Position;
+            Writer.WriteUInt16(0);
+            ReadStruct(ObjectMetadata.NeutralBehaviorParams);
+
+            Xml.ReadStartElement("DialogLines");
+            short count = 0;
+
+            for (; Xml.IsStartElement("DialogLine"); count++)
+                ObjectMetadata.NeutralBehaviorDialogLine.Accept(this);
+
+            Xml.ReadEndElement();
+            Xml.ReadEndElement();
+
+            Writer.WriteAt(countFieldPosition, count);
+        }
+
+        private void ReadParticle()
+        {
+            ObjectMetadata.Particle.Accept(this);
+        }
+
+        private void ReadPatrolPath()
+        {
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            ReadStruct(ObjectMetadata.PatrolPath);
+            int lengthFieldPosition = Writer.Position;
+            Writer.Write(0);
+            ReadStruct(ObjectMetadata.PatrolPathInfo);
+
+            int count = 0;
+            bool isEmpty = Xml.IsEmptyElement;
+
+            Xml.ReadStartElement("Points");
+
+            if (!isEmpty)
+            {
+                int loopStart = -1;
+
+                for (; Xml.IsStartElement(); count++)
+                {
+                    if (ReadPatrolPathPoint())
+                        loopStart = count;
+                }
+
+                if (loopStart != -1)
+                {
+                    if (loopStart == 0)
+                    {
+                        Writer.Write((int)ObjectMetadata.PatrolPathPointType.Loop);
+                    }
+                    else
+                    {
+                        Writer.Write((int)ObjectMetadata.PatrolPathPointType.LoopFrom);
+                        Writer.Write(loopStart);
+                    }
+
+                    if (Xml.NodeType == XmlNodeType.EndElement && Xml.LocalName == "Loop")
+                        Xml.ReadEndElement();
+                }
+
+                Xml.ReadEndElement();
+            }
+
+            Xml.ReadEndElement();
+
+            Writer.WriteAt(lengthFieldPosition, count);
+        }
+
+        private bool ReadPatrolPathPoint()
+        {
+            var pointType = MetaEnum.Parse<ObjectMetadata.PatrolPathPointType>(Xml.LocalName);
+
+            switch (pointType)
+            {
+                case ObjectMetadata.PatrolPathPointType.Loop:
+                    if (Xml.IsEmptyElement)
+                    {
+                        Xml.Skip();
+                    }
+                    else
+                    {
+                        Xml.ReadStartElement();
+                        Xml.MoveToContent();
+                    }
+                    return true;
+            }
+
+            Writer.Write((int)pointType);
+
+            switch (pointType)
+            {
+                case ObjectMetadata.PatrolPathPointType.Stop:
+                case ObjectMetadata.PatrolPathPointType.StopLooking:
+                case ObjectMetadata.PatrolPathPointType.StopScanning:
+                case ObjectMetadata.PatrolPathPointType.FreeFacing:
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.IgnorePlayer:
+                    Writer.WriteByte(Xml.GetAttribute("Value") == "Yes" ? 1 : 0);
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.ForkScript:
+                case ObjectMetadata.PatrolPathPointType.CallScript:
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("ScriptId")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveToFlag:
+                case ObjectMetadata.PatrolPathPointType.LookAtFlag:
+                case ObjectMetadata.PatrolPathPointType.MoveAndFaceFlag:
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MovementMode:
+                    Writer.Write(Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.PatrolPathMovementMode>(Xml.GetAttribute("Mode"))));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.LockFacing:
+                    Writer.Write(Convert.ToInt32(MetaEnum.Parse<ObjectMetadata.PatrolPathFacing>(Xml.GetAttribute("Facing"))));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.Pause:
+                    Writer.Write(XmlConvert.ToInt32(Xml.GetAttribute("Frames")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.GlanceAtFlagFor:
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
+                    Writer.Write(XmlConvert.ToInt32(Xml.GetAttribute("Frames")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.Scan:
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("Frames")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Rotation")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveThroughFlag:
+                case ObjectMetadata.PatrolPathPointType.MoveNearFlag:
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Distance")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveToFlagLookAndWait:
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("Frames")));
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Rotation")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.FaceToFlagAndFire:
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("FlagId")));
+                    Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("Frames")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Spread")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.LookAtPoint:
+                case ObjectMetadata.PatrolPathPointType.MoveToPoint:
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("X")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Y")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Z")));
+                    break;
+
+                case ObjectMetadata.PatrolPathPointType.MoveThroughPoint:
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("X")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Y")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Z")));
+                    Writer.Write(XmlConvert.ToSingle(Xml.GetAttribute("Distance")));
+                    break;
+
+                default:
+                    throw new NotSupportedException(string.Format("Unsupported path point type {0}", pointType));
+            }
+
+            Xml.Skip();
+
+            return false;
+        }
+
+        private void ReadPowerUp()
+        {
+            ObjectMetadata.PowerUp.Accept(this);
+        }
+
+        private void ReadSound()
+        {
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            ReadStruct(ObjectMetadata.Sound);
+
+            var volumeType = MetaEnum.Parse<ObjectMetadata.SoundVolumeType>(Xml.LocalName);
+
+            Writer.Write((int)volumeType);
+
+            switch (volumeType)
+            {
+                case ObjectMetadata.SoundVolumeType.Box:
+                    MetaType.BoundingBox.Accept(this);
+                    break;
+
+                case ObjectMetadata.SoundVolumeType.Sphere:
+                    ObjectMetadata.SoundSphere.Accept(this);
+                    break;
+            }
+
+            ReadStruct(ObjectMetadata.SoundParams);
+
+            if (volumeType == ObjectMetadata.SoundVolumeType.Sphere)
+                Writer.Skip(16);
+
+            Xml.ReadEndElement();
+        }
+
+        private void ReadTriggerVolume()
+        {
+            ObjectMetadata.TriggerVolume.Accept(this);
+        }
+
+        private void ReadTrigger()
+        {
+            Xml.ReadStartElement();
+            Xml.MoveToContent();
+
+            ReadStruct(ObjectMetadata.Trigger);
+            ReadEventList();
+
+            Xml.ReadEndElement();
+        }
+
+        private void ReadTurret()
+        {
+            ObjectMetadata.Turret.Accept(this);
+        }
+
+        private void ReadWeapon()
+        {
+            ObjectMetadata.Weapon.Accept(this);
+        }
+
+        private void ReadEventList()
+        {
+            int countFieldPosition = Writer.Position;
+            Writer.WriteUInt16(0);
+
+            if (Xml.IsStartElement("Events") && Xml.IsEmptyElement)
+            {
+                Xml.ReadStartElement();
+                return;
+            }
+
+            Xml.ReadStartElement("Events");
+            Xml.MoveToContent();
+
+            short eventCount = 0;
+
+            while (Xml.IsStartElement())
+            {
+                var eventType = MetaEnum.Parse<ObjectMetadata.EventType>(Xml.Name);
+
+                Writer.Write((short)eventType);
+
+                switch (eventType)
+                {
+                    case ObjectMetadata.EventType.None:
+                        break;
+                    case ObjectMetadata.EventType.Script:
+                        Writer.Write(Xml.GetAttribute("Function"), 32);
+                        break;
+                    default:
+                        Writer.Write(XmlConvert.ToInt16(Xml.GetAttribute("TargetId")));
+                        break;
+                }
+
+                eventCount++;
+                Xml.Skip();
+            }
+
+            Writer.WriteAt(countFieldPosition, eventCount);
+            Xml.ReadEndElement();
+        }
+
+        private void InitTypeReaders(Dictionary<ObjectMetadata.TypeTag, Action> typeReaders)
+        {
+            typeReaders.Add(ObjectMetadata.TypeTag.CHAR, ReadCharacter);
+            typeReaders.Add(ObjectMetadata.TypeTag.CMBT, ReadCombatProfile);
+            typeReaders.Add(ObjectMetadata.TypeTag.CONS, ReadConsole);
+            typeReaders.Add(ObjectMetadata.TypeTag.DOOR, ReadDoor);
+            typeReaders.Add(ObjectMetadata.TypeTag.FLAG, ReadFlag);
+            typeReaders.Add(ObjectMetadata.TypeTag.FURN, ReadFurniture);
+            typeReaders.Add(ObjectMetadata.TypeTag.MELE, ReadMeleeProfile);
+            typeReaders.Add(ObjectMetadata.TypeTag.NEUT, ReadNeutralBehavior);
+            typeReaders.Add(ObjectMetadata.TypeTag.PART, ReadParticle);
+            typeReaders.Add(ObjectMetadata.TypeTag.PATR, ReadPatrolPath);
+            typeReaders.Add(ObjectMetadata.TypeTag.PWRU, ReadPowerUp);
+            typeReaders.Add(ObjectMetadata.TypeTag.SNDG, ReadSound);
+            typeReaders.Add(ObjectMetadata.TypeTag.TRGV, ReadTriggerVolume);
+            typeReaders.Add(ObjectMetadata.TypeTag.TRIG, ReadTrigger);
+            typeReaders.Add(ObjectMetadata.TypeTag.TURR, ReadTurret);
+            typeReaders.Add(ObjectMetadata.TypeTag.WEAP, ReadWeapon);
+        }
+    }
+}
Index: /OniSplit/Xml/OnieXmlExporter.cs
===================================================================
--- /OniSplit/Xml/OnieXmlExporter.cs	(revision 1114)
+++ /OniSplit/Xml/OnieXmlExporter.cs	(revision 1114)
@@ -0,0 +1,106 @@
+﻿using System.Xml;
+using Oni.Particles;
+
+namespace Oni.Xml
+{
+    internal class OnieXmlExporter : RawXmlExporter
+    {
+        private OnieXmlExporter(BinaryReader reader, XmlWriter xml)
+            : base(reader, xml)
+        {
+        }
+
+        public static void Export(BinaryReader reader, XmlWriter xml)
+        {
+            var exporter = new OnieXmlExporter(reader, xml);
+            exporter.Export();
+        }
+
+        private void Export()
+        {
+            Reader.Skip(8);
+            var impacts = new string[Reader.ReadInt32()];
+            var materials = new string[Reader.ReadInt32()];
+            var particles = new ImpactEffectParticle[Reader.ReadInt32()];
+            var sounds = new ImpactEffectSound[Reader.ReadInt32()];
+            var effects = new ImpactEffect[Reader.ReadInt32()];
+            Reader.Skip(4);
+
+            for (int i = 0; i < impacts.Length; i++)
+            {
+                impacts[i] = Reader.ReadString(128);
+                Reader.Skip(4);
+            }
+
+            for (int i = 0; i < materials.Length; i++)
+            {
+                materials[i] = Reader.ReadString(128);
+                Reader.Skip(4);
+            }
+
+            for (int i = 0; i < impacts.Length; i++)
+            {
+                Reader.Skip(8);
+            }
+
+            for (int i = 0; i < particles.Length; i++)
+            {
+                particles[i] = new ImpactEffectParticle(Reader);
+            }
+
+            for (int i = 0; i < sounds.Length; i++)
+            {
+                sounds[i] = new ImpactEffectSound(Reader);
+            }
+
+            for (int i = 0; i < effects.Length; i++)
+            {
+                effects[i] = new ImpactEffect(Reader, impacts, materials, particles, sounds);
+            }
+
+            Xml.WriteStartElement("ImpactEffects");
+
+            foreach (string impact in impacts)
+            {
+                Xml.WriteStartElement("Impact");
+                Xml.WriteAttributeString("Name", impact);
+
+                foreach (string material in materials)
+                {
+                    bool found = false;
+
+                    foreach (ImpactEffect effect in effects)
+                    {
+                        if (effect.ImpactName == impact && effect.MaterialName == material)
+                        {
+                            found = true;
+                            break;
+                        }
+                    }
+
+                    if (!found)
+                        continue;
+
+                    Xml.WriteStartElement("Material");
+                    Xml.WriteAttributeString("Name", material);
+
+                    foreach (ImpactEffect effect in effects)
+                    {
+                        if (effect.ImpactName == impact && effect.MaterialName == material)
+                        {
+                            Xml.WriteStartElement("ImpactEffect");
+                            effect.Write(Xml);
+                            Xml.WriteEndElement();
+                        }
+                    }
+
+                    Xml.WriteEndElement();
+                }
+
+                Xml.WriteEndElement();
+            }
+
+            Xml.WriteEndElement();
+        }
+    }
+}
Index: /OniSplit/Xml/OnieXmlImporter.cs
===================================================================
--- /OniSplit/Xml/OnieXmlImporter.cs	(revision 1114)
+++ /OniSplit/Xml/OnieXmlImporter.cs	(revision 1114)
@@ -0,0 +1,266 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Metadata;
+using Oni.Particles;
+
+namespace Oni.Xml
+{
+    internal class OnieXmlImporter : RawXmlImporter
+    {
+        #region Private data
+        private List<ImpactEffect> impactEffects = new List<ImpactEffect>();
+        private List<ImpactEffectSound> sounds = new List<ImpactEffectSound>();
+        private List<ImpactEffectParticle> particles = new List<ImpactEffectParticle>();
+        private Dictionary<string, int> materials = new Dictionary<string, int>();
+        private List<KeyValuePair<string, int>> impactList;
+        private List<KeyValuePair<string, int>> materialList;
+        private List<ImpactNode> impactNodes;
+        private List<MaterialNode> materialNodes;
+        #endregion
+
+        private OnieXmlImporter(XmlReader xml, BinaryWriter writer)
+            : base(xml, writer)
+        {
+        }
+
+        public static void Import(XmlReader xml, BinaryWriter writer)
+        {
+            var importer = new OnieXmlImporter(xml, writer);
+            importer.Read();
+            importer.Write();
+        }
+
+        #region private class ImpactLookupNode
+
+        private class ImpactNode
+        {
+            private int impactIndex;
+            private List<MaterialNode> materialNodes;
+
+            public ImpactNode(int impactIndex)
+            {
+                this.impactIndex = impactIndex;
+                this.materialNodes = new List<MaterialNode>();
+            }
+
+            public int ImpactIndex
+            {
+                get
+                {
+                    return impactIndex;
+                }
+            }
+
+            public List<MaterialNode> MaterialNodes
+            {
+                get
+                {
+                    return materialNodes;
+                }
+            }
+        }
+
+        #endregion
+        #region private class MaterialLookupNode
+
+        private class MaterialNode
+        {
+            private int materialIndex;
+            private List<ImpactEffect> impactEffects;
+
+            public MaterialNode(int materialIndex)
+            {
+                this.materialIndex = materialIndex;
+                this.impactEffects = new List<ImpactEffect>();
+            }
+
+            public int MaterialIndex
+            {
+                get
+                {
+                    return materialIndex;
+                }
+            }
+
+            public List<ImpactEffect> ImpactEffects
+            {
+                get
+                {
+                    return impactEffects;
+                }
+            }
+        }
+
+        #endregion
+
+        private void Read()
+        {
+            impactList = new List<KeyValuePair<string, int>>();
+
+            while (Xml.IsStartElement("Impact"))
+            {
+                int impactIndex = impactList.Count;
+                var impactName = Xml.GetAttribute("Name");
+                impactList.Add(new KeyValuePair<string, int>(impactName, impactIndex));
+
+                if (Xml.IsEmptyElement)
+                {
+                    Xml.ReadStartElement();
+                    continue;
+                }
+
+                Xml.ReadStartElement();
+
+                while (Xml.IsStartElement("Material"))
+                {
+                    var materialName = Xml.GetAttribute("Name");
+                    int materialIndex;
+
+                    if (!materials.TryGetValue(materialName, out materialIndex))
+                    {
+                        materialIndex = materials.Count;
+                        materials.Add(materialName, materialIndex);
+                    }
+
+                    if (Xml.IsEmptyElement)
+                    {
+                        Xml.ReadStartElement();
+                        continue;
+                    }
+
+                    Xml.ReadStartElement();
+
+                    while (Xml.IsStartElement("ImpactEffect"))
+                    {
+                        Xml.ReadStartElement();
+
+                        var impactEffect = new ImpactEffect(Xml, impactName, materialName)
+                        {
+                            ImpactIndex = impactIndex,
+                            MaterialIndex = materialIndex
+                        };
+
+                        if (impactEffect.Sound != null)
+                        {
+                            impactEffect.SoundIndex = sounds.Count;
+                            sounds.Add(impactEffect.Sound);
+                        }
+                        else
+                        {
+                            impactEffect.SoundIndex = -1;
+                        }
+
+                        if (impactEffect.Particles != null && impactEffect.Particles.Length > 0)
+                        {
+                            impactEffect.ParticleIndex = particles.Count;
+                            particles.AddRange(impactEffect.Particles);
+                        }
+                        else
+                        {
+                            impactEffect.ParticleIndex = -1;
+                        }
+
+                        impactEffects.Add(impactEffect);
+
+                        Xml.ReadEndElement();
+                    }
+
+                    Xml.ReadEndElement();
+                }
+
+                Xml.ReadEndElement();
+            }
+
+            materialList = new List<KeyValuePair<string, int>>(materials);
+            materialList.Sort((x, y) => x.Value.CompareTo(y.Value));
+
+            impactNodes = new List<ImpactNode>();
+            materialNodes = new List<MaterialNode>();
+
+            foreach (var impact in impactList)
+            {
+                var impactNode = new ImpactNode(impact.Value);
+                impactNodes.Add(impactNode);
+
+                foreach (var material in materialList)
+                {
+                    var materialNode = new MaterialNode(material.Value);
+
+                    foreach (var effect in impactEffects)
+                    {
+                        if (effect.MaterialIndex == material.Value && effect.ImpactIndex == impact.Value)
+                            materialNode.ImpactEffects.Add(effect);
+                    }
+
+                    if (materialNode.ImpactEffects.Count > 0)
+                    {
+                        impactNode.MaterialNodes.Add(materialNode);
+                        materialNodes.Add(materialNode);
+                    }
+                }
+            }
+        }
+
+        private void Write()
+        {
+            Writer.Write(2);
+            Writer.Write(impactList.Count);
+            Writer.Write(materialList.Count);
+            Writer.Write(particles.Count);
+            Writer.Write(sounds.Count);
+            Writer.Write(impactEffects.Count);
+            Writer.Write(materialNodes.Count);
+
+            foreach (KeyValuePair<string, int> impact in impactList)
+            {
+                Writer.Write(impact.Key, 128);
+                Writer.Write(0);
+            }
+
+            foreach (KeyValuePair<string, int> material in materialList)
+            {
+                Writer.Write(material.Key, 128);
+                Writer.Write(0);
+            }
+
+            int currentMaterialIndex = 0;
+
+            foreach (var impactNode in impactNodes)
+            {
+                Writer.WriteInt16(impactNode.ImpactIndex);
+                Writer.WriteInt16(impactNode.MaterialNodes.Count);
+                Writer.Write(currentMaterialIndex);
+
+                currentMaterialIndex += impactNode.MaterialNodes.Count;
+            }
+
+            foreach (var particle in particles)
+            {
+                particle.Write(Writer);
+            }
+
+            foreach (var sound in sounds)
+            {
+                sound.Write(Writer);
+            }
+
+            foreach (var materialNode in materialNodes)
+            {
+                foreach (var effect in materialNode.ImpactEffects)
+                    effect.Write(Writer);
+            }
+
+            int currentImpactEffectIndex = 0;
+
+            foreach (var materialNode in materialNodes)
+            {
+                Writer.WriteInt16(materialNode.MaterialIndex);
+                Writer.WriteInt16(materialNode.ImpactEffects.Count);
+                Writer.Write(currentImpactEffectIndex);
+
+                currentImpactEffectIndex += materialNode.ImpactEffects.Count;
+            }
+        }
+    }
+}
Index: /OniSplit/Xml/ParticleXml.cs
===================================================================
--- /OniSplit/Xml/ParticleXml.cs	(revision 1114)
+++ /OniSplit/Xml/ParticleXml.cs	(revision 1114)
@@ -0,0 +1,307 @@
+﻿using System;
+using Oni.Particles;
+
+namespace Oni.Xml
+{
+    internal class ParticleXml
+    {
+        protected static readonly ParticleFlags1[] optionFlags1 = new[]
+        {
+            ParticleFlags1.Decorative,
+            ParticleFlags1.CollideWithWalls,
+            ParticleFlags1.CollideWithChars,
+        };
+
+        protected static readonly ParticleFlags2[] optionFlags2 = new[]
+        {
+            ParticleFlags2.InitiallyHidden,
+            ParticleFlags2.DrawAsSky,
+            ParticleFlags2.DontAttractThroughWalls,
+            ParticleFlags2.ExpireOnCutscene,
+            ParticleFlags2.DieOnCutscene,
+            ParticleFlags2.LockPositionToLink,
+        };
+
+        protected static readonly ParticleFlags1[] appearanceFlags1 = new ParticleFlags1[]
+        {
+        };
+
+        protected static readonly ParticleFlags1[] appearanceExFlags1 = new[]
+        {
+            ParticleFlags1.ScaleToVelocity,
+            ParticleFlags1.UseSeparateYScale,
+        };
+
+        protected static readonly ParticleFlags2[] appearanceFlags2 = new[]
+        {
+            ParticleFlags2.Invisible,
+            ParticleFlags2.IsContrailEmitter
+        };
+
+        protected static readonly ParticleFlags2[] appearanceExFlags2 = new[]
+        {
+            ParticleFlags2.Invisible,
+            ParticleFlags2.UseSpecialTint,
+            ParticleFlags2.FadeOutOnEdge,
+            ParticleFlags2.OneSidedEdgeFade,
+            ParticleFlags2.LensFlare,
+            ParticleFlags2.DecalFullBrightness
+        };
+
+        protected class EventActionParameterInfo
+        {
+            public static readonly EventActionParameterInfo[] EmptyActionParameterInfos = new EventActionParameterInfo[0];
+            public string Name;
+            public StorageType Type;
+
+            public EventActionParameterInfo(string name, StorageType type)
+            {
+                this.Name = name;
+                this.Type = type;
+            }
+        }
+
+        protected class EventActionInfo
+        {
+            public int OutCount;
+            public EventActionParameterInfo[] Parameters;
+
+            public EventActionInfo(int outCount, params EventActionParameterInfo[] parmeters)
+            {
+                this.OutCount = outCount;
+                this.Parameters = parmeters;
+            }
+        }
+
+        protected static readonly EventActionInfo[] eventActionInfoTable = new EventActionInfo[]
+        {
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Target", StorageType.Float),
+                new EventActionParameterInfo("Rate", StorageType.Float)),
+            new EventActionInfo(2,
+                new EventActionParameterInfo("Target", StorageType.Float),
+                new EventActionParameterInfo("Velocity", StorageType.Float),
+                new EventActionParameterInfo("Acceleration", StorageType.Float)),
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Target", StorageType.Float),
+                new EventActionParameterInfo("Min", StorageType.Float),
+                new EventActionParameterInfo("Max", StorageType.Float),
+                new EventActionParameterInfo("Rate", StorageType.Float)),
+            new EventActionInfo(2,
+                new EventActionParameterInfo("Target", StorageType.Float),
+                new EventActionParameterInfo("State", StorageType.PingPongState),
+                new EventActionParameterInfo("Min", StorageType.Float),
+                new EventActionParameterInfo("Max", StorageType.Float),
+                new EventActionParameterInfo("Rate", StorageType.Float)),
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Target", StorageType.Float),
+                new EventActionParameterInfo("Min", StorageType.Float),
+                new EventActionParameterInfo("Max", StorageType.Float),
+                new EventActionParameterInfo("Rate", StorageType.Float)),
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Target", StorageType.Float),
+                new EventActionParameterInfo("Rate", StorageType.Float),
+                new EventActionParameterInfo("Value", StorageType.Float)),
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Target", StorageType.Color),
+                new EventActionParameterInfo("Color0", StorageType.Color),
+                new EventActionParameterInfo("Color1", StorageType.Color),
+                new EventActionParameterInfo("Amount", StorageType.Float)),
+            null,
+            new EventActionInfo(0,
+                new EventActionParameterInfo("TimeToDie", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Action", StorageType.ActionIndex),
+                new EventActionParameterInfo("Lifetime", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Action", StorageType.ActionIndex),
+                new EventActionParameterInfo("Lifetime", StorageType.Float)),
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Time", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Emitter", StorageType.Emitter)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Emitter", StorageType.Emitter)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Emitter", StorageType.Emitter),
+                new EventActionParameterInfo("Particles", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Emitter", StorageType.Emitter)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Emitter", StorageType.Emitter)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Emitter", StorageType.Emitter)),
+            null,
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Sound", StorageType.AmbientSoundName)),
+            new EventActionInfo(0),
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Sound", StorageType.ImpulseSoundName)),
+            null,
+            null,
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Damage", StorageType.Float),
+                new EventActionParameterInfo("StunDamage", StorageType.Float),
+                new EventActionParameterInfo("KnockBack", StorageType.Float),
+                new EventActionParameterInfo("DamageType", StorageType.DamageType),
+                new EventActionParameterInfo("SelfImmune", StorageType.Boolean),
+                new EventActionParameterInfo("CanHitMultiple", StorageType.Boolean)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Damage", StorageType.Float),
+                new EventActionParameterInfo("StunDamage", StorageType.Float),
+                new EventActionParameterInfo("KnockBack", StorageType.Float),
+                new EventActionParameterInfo("Radius", StorageType.Float),
+                new EventActionParameterInfo("FallOff", StorageType.BlastFalloff),
+                new EventActionParameterInfo("DamageType", StorageType.DamageType),
+                new EventActionParameterInfo("SelfImmune", StorageType.Boolean),
+                new EventActionParameterInfo("DamageEnvironment", StorageType.Boolean)),
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Damage", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("BlastVelocity", StorageType.Float),
+                new EventActionParameterInfo("Radius", StorageType.Float)),
+            new EventActionInfo(0),
+            null,
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Space", StorageType.CoordFrame),
+                new EventActionParameterInfo("Rate", StorageType.Float),
+                new EventActionParameterInfo("RotateVelocity", StorageType.Boolean)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Space", StorageType.CoordFrame),
+                new EventActionParameterInfo("Rate", StorageType.Float),
+                new EventActionParameterInfo("RotateVelocity", StorageType.Boolean)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Space", StorageType.CoordFrame),
+                new EventActionParameterInfo("Rate", StorageType.Float),
+                new EventActionParameterInfo("RotateVelocity", StorageType.Boolean)),
+            null,
+            new EventActionInfo(0,
+                new EventActionParameterInfo("DelayTime", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Gravity", StorageType.Float),
+                new EventActionParameterInfo("MaxG", StorageType.Float),
+                new EventActionParameterInfo("HorizontalOnly", StorageType.Boolean)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("TurnSpeed", StorageType.Float),
+                new EventActionParameterInfo("PredictPosition", StorageType.Boolean),
+                new EventActionParameterInfo("HorizontalOnly", StorageType.Boolean)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("AccelRate", StorageType.Float),
+                new EventActionParameterInfo("MaxAccel", StorageType.Float),
+                new EventActionParameterInfo("DesiredDistance", StorageType.Float)),
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Fraction", StorageType.Float)),
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Theta", StorageType.Float),
+                new EventActionParameterInfo("Radius", StorageType.Float),
+                new EventActionParameterInfo("RotateSpeed", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Resistance", StorageType.Float),
+                new EventActionParameterInfo("MinimumVelocity", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Acceleration", StorageType.Float),
+                new EventActionParameterInfo("MaxSpeed", StorageType.Float),
+                new EventActionParameterInfo("SidewaysDecay", StorageType.Float),
+                new EventActionParameterInfo("DirX", StorageType.Float),
+                new EventActionParameterInfo("DirY", StorageType.Float),
+                new EventActionParameterInfo("DirZ", StorageType.Float),
+                new EventActionParameterInfo("Space", StorageType.CoordFrame)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Speed", StorageType.Float),
+                new EventActionParameterInfo("Space", StorageType.CoordFrame),
+                new EventActionParameterInfo("NoSideways", StorageType.Boolean)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Theta", StorageType.Float),
+                new EventActionParameterInfo("Radius", StorageType.Float),
+                new EventActionParameterInfo("Rotate_speed", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Direction", StorageType.Direction),
+                new EventActionParameterInfo("Value", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Effect", StorageType.ImpactName),
+                new EventActionParameterInfo("WallOffset", StorageType.Float),
+                new EventActionParameterInfo("Orientation", StorageType.CollisionOrient),
+                new EventActionParameterInfo("Attach", StorageType.Boolean)),
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("ElasticDirect", StorageType.Float),
+                new EventActionParameterInfo("ElasticGlancing", StorageType.Float)),
+            new EventActionInfo(0),
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("ImpactType", StorageType.ImpactName),
+                new EventActionParameterInfo("ImpactModifier", StorageType.ImpactModifier)),
+            null,
+            new EventActionInfo(0),
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Tick", StorageType.Float)),
+            new EventActionInfo(0),
+            null,
+            null,
+            null,
+            null,
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Target", StorageType.Float),
+                new EventActionParameterInfo("Value", StorageType.Float)),
+            new EventActionInfo(0),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Action", StorageType.ActionIndex),
+                new EventActionParameterInfo("Var", StorageType.Float),
+                new EventActionParameterInfo("Threshold", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Action", StorageType.ActionIndex),
+                new EventActionParameterInfo("Var", StorageType.Float),
+                new EventActionParameterInfo("Threshold", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Action", StorageType.ActionIndex)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Action", StorageType.ActionIndex)),
+            null,
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Emitter", StorageType.Emitter),
+                new EventActionParameterInfo("FuseTime", StorageType.Float)),
+            new EventActionInfo(0),
+            new EventActionInfo(5,
+                new EventActionParameterInfo("AxisX", StorageType.Float),
+                new EventActionParameterInfo("AxisY", StorageType.Float),
+                new EventActionParameterInfo("AxisZ", StorageType.Float),
+                new EventActionParameterInfo("CurrentAngle", StorageType.Float),
+                new EventActionParameterInfo("TimeUntilCheck", StorageType.Float),
+                new EventActionParameterInfo("SenseDistance", StorageType.Float),
+                new EventActionParameterInfo("TurningSpeed", StorageType.Float),
+                new EventActionParameterInfo("TurningDecay", StorageType.Float)),
+            new EventActionInfo(1,
+                new EventActionParameterInfo("SwirlAngle", StorageType.Float),
+                new EventActionParameterInfo("SwirlBaseRate", StorageType.Float),
+                new EventActionParameterInfo("SwirlDeltaRate", StorageType.Float),
+                new EventActionParameterInfo("SwirlSpeed", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Height", StorageType.Float)),
+            new EventActionInfo(0,
+                new EventActionParameterInfo("Speed", StorageType.Float)),
+            new EventActionInfo(1,
+                new EventActionParameterInfo("Variable", StorageType.Float),
+                new EventActionParameterInfo("BaseValue", StorageType.Float),
+                new EventActionParameterInfo("DeltaValue", StorageType.Float),
+                new EventActionParameterInfo("MinValue", StorageType.Float),
+                new EventActionParameterInfo("MaxValue", StorageType.Float)),
+            new EventActionInfo(0),
+            new EventActionInfo(0),
+            new EventActionInfo(0),
+            null,
+            null,
+            null,
+        };
+    }
+}
Index: /OniSplit/Xml/ParticleXmlExporter.cs
===================================================================
--- /OniSplit/Xml/ParticleXmlExporter.cs	(revision 1114)
+++ /OniSplit/Xml/ParticleXmlExporter.cs	(revision 1114)
@@ -0,0 +1,507 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml;
+using Oni.Imaging;
+
+namespace Oni.Xml
+{
+    using Oni.Particles;
+
+    internal class ParticleXmlExporter : ParticleXml
+    {
+        #region Private data
+        private Particle particle;
+        private XmlWriter xml;
+        #endregion
+
+        private ParticleXmlExporter(Particle particle, XmlWriter writer)
+        {
+            this.particle = particle;
+            this.xml = writer;
+        }
+
+        public static void Export(string name, BinaryReader rawReader, XmlWriter xml)
+        {
+            var particle = new Oni.Particles.Particle(rawReader);
+
+            xml.WriteStartElement("Particle");
+            xml.WriteAttributeString("Name", name);
+
+            var exporter = new ParticleXmlExporter(particle, xml);
+            exporter.Write();
+
+            xml.WriteEndElement();
+        }
+
+        public void Write()
+        {
+            WriteOptions();
+            WriteProperties();
+            WriteAppearance();
+            WriteAttractor();
+            WriteVariables();
+            WriteEmitters();
+            WriteEvents();
+        }
+
+        private void WriteProperties()
+        {
+            xml.WriteStartElement("Properties");
+
+            int flagValue = 0x00001000;
+
+            for (int i = 0; i < 13; i++, flagValue <<= 1)
+                WriteFlag1((ParticleFlags1)flagValue);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteOptions()
+        {
+            xml.WriteStartElement("Options");
+
+            WriteValue(particle.Lifetime, "Lifetime");
+            xml.WriteElementString("DisableDetailLevel", particle.DisableDetailLevel.ToString());
+
+            foreach (ParticleFlags1 flag in optionFlags1)
+                WriteFlag1(flag);
+
+            foreach (ParticleFlags2 flag in optionFlags2)
+                WriteFlag2(flag);
+
+            WriteValue(particle.CollisionRadius, "CollisionRadius");
+            xml.WriteElementString("AIDodgeRadius", XmlConvert.ToString(particle.AIDodgeRadius));
+            xml.WriteElementString("AIAlertRadius", XmlConvert.ToString(particle.AIAlertRadius));
+            xml.WriteElementString("FlyBySoundName", particle.FlyBySoundName);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteAttractor()
+        {
+            var attractor = particle.Attractor;
+
+            xml.WriteStartElement("Attractor");
+
+            xml.WriteElementString("Target", attractor.Target.ToString());
+            xml.WriteElementString("Selector", attractor.Selector.ToString());
+
+            xml.WriteElementString("Class", attractor.ClassName);
+            WriteValue(attractor.MaxDistance, "MaxDistance");
+            WriteValue(attractor.MaxAngle, "MaxAngle");
+            WriteValue(attractor.AngleSelectMin, "AngleSelectMin");
+            WriteValue(attractor.AngleSelectMax, "AngleSelectMax");
+            WriteValue(attractor.AngleSelectWeight, "AngleSelectWeight");
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteAppearance()
+        {
+            var appearance = particle.Appearance;
+
+            xml.WriteStartElement("Appearance");
+
+            if ((particle.Flags1 & ParticleFlags1.Geometry) != 0)
+            {
+                xml.WriteElementString("DisplayType", "Geometry");
+                xml.WriteElementString("TexGeom", appearance.TextureName);
+            }
+            else if ((particle.Flags2 & ParticleFlags2.Vector) != 0)
+            {
+                xml.WriteElementString("DisplayType", "Vector");
+            }
+            else if ((particle.Flags2 & ParticleFlags2.Decal) != 0)
+            {
+                xml.WriteElementString("DisplayType", "Decal");
+                xml.WriteElementString("TexGeom", appearance.TextureName);
+            }
+            else
+            {
+                xml.WriteElementString("DisplayType", particle.SpriteType.ToString());
+                xml.WriteElementString("TexGeom", appearance.TextureName);
+            }
+
+            foreach (ParticleFlags1 flag in appearanceFlags1)
+                WriteFlag1(flag);
+
+            foreach (ParticleFlags2 flag in appearanceFlags2)
+                WriteFlag2(flag);
+
+            WriteFlag1(ParticleFlags1.ScaleToVelocity);
+            WriteValue(appearance.Scale, "Scale");
+            WriteFlag1(ParticleFlags1.UseSeparateYScale);
+            WriteValue(appearance.YScale, "YScale");
+            WriteValue(appearance.Rotation, "Rotation");
+            WriteValue(appearance.Alpha, "Alpha");
+            WriteValue(appearance.XOffset, "XOffset");
+            WriteValue(appearance.XShorten, "XShorten");
+
+            WriteFlag2(ParticleFlags2.UseSpecialTint);
+            WriteValue(appearance.Tint, "Tint");
+
+            WriteFlag2(ParticleFlags2.FadeOutOnEdge);
+            WriteFlag2(ParticleFlags2.OneSidedEdgeFade);
+            WriteValue(appearance.EdgeFadeMin, "EdgeFadeMin");
+            WriteValue(appearance.EdgeFadeMax, "EdgeFadeMax");
+
+            WriteValue(appearance.MaxContrail, "MaxContrailDistance");
+
+            WriteFlag2(ParticleFlags2.LensFlare);
+            WriteValue(appearance.LensFlareDistance, "LensFlareDistance");
+
+            xml.WriteElementString("LensFlareFadeInFrames", XmlConvert.ToString(appearance.LensFlareFadeInFrames));
+            xml.WriteElementString("LensFlareFadeOutFrames", XmlConvert.ToString(appearance.LensFlareFadeOutFrames));
+
+            WriteFlag2(ParticleFlags2.DecalFullBrightness);
+            xml.WriteElementString("MaxDecals", XmlConvert.ToString(appearance.MaxDecals));
+            xml.WriteElementString("DecalFadeFrames", XmlConvert.ToString(appearance.DecalFadeFrames));
+            WriteValue(appearance.DecalWrapAngle, "DecalWrapAngle");
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteFlag1(ParticleFlags1 flag)
+        {
+            xml.WriteElementString(flag.ToString(), ((particle.Flags1 & flag) != 0) ? "true" : "false");
+        }
+
+        private void WriteFlag2(ParticleFlags2 flag)
+        {
+            xml.WriteElementString(flag.ToString(), ((particle.Flags2 & flag) != 0) ? "true" : "false");
+        }
+
+        private void WriteVariables()
+        {
+            xml.WriteStartElement("Variables");
+
+            foreach (Variable variable in particle.Variables)
+                WriteVariable(variable);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteVariable(Variable variable)
+        {
+            switch (variable.StorageType)
+            {
+                case StorageType.Float:
+                    xml.WriteStartElement("Float");
+                    break;
+                case StorageType.Color:
+                    xml.WriteStartElement("Color");
+                    break;
+                case StorageType.PingPongState:
+                    xml.WriteStartElement("PingPongState");
+                    break;
+            }
+
+            xml.WriteAttributeString("Name", variable.Name);
+            WriteValue(variable.Value);
+            xml.WriteEndElement();
+        }
+
+        private void WriteEmitters()
+        {
+            xml.WriteStartElement("Emitters");
+
+            foreach (Emitter e in particle.Emitters)
+                WriteEmitter(e);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteEmitter(Emitter emitter)
+        {
+            xml.WriteStartElement("Emitter");
+            xml.WriteElementString("Class", emitter.ParticleClass);
+            xml.WriteElementString("Flags", (emitter.Flags == EmitterFlags.None) ? string.Empty : emitter.Flags.ToString().Replace(",", string.Empty));
+            xml.WriteElementString("TurnOffTreshold", XmlConvert.ToString(emitter.TurnOffTreshold));
+            xml.WriteElementString("Probability", XmlConvert.ToString(emitter.Probability / 65535.0f));
+            xml.WriteElementString("Copies", XmlConvert.ToString(emitter.Copies));
+
+            switch (emitter.LinkTo)
+            {
+                case 0:
+                    xml.WriteElementString("LinkTo", string.Empty);
+                    break;
+                case 1:
+                    xml.WriteElementString("LinkTo", "this");
+                    break;
+                case 10:
+                    xml.WriteElementString("LinkTo", "link");
+                    break;
+                default:
+                    xml.WriteElementString("LinkTo", XmlConvert.ToString(emitter.LinkTo - 2));
+                    break;
+            }
+
+            WriteEmitterRate(emitter);
+            WriteEmitterPosition(emitter);
+            WriteEmitterDirection(emitter);
+            WriteEmitterSpeed(emitter);
+
+            xml.WriteElementString("Orientation", emitter.OrientationDir.ToString());
+            xml.WriteElementString("OrientationUp", emitter.OrientationUp.ToString());
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteEmitterRate(Emitter emitter)
+        {
+            xml.WriteStartElement("Rate");
+            xml.WriteStartElement(emitter.Rate.ToString());
+
+            switch (emitter.Rate)
+            {
+                case EmitterRate.Continous:
+                    WriteValue(emitter.Parameters[0], "Interval");
+                    break;
+                case EmitterRate.Random:
+                    WriteValue(emitter.Parameters[0], "MinInterval");
+                    WriteValue(emitter.Parameters[1], "MaxInterval");
+                    break;
+                case EmitterRate.Distance:
+                    WriteValue(emitter.Parameters[0], "Distance");
+                    break;
+                case EmitterRate.Attractor:
+                    WriteValue(emitter.Parameters[0], "RechargeTime");
+                    WriteValue(emitter.Parameters[1], "CheckInterval");
+                    break;
+            }
+
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+        }
+
+        private void WriteEmitterPosition(Emitter emitter)
+        {
+            xml.WriteStartElement("Position");
+            xml.WriteStartElement(emitter.Position.ToString());
+
+            switch (emitter.Position)
+            {
+                case EmitterPosition.Line:
+                    WriteValue(emitter.Parameters[2], "Radius");
+                    break;
+                case EmitterPosition.Circle:
+                case EmitterPosition.Sphere:
+                    WriteValue(emitter.Parameters[2], "InnerRadius");
+                    WriteValue(emitter.Parameters[3], "OuterRadius");
+                    break;
+                case EmitterPosition.Offset:
+                    WriteValue(emitter.Parameters[2], "X");
+                    WriteValue(emitter.Parameters[3], "Y");
+                    WriteValue(emitter.Parameters[4], "Z");
+                    break;
+                case EmitterPosition.Cylinder:
+                    WriteValue(emitter.Parameters[2], "Height");
+                    WriteValue(emitter.Parameters[3], "InnerRadius");
+                    WriteValue(emitter.Parameters[4], "OuterRadius");
+                    break;
+                case EmitterPosition.BodySurface:
+                case EmitterPosition.BodyBones:
+                    WriteValue(emitter.Parameters[2], "OffsetRadius");
+                    break;
+            }
+
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+        }
+
+        private void WriteEmitterDirection(Emitter emitter)
+        {
+            xml.WriteStartElement("Direction");
+            xml.WriteStartElement(emitter.Direction.ToString());
+
+            switch (emitter.Direction)
+            {
+                case EmitterDirection.Cone:
+                    WriteValue(emitter.Parameters[5], "Angle");
+                    WriteValue(emitter.Parameters[6], "CenterBias");
+                    break;
+                case EmitterDirection.Ring:
+                    WriteValue(emitter.Parameters[5], "Angle");
+                    WriteValue(emitter.Parameters[6], "Offset");
+                    break;
+                case EmitterDirection.Offset:
+                    WriteValue(emitter.Parameters[5], "X");
+                    WriteValue(emitter.Parameters[6], "Y");
+                    WriteValue(emitter.Parameters[7], "Z");
+                    break;
+                case EmitterDirection.Inaccurate:
+                    WriteValue(emitter.Parameters[5], "BaseAngle");
+                    WriteValue(emitter.Parameters[6], "Inaccuracy");
+                    WriteValue(emitter.Parameters[7], "CenterBias");
+                    break;
+            }
+
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+        }
+
+        private void WriteEmitterSpeed(Emitter emitter)
+        {
+            xml.WriteStartElement("Speed");
+            xml.WriteStartElement(emitter.Speed.ToString());
+
+            switch (emitter.Speed)
+            {
+                case EmitterSpeed.Uniform:
+                    WriteValue(emitter.Parameters[8], "Speed");
+                    break;
+                case EmitterSpeed.Stratified:
+                    WriteValue(emitter.Parameters[8], "Speed1");
+                    WriteValue(emitter.Parameters[9], "Speed2");
+                    break;
+            }
+
+            xml.WriteEndElement();
+            xml.WriteEndElement();
+        }
+
+        private void WriteEvents()
+        {
+            xml.WriteStartElement("Events");
+
+            foreach (Event e in particle.Events)
+                WriteEvent(e);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteEvent(Event e)
+        {
+            if (e.Actions.Count == 0)
+                return;
+
+            xml.WriteStartElement(e.Type.ToString());
+
+            foreach (EventAction action in e.Actions)
+                WriteAction(action);
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteAction(EventAction action)
+        {
+            int actionTypeIndex = (int)action.Type;
+
+            if (actionTypeIndex >= eventActionInfoTable.Length || eventActionInfoTable[actionTypeIndex] == null)
+            {
+                Console.Error.WriteLine("ParticleXmlExporter: Unknown event action type {0}, ignoring", actionTypeIndex);
+                return;
+            }
+
+            xml.WriteStartElement(action.Type.ToString());
+
+            EventActionInfo info = eventActionInfoTable[actionTypeIndex];
+            int i = 0;
+
+            foreach (VariableReference variable in action.Variables)
+            {
+                if (i < info.Parameters.Length)
+                {
+                    xml.WriteElementString(info.Parameters[i++].Name, variable.Name);
+                }
+            }
+
+            foreach (Value value in action.Parameters)
+            {
+                if (i < info.Parameters.Length)
+                {
+                    xml.WriteStartElement(info.Parameters[i++].Name);
+                    WriteValue(value);
+                    xml.WriteEndElement();
+                }
+            }
+
+            xml.WriteEndElement();
+        }
+
+        private void WriteValue(Value value, string name)
+        {
+            xml.WriteStartElement(name);
+            WriteValue(value);
+            xml.WriteEndElement();
+        }
+
+        private void WriteValue(Value value)
+        {
+            if (value == null)
+                return;
+
+            switch (value.Type)
+            {
+                case ValueType.Variable:
+                case ValueType.InstanceName:
+                    xml.WriteString(value.Name);
+                    break;
+
+                case ValueType.Float:
+                    xml.WriteString(XmlConvert.ToString(value.Float1));
+                    break;
+
+                case ValueType.FloatRandom:
+                    xml.WriteStartElement("Random");
+                    xml.WriteAttributeString("Min", XmlConvert.ToString(value.Float1));
+                    xml.WriteAttributeString("Max", XmlConvert.ToString(value.Float2));
+                    xml.WriteEndElement();
+                    break;
+
+                case ValueType.FloatBellCurve:
+                    xml.WriteStartElement("BellCurve");
+                    xml.WriteAttributeString("Mean", XmlConvert.ToString(value.Float1));
+                    xml.WriteAttributeString("StdDev", XmlConvert.ToString(value.Float2));
+                    xml.WriteEndElement();
+                    break;
+
+                case ValueType.Color:
+                    WriteColor(value.Color1);
+                    break;
+
+                case ValueType.ColorRandom:
+                    xml.WriteStartElement("Random");
+                    WriteColorAttribute("Min", value.Color1);
+                    WriteColorAttribute("Max", value.Color2);
+                    xml.WriteEndElement();
+                    break;
+
+                case ValueType.ColorBellCurve:
+                    xml.WriteStartElement("BellCurve");
+                    WriteColorAttribute("Mean", value.Color1);
+                    WriteColorAttribute("StdDev", value.Color2);
+                    xml.WriteEndElement();
+                    break;
+
+                case ValueType.Int32:
+                    xml.WriteString(XmlConvert.ToString(value.Int));
+                    break;
+
+                case ValueType.TimeCycle:
+                    xml.WriteStartElement("TimeCycle");
+                    xml.WriteAttributeString("Length", XmlConvert.ToString(value.Float1));
+                    xml.WriteAttributeString("Scale", XmlConvert.ToString(value.Float2));
+                    xml.WriteEndElement();
+                    break;
+            }
+        }
+
+        private void WriteColor(Color color)
+        {
+            if (color.A == 255)
+                xml.WriteValue(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", color.R, color.G, color.B));
+            else
+                xml.WriteValue(string.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}", color.R, color.G, color.B, color.A));
+        }
+
+        private void WriteColorAttribute(string name, Color color)
+        {
+            if (color.A == 255)
+                xml.WriteAttributeString(name, string.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", color.R, color.G, color.B));
+            else
+                xml.WriteAttributeString(name, string.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3}", color.R, color.G, color.B, color.A));
+        }
+    }
+}
Index: /OniSplit/Xml/ParticleXmlImporter.cs
===================================================================
--- /OniSplit/Xml/ParticleXmlImporter.cs	(revision 1114)
+++ /OniSplit/Xml/ParticleXmlImporter.cs	(revision 1114)
@@ -0,0 +1,827 @@
+﻿using System;
+using System.Globalization;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    using Oni.Particles;
+
+    internal class ParticleXmlImporter : ParticleXml
+    {
+        #region Private data
+        private XmlReader xml;
+        private Particle particle;
+        #endregion
+
+        public ParticleXmlImporter(XmlReader xml)
+        {
+            this.xml = xml;
+            this.particle = new Particle();
+        }
+
+        public static void Import(XmlReader xml, BinaryWriter writer)
+        {
+            int lengthPosition = writer.Position;
+
+            writer.WriteUInt16(0);
+            writer.WriteUInt16(18);
+
+            var reader = new ParticleXmlImporter(xml);
+            reader.Read();
+            reader.particle.Write(writer);
+
+            int length = writer.Position - lengthPosition;
+            writer.PushPosition(lengthPosition);
+            writer.WriteUInt16(length);
+            writer.PopPosition();
+        }
+
+        public void Read()
+        {
+            ReadOptions();
+            ReadProperties();
+            ReadAppearance();
+            ReadAttractor();
+            ReadVariables();
+            ReadEmitters();
+            ReadEvents();
+        }
+
+        private void ReadOptions()
+        {
+            if (!xml.IsStartElement("Options"))
+                return;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "DisableDetailLevel":
+                        particle.DisableDetailLevel = MetaEnum.Parse<DisableDetailLevel>(xml.ReadElementContentAsString());
+                        continue;
+
+                    case "Lifetime":
+                        particle.Lifetime = ReadValueFloat();
+                        continue;
+
+                    case "CollisionRadius":
+                        particle.CollisionRadius = ReadValueFloat();
+                        continue;
+
+                    case "FlyBySoundName":
+                        particle.FlyBySoundName = xml.ReadElementContentAsString();
+                        continue;
+
+                    case "AIAlertRadius":
+                        particle.AIAlertRadius = xml.ReadElementContentAsFloat();
+                        continue;
+
+                    case "AIDodgeRadius":
+                        particle.AIDodgeRadius = xml.ReadElementContentAsFloat();
+                        continue;
+
+                    default:
+                        if (ReadFlag1() || ReadFlag2())
+                            continue;
+                        break;
+                }
+
+                throw new FormatException(string.Format("Unknown option {0}", xml.LocalName));
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadProperties()
+        {
+            if (!xml.IsStartElement("Properties"))
+                return;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                if (ReadFlag1())
+                    continue;
+
+                throw new FormatException(string.Format("Unknown property {0}", xml.LocalName));
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadAppearance()
+        {
+            if (!xml.IsStartElement("Appearance"))
+                return;
+
+            Appearance appearance = particle.Appearance;
+
+            xml.ReadStartElement();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "DisplayType":
+                        string text = xml.ReadElementContentAsString();
+
+                        switch (text)
+                        {
+                            case "Geometry":
+                                particle.Flags1 |= ParticleFlags1.Geometry;
+                                break;
+                            case "Vector":
+                                particle.Flags2 |= ParticleFlags2.Vector;
+                                break;
+                            case "Decal":
+                                particle.Flags2 |= ParticleFlags2.Decal;
+                                break;
+                            default:
+                                particle.SpriteType = MetaEnum.Parse<SpriteType>(text);
+                                break;
+                        }
+
+                        continue;
+
+                    case "TexGeom":
+                        appearance.TextureName = xml.ReadElementContentAsString();
+                        continue;
+
+                    case "Scale":
+                        appearance.Scale = ReadValueFloat();
+                        continue;
+                    case "YScale":
+                        appearance.YScale = ReadValueFloat();
+                        continue;
+                    case "Rotation":
+                        appearance.Rotation = ReadValueFloat();
+                        continue;
+                    case "Alpha":
+                        appearance.Alpha = ReadValueFloat();
+                        continue;
+                    case "XOffset":
+                        appearance.XOffset = ReadValueFloat();
+                        continue;
+                    case "XShorten":
+                        appearance.XShorten = ReadValueFloat();
+                        continue;
+                    case "Tint":
+                        appearance.Tint = ReadValueColor();
+                        continue;
+                    case "EdgeFadeMin":
+                        appearance.EdgeFadeMin = ReadValueFloat();
+                        continue;
+                    case "EdgeFadeMax":
+                        appearance.EdgeFadeMax = ReadValueFloat();
+                        continue;
+                    case "MaxContrailDistance":
+                        appearance.MaxContrail = ReadValueFloat();
+                        continue;
+                    case "LensFlareDistance":
+                        appearance.LensFlareDistance = ReadValueFloat();
+                        continue;
+                    case "LensFlareFadeInFrames":
+                        appearance.LensFlareFadeInFrames = xml.ReadElementContentAsInt();
+                        continue;
+                    case "LensFlareFadeOutFrames":
+                        appearance.LensFlareFadeOutFrames = xml.ReadElementContentAsInt();
+                        continue;
+                    case "MaxDecals":
+                        appearance.MaxDecals = xml.ReadElementContentAsInt();
+                        continue;
+                    case "DecalFadeFrames":
+                        appearance.DecalFadeFrames = xml.ReadElementContentAsInt();
+                        continue;
+                    case "DecalWrapAngle":
+                        appearance.DecalWrapAngle = ReadValueFloat();
+                        continue;
+                    default:
+                        if (ReadFlag1() || ReadFlag2())
+                            continue;
+                        break;
+                }
+
+                throw new FormatException(string.Format("Unknown appearance property {0}", xml.LocalName));
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadAttractor()
+        {
+            if (!xml.IsStartElement("Attractor"))
+                return;
+
+            xml.ReadStartElement();
+
+            Attractor attractor = particle.Attractor;
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Target":
+                        attractor.Target = (AttractorTarget)Enum.Parse(typeof(AttractorTarget), xml.ReadElementContentAsString());
+                        continue;
+                    case "Selector":
+                        attractor.Selector = (AttractorSelector)Enum.Parse(typeof(AttractorSelector), xml.ReadElementContentAsString());
+                        continue;
+                    case "Class":
+                        attractor.ClassName = xml.ReadElementContentAsString();
+                        continue;
+                    case "MaxDistance":
+                        attractor.MaxDistance = ReadValueFloat();
+                        continue;
+                    case "MaxAngle":
+                        attractor.MaxAngle = ReadValueFloat();
+                        continue;
+                    case "AngleSelectMax":
+                        attractor.AngleSelectMax = ReadValueFloat();
+                        continue;
+                    case "AngleSelectMin":
+                        attractor.AngleSelectMin = ReadValueFloat();
+                        continue;
+                    case "AngleSelectWeight":
+                        attractor.AngleSelectWeight = ReadValueFloat();
+                        continue;
+                }
+
+                throw new FormatException(string.Format("Unknown attractor property {0}", xml.LocalName));
+            }
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadVariables()
+        {
+            if (!xml.IsStartElement("Variables"))
+                return;
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                while (xml.IsStartElement())
+                    particle.Variables.Add(ReadVariable());
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private Variable ReadVariable()
+        {
+            string typeName = xml.LocalName;
+            string name = xml.GetAttribute("Name");
+
+            switch (typeName)
+            {
+                case "Float":
+                    return new Variable(name, StorageType.Float, ReadValueFloat());
+
+                case "Color":
+                    return new Variable(name, StorageType.Color, ReadValueColor());
+
+                case "PingPongState":
+                    return new Variable(name, StorageType.PingPongState, ReadValueInt());
+
+                default:
+                    throw new XmlException(string.Format("Unknown variable type '{0}'", typeName));
+
+            }
+        }
+
+        private void ReadEmitters()
+        {
+            if (!xml.IsStartElement("Emitters"))
+                return;
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                while (xml.IsStartElement())
+                    particle.Emitters.Add(ReadEmitter());
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private Emitter ReadEmitter()
+        {
+            xml.ReadStartElement();
+
+            Emitter emitter = new Emitter();
+
+            while (xml.IsStartElement())
+            {
+                switch (xml.LocalName)
+                {
+                    case "Class":
+                        emitter.ParticleClass = xml.ReadElementContentAsString();
+                        continue;
+
+                    case "Flags":
+                        emitter.Flags = MetaEnum.Parse<EmitterFlags>(xml.ReadElementContentAsString());
+                        continue;
+
+                    case "TurnOffTreshold":
+                        emitter.TurnOffTreshold = xml.ReadElementContentAsInt();
+                        continue;
+
+                    case "Probability":
+                        emitter.Probability = (int)(xml.ReadElementContentAsFloat() * 65535.0f);
+                        continue;
+
+                    case "Copies":
+                        emitter.Copies = xml.ReadElementContentAsFloat();
+                        continue;
+
+                    case "LinkTo":
+                        string text = xml.ReadElementContentAsString();
+                        if (!string.IsNullOrEmpty(text))
+                        {
+                            if (text == "this")
+                                emitter.LinkTo = 1;
+                            else if (text == "link")
+                                emitter.LinkTo = 10;
+                            else
+                                emitter.LinkTo = int.Parse(text, CultureInfo.InvariantCulture) + 2;
+                        }
+                        continue;
+
+                    case "Rate":
+                        xml.ReadStartElement();
+                        ReadEmitterRate(emitter);
+                        xml.ReadEndElement();
+                        continue;
+
+                    case "Position":
+                        xml.ReadStartElement();
+                        ReadEmitterPosition(emitter);
+                        xml.ReadEndElement();
+                        continue;
+
+                    case "Speed":
+                        xml.ReadStartElement();
+                        ReadEmitterSpeed(emitter);
+                        xml.ReadEndElement();
+                        continue;
+
+                    case "Direction":
+                        xml.ReadStartElement();
+                        ReadEmitterDirection(emitter);
+                        xml.ReadEndElement();
+                        continue;
+
+                    case "Orientation":
+                        emitter.OrientationDir = MetaEnum.Parse<EmitterOrientation>(xml.ReadElementContentAsString());
+                        continue;
+
+                    case "OrientationUp":
+                        emitter.OrientationUp = MetaEnum.Parse<EmitterOrientation>(xml.ReadElementContentAsString());
+                        continue;
+                }
+
+                throw new FormatException(string.Format("Unknown emitter property '{0}'", xml.LocalName));
+            }
+
+            xml.ReadEndElement();
+
+            return emitter;
+        }
+
+        private void ReadEmitterRate(Emitter emitter)
+        {
+            emitter.Rate = MetaEnum.Parse<EmitterRate>(xml.LocalName);
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                switch (emitter.Rate)
+                {
+                    case EmitterRate.Continous:
+                        emitter.Parameters[0] = ReadValueFloat("Interval");
+                        break;
+                    case EmitterRate.Random:
+                        emitter.Parameters[0] = ReadValueFloat("MinInterval");
+                        emitter.Parameters[1] = ReadValueFloat("MaxInterval");
+                        break;
+                    case EmitterRate.Distance:
+                        emitter.Parameters[0] = ReadValueFloat("Distance");
+                        break;
+                    case EmitterRate.Attractor:
+                        emitter.Parameters[0] = ReadValueFloat("RechargeTime");
+                        emitter.Parameters[1] = ReadValueFloat("CheckInterval");
+                        break;
+
+                }
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private void ReadEmitterPosition(Emitter emitter)
+        {
+            emitter.Position = MetaEnum.Parse<EmitterPosition>(xml.LocalName);
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                switch (emitter.Position)
+                {
+                    case EmitterPosition.Line:
+                        emitter.Parameters[2] = ReadValueFloat("Radius");
+                        break;
+                    case EmitterPosition.Circle:
+                    case EmitterPosition.Sphere:
+                        emitter.Parameters[2] = ReadValueFloat("InnerRadius");
+                        emitter.Parameters[3] = ReadValueFloat("OuterRadius");
+                        break;
+                    case EmitterPosition.Offset:
+                        emitter.Parameters[2] = ReadValueFloat("X");
+                        emitter.Parameters[3] = ReadValueFloat("Y");
+                        emitter.Parameters[4] = ReadValueFloat("Z");
+                        break;
+                    case EmitterPosition.Cylinder:
+                        emitter.Parameters[2] = ReadValueFloat("Height");
+                        emitter.Parameters[3] = ReadValueFloat("InnerRadius");
+                        emitter.Parameters[4] = ReadValueFloat("OuterRadius");
+                        break;
+                    case EmitterPosition.BodySurface:
+                    case EmitterPosition.BodyBones:
+                        emitter.Parameters[2] = ReadValueFloat("OffsetRadius");
+                        break;
+                }
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private void ReadEmitterDirection(Emitter emitter)
+        {
+            emitter.Direction = MetaEnum.Parse<EmitterDirection>(xml.LocalName);
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                switch (emitter.Direction)
+                {
+                    case EmitterDirection.Cone:
+                        emitter.Parameters[5] = ReadValueFloat("Angle");
+                        emitter.Parameters[6] = ReadValueFloat("CenterBias");
+                        break;
+                    case EmitterDirection.Ring:
+                        emitter.Parameters[5] = ReadValueFloat("Angle");
+                        emitter.Parameters[6] = ReadValueFloat("Offset");
+                        break;
+                    case EmitterDirection.Offset:
+                        emitter.Parameters[5] = ReadValueFloat("X");
+                        emitter.Parameters[6] = ReadValueFloat("Y");
+                        emitter.Parameters[7] = ReadValueFloat("Z");
+                        break;
+                    case EmitterDirection.Inaccurate:
+                        emitter.Parameters[5] = ReadValueFloat("BaseAngle");
+                        emitter.Parameters[6] = ReadValueFloat("Inaccuracy");
+                        emitter.Parameters[7] = ReadValueFloat("CenterBias");
+                        break;
+                }
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private void ReadEmitterSpeed(Emitter emitter)
+        {
+            emitter.Speed = MetaEnum.Parse<EmitterSpeed>(xml.LocalName);
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                switch (emitter.Speed)
+                {
+                    case EmitterSpeed.Uniform:
+                        emitter.Parameters[8] = ReadValueFloat("Speed");
+                        break;
+                    case EmitterSpeed.Stratified:
+                        emitter.Parameters[8] = ReadValueFloat("Speed1");
+                        emitter.Parameters[9] = ReadValueFloat("Speed2");
+                        break;
+                }
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private void ReadEvents()
+        {
+            if (!xml.IsStartElement("Events"))
+                return;
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                while (xml.IsStartElement())
+                    ReadEvent();
+
+                xml.ReadEndElement();
+            }
+        }
+
+        private void ReadEvent()
+        {
+            Event e = new Event((EventType)Enum.Parse(typeof(EventType), xml.LocalName));
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                while (xml.IsStartElement())
+                    e.Actions.Add(ReadEventAction());
+
+                xml.ReadEndElement();
+            }
+
+            particle.Events.Add(e);
+        }
+
+        private EventAction ReadEventAction()
+        {
+            EventAction action = new EventAction((EventActionType)Enum.Parse(typeof(EventActionType), xml.LocalName));
+            EventActionInfo info = eventActionInfoTable[(int)action.Type];
+
+            if (xml.IsEmptyElement)
+            {
+                xml.ReadStartElement();
+            }
+            else
+            {
+                xml.ReadStartElement();
+
+                for (int i = 0; xml.IsStartElement(); i++)
+                {
+                    if (i < info.OutCount)
+                    {
+                        action.Variables.Add(new VariableReference(xml.ReadElementContentAsString()));
+                        continue;
+                    }
+
+                    if (i >= info.Parameters.Length)
+                        throw new XmlException(string.Format("Too many arguments for action '{0}'", action.Type));
+
+                    switch (info.Parameters[i].Type)
+                    {
+                        case StorageType.Float:
+                        case StorageType.BlastFalloff:
+                            action.Parameters.Add(ReadValueFloat());
+                            continue;
+
+                        case StorageType.Color:
+                            action.Parameters.Add(ReadValueColor());
+                            continue;
+
+                        case StorageType.ActionIndex:
+                        case StorageType.Emitter:
+                        case StorageType.CoordFrame:
+                        case StorageType.CollisionOrient:
+                        case StorageType.Boolean:
+                        case StorageType.PingPongState:
+                        case StorageType.ImpactModifier:
+                        case StorageType.DamageType:
+                        case StorageType.Direction:
+                            action.Parameters.Add(ReadValueInt());
+                            continue;
+
+                        case StorageType.ImpulseSoundName:
+                        case StorageType.AmbientSoundName:
+                        case StorageType.ImpactName:
+                            action.Parameters.Add(ReadValueInstance());
+                            continue;
+                    }
+                }
+
+                xml.ReadEndElement();
+            }
+
+            return action;
+        }
+
+        private Value ReadValueInstance()
+        {
+            return new Value(ValueType.InstanceName, xml.ReadElementContentAsString());
+        }
+
+        private Value ReadValueInt()
+        {
+            Value value = null;
+            xml.ReadStartElement();
+
+            string text = xml.ReadElementContentAsString();
+            int i;
+
+            if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out i))
+                value = new Value(i);
+            else
+                value = new Value(ValueType.Variable, text.Trim());
+
+            xml.ReadEndElement();
+            return value;
+        }
+
+        private Value ReadValueFloat(string name)
+        {
+            if (xml.LocalName != name)
+                throw new XmlException(string.Format(CultureInfo.CurrentCulture, "Unexpected '{0}' element found at line {1}", xml.LocalName, 0));
+
+            return ReadValueFloat();
+        }
+
+        private Value ReadValueFloat()
+        {
+            if (xml.IsEmptyElement)
+            {
+                xml.Read();
+                return new Value(0.0f);
+            }
+
+            Value value = null;
+
+            xml.ReadStartElement();
+
+            if (xml.NodeType == XmlNodeType.Text)
+            {
+                string text = xml.ReadElementContentAsString();
+                float f;
+
+                if (float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out f))
+                    value = new Value(f);
+                else
+                    value = new Value(ValueType.Variable, text.Trim());
+            }
+            else if (xml.NodeType == XmlNodeType.Element)
+            {
+                string name = xml.LocalName;
+
+                if (name == "Random")
+                {
+                    float min = float.Parse(xml.GetAttribute("Min"), CultureInfo.InvariantCulture);
+                    float max = float.Parse(xml.GetAttribute("Max"), CultureInfo.InvariantCulture);
+                    value = new Value(ValueType.FloatRandom, min, max);
+                }
+                else if (name == "BellCurve")
+                {
+                    float mean = float.Parse(xml.GetAttribute("Mean"), CultureInfo.InvariantCulture);
+                    float stddev = float.Parse(xml.GetAttribute("StdDev"), CultureInfo.InvariantCulture);
+                    value = new Value(ValueType.FloatBellCurve, mean, stddev);
+                }
+                else
+                {
+                    throw new XmlException(string.Format(CultureInfo.CurrentCulture, "Unknown value type '{0}'", name));
+                }
+
+                xml.ReadStartElement();
+            }
+
+            xml.ReadEndElement();
+            return value;
+        }
+
+        private Value ReadValueColor()
+        {
+            Value value = null;
+
+            xml.ReadStartElement();
+
+            if (xml.NodeType == XmlNodeType.Text)
+            {
+                string text = xml.ReadElementContentAsString();
+                Color c;
+
+                if (Color.TryParse(text, out c))
+                    value = new Value(c);
+                else
+                    value = new Value(ValueType.Variable, text.Trim());
+            }
+            else if (xml.NodeType == XmlNodeType.Element)
+            {
+                string name = xml.LocalName;
+
+                if (name == "Random")
+                {
+                    Color min = Color.Parse(xml.GetAttribute("Min"));
+                    Color max = Color.Parse(xml.GetAttribute("Max"));
+                    value = new Value(ValueType.ColorRandom, min, max);
+                }
+                else if (name == "BellCurve")
+                {
+                    Color mean = Color.Parse(xml.GetAttribute("Mean"));
+                    Color stddev = Color.Parse(xml.GetAttribute("StdDev"));
+                    value = new Value(ValueType.ColorBellCurve, mean, stddev);
+                }
+                else
+                {
+                    throw new XmlException(string.Format(CultureInfo.CurrentCulture, "Unknown value type '{0}'", name));
+                }
+
+                xml.ReadStartElement();
+            }
+
+            xml.ReadEndElement();
+            return value;
+        }
+
+        private bool ReadFlag1()
+        {
+            ParticleFlags1 flag;
+
+            try
+            {
+                flag = (ParticleFlags1)Enum.Parse(typeof(ParticleFlags1), xml.LocalName);
+            }
+            catch
+            {
+                return false;
+            }
+
+            if (ReadFlagValue())
+                particle.Flags1 |= flag;
+
+            return true;
+        }
+
+        private bool ReadFlag2()
+        {
+            ParticleFlags2 flag;
+
+            try
+            {
+                flag = (ParticleFlags2)Enum.Parse(typeof(ParticleFlags2), xml.LocalName);
+            }
+            catch
+            {
+                return false;
+            }
+
+            if (ReadFlagValue())
+                particle.Flags2 |= flag;
+
+            return true;
+        }
+
+        private bool ReadFlagValue()
+        {
+            string text = xml.ReadElementContentAsString();
+
+            switch (text)
+            {
+                case "false":
+                    return false;
+                case "true":
+                    return true;
+                default:
+                    throw new FormatException(string.Format(CultureInfo.CurrentCulture, "Unknown value '{0}'", text));
+            }
+        }
+    }
+}
Index: /OniSplit/Xml/RawXmlExporter.cs
===================================================================
--- /OniSplit/Xml/RawXmlExporter.cs	(revision 1114)
+++ /OniSplit/Xml/RawXmlExporter.cs	(revision 1114)
@@ -0,0 +1,257 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    internal class RawXmlExporter : IMetaTypeVisitor
+    {
+        private readonly BinaryReader reader;
+        private readonly XmlWriter xml;
+        private Stack<int> startOffsetStack;
+
+        public RawXmlExporter(BinaryReader reader, XmlWriter xml)
+        {
+            this.reader = reader;
+            this.xml = xml;
+
+            BeginStruct(reader.Position);
+        }
+
+        protected BinaryReader Reader => reader;
+
+        protected XmlWriter Xml => xml;
+
+        protected void BeginStruct(int startOffset)
+        {
+            startOffsetStack = new Stack<int>();
+            startOffsetStack.Push(startOffset);
+        }
+
+        #region IMetaTypeVisitor Members
+
+        void IMetaTypeVisitor.VisitEnum(MetaEnum type)
+        {
+            type.BinaryToXml(reader, xml);
+        }
+
+        void IMetaTypeVisitor.VisitByte(MetaByte type)
+        {
+            xml.WriteValue(reader.ReadByte());
+        }
+
+        void IMetaTypeVisitor.VisitInt16(MetaInt16 type)
+        {
+            xml.WriteValue(reader.ReadInt16());
+        }
+
+        void IMetaTypeVisitor.VisitUInt16(MetaUInt16 type)
+        {
+            xml.WriteValue(reader.ReadUInt16());
+        }
+
+        void IMetaTypeVisitor.VisitInt32(MetaInt32 type)
+        {
+            xml.WriteValue(reader.ReadInt32());
+        }
+
+        void IMetaTypeVisitor.VisitUInt32(MetaUInt32 type)
+        {
+            xml.WriteValue(reader.ReadUInt32());
+        }
+
+        void IMetaTypeVisitor.VisitInt64(MetaInt64 type)
+        {
+            xml.WriteValue(reader.ReadInt64());
+        }
+
+        void IMetaTypeVisitor.VisitUInt64(MetaUInt64 type)
+        {
+            xml.WriteValue(XmlConvert.ToString(reader.ReadUInt64()));
+        }
+
+        void IMetaTypeVisitor.VisitFloat(MetaFloat type)
+        {
+            xml.WriteValue(reader.ReadSingle());
+        }
+
+        void IMetaTypeVisitor.VisitVector2(MetaVector2 type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(2));
+        }
+
+        void IMetaTypeVisitor.VisitVector3(MetaVector3 type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(3));
+        }
+
+        void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(12));
+        }
+
+        void IMetaTypeVisitor.VisitPlane(MetaPlane type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(4));
+        }
+
+        void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type)
+        {
+            xml.WriteFloatArray(reader.ReadSingleArray(4));
+        }
+
+        void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type)
+        {
+            WriteFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type)
+        {
+            WriteFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitColor(MetaColor type)
+        {
+            Color color = reader.ReadColor();
+
+            if (color.A == 255)
+                xml.WriteValue(string.Format("{0} {1} {2}", color.R, color.G, color.B));
+            else
+                xml.WriteValue(string.Format("{0} {1} {2} {3}", color.R, color.G, color.B, color.A));
+        }
+
+        void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type)
+        {
+            xml.WriteValue(reader.ReadInt32());
+        }
+
+        void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type)
+        {
+            xml.WriteValue(reader.ReadInt32());
+        }
+
+        void IMetaTypeVisitor.VisitPointer(MetaPointer type)
+        {
+            VisitLink(type);
+        }
+
+        void IMetaTypeVisitor.VisitString(MetaString type)
+        {
+            string s = reader.ReadString(type.Count);
+            s = CleanupInvalidCharacters(s);
+            xml.WriteValue(s);
+        }
+
+        void IMetaTypeVisitor.VisitPadding(MetaPadding type)
+        {
+            reader.Position += type.Count;
+        }
+
+        void IMetaTypeVisitor.VisitStruct(MetaStruct type)
+        {
+            WriteFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitArray(MetaArray type)
+        {
+            WriteArray(type.ElementType, type.Count);
+        }
+
+        void IMetaTypeVisitor.VisitVarArray(MetaVarArray type)
+        {
+            int count;
+
+            if (type.CountField.Type == MetaType.Int16)
+                count = reader.ReadInt16();
+            else
+                count = reader.ReadInt32();
+
+            WriteArray(type.ElementType, count);
+        }
+
+        #endregion
+
+        protected virtual void VisitLink(MetaPointer link)
+        {
+            throw new NotSupportedException();
+        }
+
+        private void WriteFields(IEnumerable<Field> fields)
+        {
+            foreach (Field field in fields)
+            {
+                if (field.Type is MetaPadding)
+                {
+                    field.Type.Accept(this);
+                    continue;
+                }
+
+                string name = field.Name;
+
+                if (string.IsNullOrEmpty(name))
+                {
+                    int fieldOffset = reader.Position - startOffsetStack.Peek();
+                    name = string.Format("Offset_{0:X4}", fieldOffset);
+                }
+
+                xml.WriteStartElement(name);
+                field.Type.Accept(this);
+                xml.WriteEndElement();
+            }
+        }
+
+        private void WriteArray(MetaType elementType, int count)
+        {
+            bool simpleArray = elementType.IsBlittable;
+            simpleArray = false;
+
+            for (int i = 0; i < count; i++)
+            {
+                startOffsetStack.Push(reader.Position);
+
+                if (!simpleArray)
+                    xml.WriteStartElement(elementType.Name);
+
+                elementType.Accept(this);
+
+                if (!simpleArray)
+                    xml.WriteEndElement();
+                else if (i != count - 1)
+                    xml.WriteWhitespace(" \n");
+
+                startOffsetStack.Pop();
+            }
+        }
+
+        private static string CleanupInvalidCharacters(string s)
+        {
+            char[] c = null;
+
+            for (int i = 0; i < s.Length; i++)
+            {
+                if (s[i] >= ' ')
+                    continue;
+
+                switch (s[i])
+                {
+                    case '\t':
+                    case '\n':
+                    case '\r':
+                        continue;
+                }
+
+                if (c == null)
+                    c = s.ToCharArray();
+
+                c[i] = ' ';
+            }
+
+            if (c == null)
+                return s;
+
+            return new string(c);
+        }
+    }
+}
Index: /OniSplit/Xml/RawXmlImporter.cs
===================================================================
--- /OniSplit/Xml/RawXmlImporter.cs	(revision 1114)
+++ /OniSplit/Xml/RawXmlImporter.cs	(revision 1114)
@@ -0,0 +1,255 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    internal class RawXmlImporter : IMetaTypeVisitor
+    {
+        private static readonly Func<string, float> floatConverter = XmlConvert.ToSingle;
+        private static readonly Func<string, byte> byteConverter = XmlConvert.ToByte;
+        private readonly XmlReader xml;
+        private readonly BinaryWriter writer;
+        private Stack<int> startOffsetStack;
+
+        public RawXmlImporter(XmlReader xml, BinaryWriter writer)
+        {
+            this.xml = xml;
+            this.writer = writer;
+        }
+
+        protected XmlReader Xml => xml;
+
+        protected BinaryWriter Writer => writer;
+
+        protected void BeginStruct(int startPosition)
+        {
+            startOffsetStack = new Stack<int>();
+            startOffsetStack.Push(startPosition);
+        }
+
+        #region IMetaTypeVisitor Members
+
+        void IMetaTypeVisitor.VisitEnum(MetaEnum type)
+        {
+            type.XmlToBinary(xml, writer);
+        }
+
+        void IMetaTypeVisitor.VisitByte(MetaByte type)
+        {
+            writer.Write(XmlConvert.ToByte(xml.ReadElementContentAsString()));
+        }
+
+        void IMetaTypeVisitor.VisitInt16(MetaInt16 type)
+        {
+            writer.Write(XmlConvert.ToInt16(xml.ReadElementContentAsString()));
+        }
+
+        void IMetaTypeVisitor.VisitUInt16(MetaUInt16 type)
+        {
+            writer.Write(XmlConvert.ToUInt16(xml.ReadElementContentAsString()));
+        }
+
+        void IMetaTypeVisitor.VisitInt32(MetaInt32 type)
+        {
+            writer.Write(xml.ReadElementContentAsInt());
+        }
+
+        void IMetaTypeVisitor.VisitUInt32(MetaUInt32 type)
+        {
+            writer.Write(XmlConvert.ToUInt32(xml.ReadElementContentAsString()));
+        }
+
+        void IMetaTypeVisitor.VisitInt64(MetaInt64 type)
+        {
+            writer.Write(xml.ReadElementContentAsLong());
+        }
+
+        void IMetaTypeVisitor.VisitUInt64(MetaUInt64 type)
+        {
+            writer.Write(XmlConvert.ToUInt64(xml.ReadElementContentAsString()));
+        }
+
+        void IMetaTypeVisitor.VisitFloat(MetaFloat type)
+        {
+            writer.Write(xml.ReadElementContentAsFloat());
+        }
+
+        void IMetaTypeVisitor.VisitColor(MetaColor type)
+        {
+            byte[] values = xml.ReadElementContentAsArray(byteConverter);
+
+            if (values.Length > 3)
+                writer.Write(new Color(values[0], values[1], values[2], values[3]));
+            else
+                writer.Write(new Color(values[0], values[1], values[2]));
+        }
+
+        void IMetaTypeVisitor.VisitVector2(MetaVector2 type)
+        {
+            writer.Write(xml.ReadElementContentAsArray(floatConverter, 2));
+        }
+
+        void IMetaTypeVisitor.VisitVector3(MetaVector3 type)
+        {
+            writer.Write(xml.ReadElementContentAsArray(floatConverter, 3));
+        }
+
+        void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type)
+        {
+            writer.WriteMatrix4x3(xml.ReadElementContentAsMatrix43());
+        }
+
+        void IMetaTypeVisitor.VisitPlane(MetaPlane type)
+        {
+            writer.Write(xml.ReadElementContentAsArray(floatConverter, 4));
+        }
+
+        void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type)
+        {
+            writer.Write(xml.ReadElementContentAsQuaternion());
+        }
+
+        void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type)
+        {
+            ReadFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type)
+        {
+            ReadFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type)
+        {
+            throw new NotImplementedException();
+        }
+
+        void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type)
+        {
+            throw new NotImplementedException();
+        }
+
+        void IMetaTypeVisitor.VisitString(MetaString type)
+        {
+            writer.Write(xml.ReadElementContentAsString(), type.Count);
+        }
+
+        void IMetaTypeVisitor.VisitPadding(MetaPadding type)
+        {
+            writer.Write(type.FillByte, type.Count);
+        }
+
+        void IMetaTypeVisitor.VisitPointer(MetaPointer type)
+        {
+            throw new NotImplementedException();
+        }
+
+        void IMetaTypeVisitor.VisitStruct(MetaStruct type)
+        {
+            ReadFields(type.Fields);
+        }
+
+        void IMetaTypeVisitor.VisitArray(MetaArray type)
+        {
+            int count = ReadArray(type.ElementType, type.Count);
+
+            if (count < type.Count)
+                writer.Skip((type.Count - count) * type.ElementType.Size);
+        }
+
+        void IMetaTypeVisitor.VisitVarArray(MetaVarArray type)
+        {
+            int countFieldPosition = writer.Position;
+            int count;
+
+            if (type.CountField.Type == MetaType.Int16)
+            {
+                writer.WriteInt16(0);
+                count = ReadArray(type.ElementType, UInt16.MaxValue);
+            }
+            else
+            {
+                writer.Write(0);
+                count = ReadArray(type.ElementType, Int32.MaxValue);
+            }
+
+            int position = writer.Position;
+            writer.Position = countFieldPosition;
+
+            if (type.CountField.Type == MetaType.Int16)
+                writer.WriteUInt16(count);
+            else
+                writer.Write(count);
+
+            writer.Position = position;
+        }
+
+        #endregion
+
+        private void ReadFields(IEnumerable<Field> fields)
+        {
+            xml.ReadStartElement();
+            xml.MoveToContent();
+
+            foreach (Field field in fields)
+            {
+                try
+                {
+                    field.Type.Accept(this);
+                }
+                catch (Exception ex)
+                {
+                    var lineInfo = xml as IXmlLineInfo;
+                    int line = lineInfo != null ? lineInfo.LineNumber : 0;
+                    throw new InvalidOperationException(string.Format("Cannot read field '{0}' (line {1})", field.Name, line), ex);
+                }
+            }
+
+            xml.ReadEndElement();
+        }
+
+        protected void ReadStruct(MetaStruct s)
+        {
+            foreach (Field field in s.Fields)
+            {
+                try
+                {
+                    field.Type.Accept(this);
+                }
+                catch (Exception ex)
+                {
+                    throw new InvalidOperationException(string.Format("Cannot read field '{0}'", field.Name), ex);
+                }
+            }
+        }
+
+        private int ReadArray(MetaType elementType, int maxCount)
+        {
+            if (xml.IsEmptyElement)
+            {
+                xml.Read();
+                return 0;
+            }
+
+            xml.ReadStartElement();
+            xml.MoveToContent();
+
+            var localName = xml.LocalName;
+            int count = 0;
+
+            for (; count < maxCount && xml.IsStartElement(localName); count++)
+            {
+                startOffsetStack.Push(writer.Position);
+                elementType.Accept(this);
+                startOffsetStack.Pop();
+            }
+
+            xml.ReadEndElement();
+
+            return count;
+        }
+    }
+}
Index: /OniSplit/Xml/TmbdXmlExporter.cs
===================================================================
--- /OniSplit/Xml/TmbdXmlExporter.cs	(revision 1114)
+++ /OniSplit/Xml/TmbdXmlExporter.cs	(revision 1114)
@@ -0,0 +1,62 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+
+namespace Oni.Xml
+{
+    internal class TmbdXmlExporter : RawXmlExporter
+    {
+        private TmbdXmlExporter(BinaryReader reader, XmlWriter writer)
+            : base(reader, writer)
+        {
+        }
+
+        public static void Export(BinaryReader reader, XmlWriter writer)
+        {
+            var exporter = new TmbdXmlExporter(reader, writer);
+            exporter.Export();
+        }
+
+        private void Export()
+        {
+            int size = Reader.ReadInt32();
+            int version = Reader.ReadInt32();
+            int count = Reader.ReadInt32();
+
+            var materials = new Dictionary<string, List<string>>(count);
+
+            for (int i = 0; i < count; i++)
+            {
+                var materialName = Reader.ReadString(32);
+                var textureName = Reader.ReadString(32);
+
+                List<string> textures;
+
+                if (!materials.TryGetValue(materialName, out textures))
+                {
+                    textures = new List<string>();
+                    materials.Add(materialName, textures);
+                }
+
+                textures.Add(textureName);
+            }
+
+            Xml.WriteStartElement("TextureMaterials");
+
+            foreach (var pair in materials)
+            {
+                Xml.WriteStartElement("Material");
+                Xml.WriteAttributeString("Name", pair.Key);
+
+                foreach (var textureName in pair.Value)
+                    Xml.WriteElementString("Texture", textureName);
+
+                Xml.WriteEndElement();
+            }
+
+            Xml.WriteEndElement();
+        }
+    }
+}
Index: /OniSplit/Xml/TmbdXmlImporter.cs
===================================================================
--- /OniSplit/Xml/TmbdXmlImporter.cs	(revision 1114)
+++ /OniSplit/Xml/TmbdXmlImporter.cs	(revision 1114)
@@ -0,0 +1,68 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Xml;
+
+namespace Oni.Xml
+{
+    internal class TmbdXmlImporter : RawXmlImporter
+    {
+        private TmbdXmlImporter(XmlReader reader, BinaryWriter writer)
+            : base(reader, writer)
+        {
+        }
+
+        public static void Import(XmlReader reader, BinaryWriter writer)
+        {
+            var importer = new TmbdXmlImporter(reader, writer);
+            importer.Import();
+        }
+
+        private void Import()
+        {
+            Writer.Write(1);
+            int countPosition = Writer.Position;
+            Writer.Write(0);
+
+            var materials = new Dictionary<string, List<string>>();
+
+            while (Xml.IsStartElement("Material"))
+            {
+                string materialName = Xml.GetAttribute("Name");
+
+                Xml.ReadStartElement();
+
+                while (Xml.IsStartElement("Texture"))
+                {
+                    string textureName = Xml.ReadElementContentAsString();
+
+                    List<string> textures;
+
+                    if (!materials.TryGetValue(materialName, out textures))
+                    {
+                        textures = new List<string>();
+                        materials.Add(materialName, textures);
+                    }
+
+                    textures.Add(textureName);
+                }
+
+                Xml.ReadEndElement();
+            }
+
+            int count = 0;
+
+            foreach (var pair in materials)
+            {
+                foreach (string textureName in pair.Value)
+                {
+                    Writer.Write(pair.Key, 32);
+                    Writer.Write(textureName, 32);
+
+                    count++;
+                }
+            }
+
+            Writer.WriteAt(countPosition, count);
+        }
+    }
+}
Index: /OniSplit/Xml/XmlExporter.cs
===================================================================
--- /OniSplit/Xml/XmlExporter.cs	(revision 1114)
+++ /OniSplit/Xml/XmlExporter.cs	(revision 1114)
@@ -0,0 +1,405 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Xml;
+using Oni.Collections;
+using Oni.Metadata;
+using Oni.Sound;
+
+namespace Oni.Xml
+{
+    internal sealed class XmlExporter : Exporter
+    {
+        private bool noAnimation;
+        private bool recursive;
+        private Totoro.Body animBody;
+        private bool mergeAnimations;
+        private Dae.Node animBodyNode;
+        private Totoro.Animation mergedAnim;
+        private string animDaeFileName;
+        private readonly Dictionary<InstanceDescriptor, string> externalChildren = new Dictionary<InstanceDescriptor, string>();
+        private readonly Set<InstanceDescriptor> queued = new Set<InstanceDescriptor>();
+        private readonly Queue<InstanceDescriptor> exportQueue = new Queue<InstanceDescriptor>();
+        private InstanceDescriptor mainDescriptor;
+        private string baseFileName;
+        private XmlWriter xml;
+
+        public XmlExporter(InstanceFileManager fileManager, string outputDirPath)
+            : base(fileManager, outputDirPath)
+        {
+        }
+
+        public bool NoAnimation
+        {
+            get { return noAnimation; }
+            set { noAnimation = value; }
+        }
+
+        public bool Recursive
+        {
+            get { return recursive; }
+            set { recursive = value; }
+        }
+
+        public Totoro.Body AnimationBody
+        {
+            get { return animBody; }
+            set
+            {
+                animBody = value;
+                animBodyNode = null;
+            }
+        }
+
+        public bool MergeAnimations
+        {
+            get { return mergeAnimations; }
+            set { mergeAnimations = value; }
+        }
+
+        protected override void ExportInstance(InstanceDescriptor descriptor)
+        {
+            exportQueue.Enqueue(descriptor);
+
+            mainDescriptor = descriptor;
+
+            string filePath = baseFileName = CreateFileName(descriptor, ".xml");
+            baseFileName = Path.GetFileNameWithoutExtension(filePath);
+
+            if (recursive && animBody == null && descriptor.Template.Tag == TemplateTag.ONCC)
+                animBody = Totoro.BodyDatReader.Read(descriptor);
+
+            using (xml = CreateXmlWriter(filePath))
+                ExportDescriptors(xml);
+        }
+
+        private void ExportChild(InstanceDescriptor descriptor)
+        {
+            if (descriptor.Template.Tag == TemplateTag.TRCM && mainDescriptor.Template.Tag == TemplateTag.TRBS)
+            {
+                xml.WriteValue(WriteBody(descriptor));
+                return;
+            }
+
+            if (descriptor.Template.Tag == TemplateTag.M3GM)
+            {
+                if (!descriptor.IsPlaceholder)
+                {
+                    xml.WriteValue(WriteGeometry(descriptor));
+                    return;
+                }
+
+                if (recursive)
+                {
+                    var m3gmFile = InstanceFileManager.FindInstance(descriptor.FullName, descriptor.File);
+
+                    if (m3gmFile != null && m3gmFile.Descriptors[0].Template.Tag == TemplateTag.M3GM && m3gmFile.Descriptors[0].Name == descriptor.Name)
+                    {
+                        xml.WriteValue(WriteGeometry(m3gmFile.Descriptors[0]));
+                        return;
+                    }
+                }
+            }
+
+            if (!recursive || !descriptor.HasName)
+            {
+                if (descriptor.HasName)
+                {
+                    xml.WriteValue(descriptor.FullName);
+                }
+                else
+                {
+                    xml.WriteValue(string.Format(CultureInfo.InvariantCulture, "#{0}", descriptor.Index));
+
+                    if (queued.Add(descriptor))
+                        exportQueue.Enqueue(descriptor);
+                }
+
+                return;
+            }
+
+            var childFile = InstanceFileManager.FindInstance(descriptor.FullName, descriptor.File);
+
+            if (childFile == null || childFile == mainDescriptor.File)
+            {
+                xml.WriteValue(descriptor.FullName);
+                return;
+            }
+
+            string fileName;
+
+            if (!externalChildren.TryGetValue(descriptor, out fileName))
+            {
+                var exporter = new XmlExporter(InstanceFileManager, OutputDirPath)
+                {
+                    recursive = recursive,
+                    animBody = animBody,
+                    mergeAnimations = mergeAnimations
+                };
+
+                exporter.ExportFiles(new[] { childFile.FilePath });
+
+                fileName = Path.GetFileName(CreateFileName(descriptor, ".xml"));
+
+                externalChildren.Add(descriptor, fileName);
+            }
+
+            xml.WriteValue(fileName);
+        }
+
+        private static XmlWriter CreateXmlWriter(string filePath)
+        {
+            var settings = new XmlWriterSettings
+            {
+                CloseOutput = true,
+                Indent = true,
+                IndentChars = "    "
+            };
+
+            var stream = File.Create(filePath);
+            var writer = XmlWriter.Create(stream, settings);
+
+            try
+            {
+                writer.WriteStartElement("Oni");
+            }
+            catch
+            {
+#if NETCORE
+                writer.Dispose();
+#else
+                writer.Close();
+#endif
+                throw;
+            }
+
+            return writer;
+        }
+
+        private void ExportDescriptors(XmlWriter writer)
+        {
+            while (exportQueue.Count > 0)
+            {
+                var descriptor = exportQueue.Dequeue();
+
+                if (descriptor.IsPlaceholder || (descriptor.HasName && descriptor != mainDescriptor))
+                    continue;
+
+                switch (descriptor.Template.Tag)
+                {
+                    case TemplateTag.TRAM:
+                        WriteAnimation(descriptor);
+                        break;
+
+                    case TemplateTag.BINA:
+                        WriteBinaryObject(descriptor);
+                        break;
+
+                    case TemplateTag.TXMP:
+                        //
+                        // Only export TXMP instances that have a name, the others
+                        // are animation frames and they're exported as part of named textures
+                        //
+
+                        if (descriptor.HasName)
+                            Oni.Motoko.TextureXmlExporter.Export(descriptor, writer, OutputDirPath, baseFileName);
+
+                        break;
+
+                    case TemplateTag.TXAN:
+                        //
+                        // Do nothing: TXAN instances are exported as part of TXMP instances
+                        //
+                        break;
+
+                    case TemplateTag.OSBD:
+                        WriteBinarySound(descriptor);
+                        break;
+
+                    default:
+                        GenericXmlWriter.Write(xml, ExportChild, descriptor);
+                        break;
+                }
+            }
+        }
+
+        private void WriteAnimation(InstanceDescriptor tram)
+        {
+            var anim = Totoro.AnimationDatReader.Read(tram);
+
+            if (animBody == null)
+            {
+                Totoro.AnimationXmlWriter.Write(anim, xml, null, 0, 0);
+            }
+            else
+            {
+                if (animBodyNode == null)
+                {
+                    var textureWriter = new Motoko.TextureDaeWriter(OutputDirPath);
+                    var geometryWriter = new Motoko.GeometryDaeWriter(textureWriter);
+                    var bodyWriter = new Totoro.BodyDaeWriter(geometryWriter);
+
+                    animBodyNode = bodyWriter.Write(animBody, false, null);
+                }
+
+                if (mergeAnimations)
+                {
+                    if (mergedAnim == null)
+                    {
+                        mergedAnim = new Totoro.Animation();
+                        animDaeFileName = tram.FullName + ".dae";
+                    }
+
+                    var startFrame = mergedAnim.Heights.Count;
+                    Totoro.AnimationDaeWriter.AppendFrames(mergedAnim, anim);
+                    var endFrame = mergedAnim.Heights.Count;
+
+                    Totoro.AnimationXmlWriter.Write(anim, xml, animDaeFileName, startFrame, endFrame);
+                }
+                else
+                {
+                    var fileName = tram.FullName + ".dae";
+
+                    Totoro.AnimationDaeWriter.Write(animBodyNode, anim);
+
+                    Dae.Writer.WriteFile(Path.Combine(OutputDirPath, fileName), new Dae.Scene
+                    {
+                        Nodes = { animBodyNode }
+                    });
+
+                    Totoro.AnimationXmlWriter.Write(anim, xml, fileName, 0, 0);
+                }
+            }
+        }
+
+        protected override void Flush()
+        {
+            if (mergedAnim != null)
+            {
+                Totoro.AnimationDaeWriter.Write(animBodyNode, mergedAnim);
+
+                Dae.Writer.WriteFile(Path.Combine(OutputDirPath, animDaeFileName), new Dae.Scene
+                {
+                    Nodes = { animBodyNode }
+                });
+
+                mergedAnim = null;
+            }
+        }
+
+        private string WriteBody(InstanceDescriptor descriptor)
+        {
+            string fileName;
+
+            if (!externalChildren.TryGetValue(descriptor, out fileName))
+            {
+                var body = Totoro.BodyDatReader.Read(descriptor);
+
+                var textureWriter = new Motoko.TextureDaeWriter(OutputDirPath);
+                var geometryWriter = new Motoko.GeometryDaeWriter(textureWriter);
+                var bodyWriter = new Totoro.BodyDaeWriter(geometryWriter);
+
+                var pelvis = bodyWriter.Write(body, noAnimation, null);
+
+                fileName = string.Format("{0}_TRCM{1}.dae", mainDescriptor.FullName, descriptor.Index);
+
+                Dae.Writer.WriteFile(Path.Combine(OutputDirPath, fileName), new Dae.Scene
+                {
+                    Nodes = { pelvis }
+                });
+
+                externalChildren.Add(descriptor, fileName);
+            }
+
+            return fileName;
+        }
+
+        private string WriteGeometry(InstanceDescriptor descriptor)
+        {
+            string fileName;
+
+            if (!externalChildren.TryGetValue(descriptor, out fileName))
+            {
+                var geometry = Motoko.GeometryDatReader.Read(descriptor);
+
+                var textureWriter = new Motoko.TextureDaeWriter(OutputDirPath);
+                var geometryWriter = new Motoko.GeometryDaeWriter(textureWriter);
+
+                if (descriptor.HasName)
+                    fileName = descriptor.FullName + ".dae";
+                else
+                    fileName = string.Format("{0}_{1}.dae", mainDescriptor.Name, descriptor.Index);
+
+                var node = geometryWriter.WriteNode(geometry, geometry.Name);
+
+                Dae.Writer.WriteFile(Path.Combine(OutputDirPath, fileName), new Dae.Scene
+                {
+                    Nodes = { node }
+                });
+
+                externalChildren.Add(descriptor, fileName);
+            }
+
+            return fileName;
+        }
+
+        private void WriteBinarySound(InstanceDescriptor descriptor)
+        {
+            int dataSize, dataOffset;
+
+            using (var reader = descriptor.OpenRead())
+            {
+                dataSize = reader.ReadInt32();
+                dataOffset = reader.ReadInt32();
+            }
+
+            using (var rawReader = descriptor.GetRawReader(dataOffset))
+            {
+                OsbdXmlExporter.Export(rawReader, xml);
+            }
+        }
+
+        private void WriteBinaryObject(InstanceDescriptor descriptor)
+        {
+            int dataSize, dataOffset;
+
+            using (var reader = descriptor.OpenRead())
+            {
+                dataSize = reader.ReadInt32();
+                dataOffset = reader.ReadInt32();
+            }
+
+            using (var rawReader = descriptor.GetRawReader(dataOffset))
+            {
+                var tag = (BinaryTag)rawReader.ReadInt32();
+
+                switch (tag)
+                {
+                    case BinaryTag.OBJC:
+                        ObjcXmlExporter.Export(rawReader, xml);
+                        break;
+
+                    case BinaryTag.PAR3:
+                        ParticleXmlExporter.Export(descriptor.FullName.Substring(8), rawReader, xml);
+                        break;
+
+                    case BinaryTag.TMBD:
+                        TmbdXmlExporter.Export(rawReader, xml);
+                        break;
+
+                    case BinaryTag.ONIE:
+                        OnieXmlExporter.Export(rawReader, xml);
+                        break;
+
+                    case BinaryTag.SABD:
+                        SabdXmlExporter.Export(rawReader, xml);
+                        break;
+
+                    default:
+                        throw new NotSupportedException(string.Format("Unsupported BINA type '{0}'", Utils.TagToString((int)tag)));
+                }
+            }
+        }
+    }
+}
Index: /OniSplit/Xml/XmlImporter.cs
===================================================================
--- /OniSplit/Xml/XmlImporter.cs	(revision 1114)
+++ /OniSplit/Xml/XmlImporter.cs	(revision 1114)
@@ -0,0 +1,770 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Xml;
+using Oni.Imaging;
+using Oni.Metadata;
+using Oni.Sound;
+
+namespace Oni.Xml
+{
+    internal class XmlImporter : Importer
+    {
+        private static readonly Func<string, float> floatConverter = XmlConvert.ToSingle;
+        private static readonly Func<string, byte> byteConverter = XmlConvert.ToByte;
+        protected XmlReader xml;
+        private readonly string[] args;
+        private string baseDir;
+        private string filePath;
+        private bool firstInstance;
+        private Dictionary<string, ImporterDescriptor> localRefs;
+        private Dictionary<string, ImporterDescriptor> externalRefs;
+        private ImporterDescriptor currentDescriptor;
+        private BinaryWriter currentWriter;
+
+        #region protected struct RawArray
+
+        protected struct RawArray
+        {
+            private int offset;
+            private int count;
+
+            public RawArray(int offset, int count)
+            {
+                this.offset = offset;
+                this.count = count;
+            }
+
+            public int Offset => offset;
+            public int Count => count;
+        }
+
+        #endregion
+
+        public XmlImporter(string[] args)
+        {
+            this.args = args;
+        }
+
+        public override void Import(string filePath, string outputDirPath)
+        {
+            this.filePath = filePath;
+
+            BeginImport();
+
+            using (xml = CreateXmlReader(filePath))
+            {
+                while (xml.IsStartElement())
+                {
+                    switch (xml.LocalName)
+                    {
+                        case "Objects":
+                            ReadObjects();
+                            break;
+
+                        case "Texture":
+                            ReadTexture();
+                            break;
+
+                        case "ImpactEffects":
+                            ReadImpactEffects();
+                            break;
+
+                        case "SoundAnimation":
+                            ReadSoundAnimation();
+                            break;
+
+                        case "TextureMaterials":
+                            ReadTextureMaterials();
+                            break;
+
+                        case "Particle":
+                            ReadParticle();
+                            break;
+
+                        case "AmbientSound":
+                        case "ImpulseSound":
+                        case "SoundGroup":
+                            ReadSoundData();
+                            break;
+
+                        case "Animation":
+                            ReadAnimation();
+                            break;
+
+                        default:
+                            ReadInstance();
+                            break;
+                    }
+                }
+            }
+
+            Write(outputDirPath);
+        }
+
+        public override void BeginImport()
+        {
+            base.BeginImport();
+
+            baseDir = Path.GetDirectoryName(filePath);
+            localRefs = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);
+            externalRefs = new Dictionary<string, ImporterDescriptor>(StringComparer.Ordinal);
+            firstInstance = true;
+        }
+
+        private static XmlReader CreateXmlReader(string filePath)
+        {
+            var settings = new XmlReaderSettings()
+            {
+                CloseInput = true,
+                IgnoreWhitespace = true,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true
+            };
+
+            var xml = XmlReader.Create(filePath, settings);
+
+            try
+            {
+                if (!xml.Read())
+                    throw new InvalidDataException("Not an Oni XML file");
+
+                xml.MoveToContent();
+
+                if (!xml.IsStartElement("Oni"))
+                    throw new InvalidDataException("Not an Oni XML file");
+
+                if (xml.IsEmptyElement)
+                    throw new InvalidDataException("No instances found");
+
+                xml.ReadStartElement();
+                xml.MoveToContent();
+            }
+            catch
+            {
+#if NETCORE
+                xml.Dispose();
+#else
+                xml.Close();
+#endif
+                throw;
+            }
+
+            return xml;
+        }
+
+        private void ReadInstance()
+        {
+            var xmlid = xml.GetAttribute("id");
+            var tagName = xml.GetAttribute("type");
+
+            if (tagName == null)
+                tagName = xml.LocalName;
+
+            var tag = (TemplateTag)Enum.Parse(typeof(TemplateTag), tagName);
+            var metadata = InstanceMetadata.GetMetadata(InstanceFileHeader.OniPCTemplateChecksum);
+            var template = metadata.GetTemplate(tag);
+
+            string name = null;
+
+            if (firstInstance)
+            {
+                name = Path.GetFileNameWithoutExtension(filePath);
+
+                if (!name.StartsWith(tagName, StringComparison.Ordinal))
+                    name = tagName + name;
+
+                firstInstance = false;
+            }
+
+            var writer = BeginXmlInstance(tag, name, xmlid);
+            template.Type.Accept(new XmlToBinaryVisitor(this, xml, writer));
+            EndXmlInstance();
+        }
+
+        private void ReadAnimation()
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            var writer = BeginXmlInstance(TemplateTag.TRAM, name, "0");
+            var animation = Totoro.AnimationXmlReader.Read(xml, Path.GetDirectoryName(filePath));
+            Totoro.AnimationDatWriter.Write(animation, this, writer);
+            EndXmlInstance();
+        }
+
+        private void ReadParticle()
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (!name.StartsWith("BINA3RAP", StringComparison.Ordinal))
+                name = "BINA3RAP" + name;
+
+            xml.ReadStartElement();
+
+            int rawDataOffset = RawWriter.Align32();
+
+            RawWriter.Write((int)BinaryTag.PAR3);
+            RawWriter.Write(0);
+
+            ParticleXmlImporter.Import(xml, RawWriter);
+
+            int rawDataLength = RawWriter.Position - rawDataOffset;
+            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);
+
+            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
+            writer.Write(rawDataLength);
+            writer.Write(rawDataOffset);
+            EndXmlInstance();
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadSoundData()
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            int rawDataOffset = RawWriter.Align32();
+
+            var osbdImporter = new OsbdXmlImporter(xml, RawWriter);
+            osbdImporter.Import();
+
+            int rawDataLength = RawWriter.Position - rawDataOffset;
+            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);
+
+            var writer = BeginXmlInstance(TemplateTag.OSBD, name, "0");
+            writer.Write(rawDataLength);
+            writer.Write(rawDataOffset);
+            EndXmlInstance();
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadObjects()
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (!name.StartsWith("BINACJBO", StringComparison.Ordinal))
+                name = "BINACJBO" + name;
+
+            xml.ReadStartElement();
+
+            int rawDataOffset = RawWriter.Align32();
+
+            RawWriter.Write((int)BinaryTag.OBJC);
+            RawWriter.Write(0);
+
+            ObjcXmlImporter.Import(xml, RawWriter);
+
+            int rawDataLength = RawWriter.Position - rawDataOffset;
+            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);
+
+            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
+            writer.Write(rawDataLength);
+            writer.Write(rawDataOffset);
+            EndXmlInstance();
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadTextureMaterials()
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (!name.StartsWith("BINADBMT", StringComparison.Ordinal))
+                name = "BINADBMT" + name;
+
+            xml.ReadStartElement();
+
+            int rawDataOffset = RawWriter.Align32();
+
+            RawWriter.Write((int)BinaryTag.TMBD);
+            RawWriter.Write(0);
+
+            TmbdXmlImporter.Import(xml, RawWriter);
+
+            int rawDataLength = RawWriter.Position - rawDataOffset;
+            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);
+
+            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
+            writer.Write(rawDataLength);
+            writer.Write(rawDataOffset);
+            EndXmlInstance();
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadImpactEffects()
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (!name.StartsWith("BINAEINO", StringComparison.Ordinal))
+                name = "BINAEINO" + name;
+
+            xml.ReadStartElement();
+
+            int rawDataOffset = RawWriter.Align32();
+
+            RawWriter.Write((int)BinaryTag.ONIE);
+            RawWriter.Write(0);
+
+            OnieXmlImporter.Import(xml, RawWriter);
+
+            int rawDataLength = RawWriter.Position - rawDataOffset;
+            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);
+
+            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
+            writer.Write(rawDataLength);
+            writer.Write(rawDataOffset);
+            EndXmlInstance();
+
+            xml.ReadEndElement();
+        }
+
+        private void ReadSoundAnimation()
+        {
+            var name = Path.GetFileNameWithoutExtension(filePath);
+
+            if (!name.StartsWith("BINADBAS", StringComparison.Ordinal))
+                name = "BINADBAS" + name;
+
+            int rawDataOffset = RawWriter.Align32();
+
+            RawWriter.Write((int)BinaryTag.SABD);
+            RawWriter.Write(0);
+
+            SabdXmlImporter.Import(xml, RawWriter);
+
+            int rawDataLength = RawWriter.Position - rawDataOffset;
+            RawWriter.WriteAt(rawDataOffset + 4, rawDataLength - 8);
+
+            var writer = BeginXmlInstance(TemplateTag.BINA, name, "0");
+            writer.Write(rawDataLength);
+            writer.Write(rawDataOffset);
+            EndXmlInstance();
+        }
+
+        private void ReadTexture()
+        {
+            var textureImporter = new Motoko.TextureXmlImporter(this, xml, filePath);
+            textureImporter.Import();
+        }
+
+        public BinaryWriter BeginXmlInstance(TemplateTag tag, string name, string xmlid)
+        {
+            if (!localRefs.TryGetValue(xmlid, out currentDescriptor))
+            {
+                currentDescriptor = ImporterFile.CreateInstance(tag, name);
+                localRefs.Add(xmlid, currentDescriptor);
+            }
+            else if (currentDescriptor.Tag != tag)
+            {
+                throw new InvalidDataException(string.Format("{0} was expected to be of type {1} but it's type is {2}", xmlid, tag, currentDescriptor.Tag));
+            }
+
+            currentWriter = currentDescriptor.OpenWrite();
+
+            return currentWriter;
+        }
+
+        public void EndXmlInstance()
+        {
+            currentWriter.Dispose();
+        }
+
+#region private class XmlToBinaryVisitor
+
+        private class XmlToBinaryVisitor : IMetaTypeVisitor
+        {
+            private readonly XmlImporter importer;
+            private readonly XmlReader xml;
+            private readonly BinaryWriter writer;
+
+            public XmlToBinaryVisitor(XmlImporter importer, XmlReader xml, BinaryWriter writer)
+            {
+                this.importer = importer;
+                this.xml = xml;
+                this.writer = writer;
+            }
+
+#region IMetaTypeVisitor Members
+
+            void IMetaTypeVisitor.VisitEnum(MetaEnum type)
+            {
+                type.XmlToBinary(xml, writer);
+            }
+
+            void IMetaTypeVisitor.VisitByte(MetaByte type)
+            {
+                writer.Write(XmlConvert.ToByte(xml.ReadElementContentAsString()));
+            }
+
+            void IMetaTypeVisitor.VisitInt16(MetaInt16 type)
+            {
+                writer.Write(XmlConvert.ToInt16(xml.ReadElementContentAsString()));
+            }
+
+            void IMetaTypeVisitor.VisitUInt16(MetaUInt16 type)
+            {
+                writer.Write(XmlConvert.ToUInt16(xml.ReadElementContentAsString()));
+            }
+
+            void IMetaTypeVisitor.VisitInt32(MetaInt32 type)
+            {
+                writer.Write(xml.ReadElementContentAsInt());
+            }
+
+            void IMetaTypeVisitor.VisitUInt32(MetaUInt32 type)
+            {
+                writer.Write(XmlConvert.ToUInt32(xml.ReadElementContentAsString()));
+            }
+
+            void IMetaTypeVisitor.VisitInt64(MetaInt64 type)
+            {
+                writer.Write(xml.ReadElementContentAsLong());
+            }
+
+            void IMetaTypeVisitor.VisitUInt64(MetaUInt64 type)
+            {
+                writer.Write(XmlConvert.ToUInt64(xml.ReadElementContentAsString()));
+            }
+
+            void IMetaTypeVisitor.VisitFloat(MetaFloat type)
+            {
+                writer.Write(xml.ReadElementContentAsFloat());
+            }
+
+            void IMetaTypeVisitor.VisitColor(MetaColor type)
+            {
+                byte[] values = xml.ReadElementContentAsArray(byteConverter);
+
+                if (values.Length > 3)
+                    writer.Write(new Color(values[0], values[1], values[2], values[3]));
+                else
+                    writer.Write(new Color(values[0], values[1], values[2]));
+            }
+
+            void IMetaTypeVisitor.VisitVector2(MetaVector2 type)
+            {
+                writer.Write(xml.ReadElementContentAsVector2());
+            }
+
+            void IMetaTypeVisitor.VisitVector3(MetaVector3 type)
+            {
+                writer.Write(xml.ReadElementContentAsVector3());
+            }
+
+            void IMetaTypeVisitor.VisitMatrix4x3(MetaMatrix4x3 type)
+            {
+                writer.Write(xml.ReadElementContentAsArray(floatConverter, 12));
+            }
+
+            void IMetaTypeVisitor.VisitPlane(MetaPlane type)
+            {
+                writer.Write(xml.ReadElementContentAsArray(floatConverter, 4));
+            }
+
+            void IMetaTypeVisitor.VisitQuaternion(MetaQuaternion type)
+            {
+                writer.Write(xml.ReadElementContentAsArray(floatConverter, 4));
+            }
+
+            void IMetaTypeVisitor.VisitBoundingSphere(MetaBoundingSphere type)
+            {
+                ReadFields(type.Fields);
+            }
+
+            void IMetaTypeVisitor.VisitBoundingBox(MetaBoundingBox type)
+            {
+                ReadFields(type.Fields);
+            }
+
+            void IMetaTypeVisitor.VisitRawOffset(MetaRawOffset type)
+            {
+                //writer.Write(xml.ReadElementContentAsInt());
+                throw new NotImplementedException();
+            }
+
+            void IMetaTypeVisitor.VisitSepOffset(MetaSepOffset type)
+            {
+                //writer.Write(xml.ReadElementContentAsInt());
+                throw new NotImplementedException();
+            }
+
+            void IMetaTypeVisitor.VisitString(MetaString type)
+            {
+                writer.Write(xml.ReadElementContentAsString(), type.Count);
+            }
+
+            void IMetaTypeVisitor.VisitPadding(MetaPadding type)
+            {
+                writer.Write(type.FillByte, type.Count);
+            }
+
+            void IMetaTypeVisitor.VisitPointer(MetaPointer type)
+            {
+                var xmlid = xml.ReadElementContentAsString();
+
+                if (xmlid != null)
+                    xmlid = xmlid.Trim();
+
+                if (string.IsNullOrEmpty(xmlid))
+                {
+                    writer.Write(0);
+                    return;
+                }
+
+                writer.Write(importer.ResolveReference(xmlid, type.Tag));
+            }
+
+            void IMetaTypeVisitor.VisitStruct(MetaStruct type)
+            {
+                ReadFields(type.Fields);
+            }
+
+            void IMetaTypeVisitor.VisitArray(MetaArray type)
+            {
+                int count = ReadArray(type.ElementType, type.Count);
+
+                if (count < type.Count)
+                    writer.Skip((type.Count - count) * type.ElementType.Size);
+            }
+
+            void IMetaTypeVisitor.VisitVarArray(MetaVarArray type)
+            {
+                int countFieldPosition = writer.Position;
+                int count;
+
+                if (type.CountField.Type == MetaType.Int16)
+                {
+                    writer.WriteInt16(0);
+                    count = ReadArray(type.ElementType, UInt16.MaxValue);
+                }
+                else
+                {
+                    writer.Write(0);
+                    count = ReadArray(type.ElementType, Int32.MaxValue);
+                }
+
+                int position = writer.Position;
+                writer.Position = countFieldPosition;
+
+                if (type.CountField.Type == MetaType.Int16)
+                    writer.WriteUInt16(count);
+                else
+                    writer.Write(count);
+
+                writer.Position = position;
+            }
+
+#endregion
+
+            private void ReadFields(IEnumerable<Field> fields)
+            {
+                xml.ReadStartElement();
+                xml.MoveToContent();
+
+                foreach (var field in fields)
+                {
+                    try
+                    {
+                        field.Type.Accept(this);
+                    }
+                    catch (Exception ex)
+                    {
+                        throw new InvalidOperationException(string.Format("Cannot read field '{0}'", field.Name), ex);
+                    }
+                }
+
+                xml.ReadEndElement();
+            }
+
+            protected void ReadStruct(MetaStruct s)
+            {
+                foreach (var field in s.Fields)
+                {
+                    try
+                    {
+                        field.Type.Accept(this);
+                    }
+                    catch (Exception ex)
+                    {
+                        throw new InvalidOperationException(string.Format("Cannot read field '{0}'", field.Name), ex);
+                    }
+                }
+            }
+
+            private int ReadArray(MetaType elementType, int maxCount)
+            {
+                if (xml.IsEmptyElement)
+                {
+                    xml.Read();
+                    return 0;
+                }
+
+                xml.ReadStartElement();
+                xml.MoveToContent();
+
+                string elementName = xml.LocalName;
+                int count = 0;
+
+                for (; count < maxCount && xml.IsStartElement(elementName); count++)
+                    elementType.Accept(this);
+
+                xml.ReadEndElement();
+
+                return count;
+            }
+
+            protected int ReadRawElement(string name, MetaType elementType)
+            {
+                if (!xml.IsStartElement(name))
+                    return 0;
+
+                if (xml.IsEmptyElement)
+                {
+                    xml.ReadStartElement();
+                    return 0;
+                }
+
+                int rawOffset = importer.RawWriter.Align32();
+
+                elementType.Accept(new RawXmlImporter(xml, importer.RawWriter));
+
+                return rawOffset;
+            }
+
+            protected RawArray ReadRawArray(string name, MetaType elementType)
+            {
+                if (!xml.IsStartElement(name))
+                    return new RawArray();
+
+                if (xml.IsEmptyElement)
+                {
+                    xml.ReadStartElement();
+                    return new RawArray();
+                }
+
+                xml.ReadStartElement();
+
+                int rawOffset = importer.RawWriter.Align32();
+
+                var rawImporter = new RawXmlImporter(xml, importer.RawWriter);
+                int elementCount = 0;
+
+                while (xml.IsStartElement(elementType.Name))
+                {
+                    elementType.Accept(rawImporter);
+                    elementCount++;
+                }
+
+                xml.ReadEndElement();
+
+                return new RawArray(rawOffset, elementCount);
+            }
+        }
+
+#endregion
+
+        private ImporterDescriptor ResolveReference(string xmlid, TemplateTag tag)
+        {
+            ImporterDescriptor descriptor;
+
+            if (xmlid[0] == '#')
+                descriptor = ResolveLocalReference(xmlid.Substring(1), tag);
+            else
+                descriptor = ResolveExternalReference(xmlid, tag);
+
+            return descriptor;
+        }
+
+        private ImporterDescriptor ResolveLocalReference(string xmlid, TemplateTag tag)
+        {
+            ImporterDescriptor descriptor;
+
+            if (!localRefs.TryGetValue(xmlid, out descriptor))
+            {
+                descriptor = ImporterFile.CreateInstance(tag);
+                localRefs.Add(xmlid, descriptor);
+            }
+            else if (tag != TemplateTag.NONE && tag != descriptor.Tag)
+            {
+                throw new InvalidDataException(string.Format("{0} was expected to be of type {1} but it's type is {2}", xmlid, tag, descriptor.Tag));
+            }
+
+            return descriptor;
+        }
+
+        private ImporterDescriptor ResolveExternalReference(string xmlid, TemplateTag tag)
+        {
+            ImporterDescriptor descriptor;
+
+            if (!externalRefs.TryGetValue(xmlid, out descriptor))
+            {
+                if (xmlid.EndsWith(".xml", StringComparison.Ordinal)
+                    || xmlid.EndsWith(".dae", StringComparison.Ordinal)
+                    || xmlid.EndsWith(".obj", StringComparison.Ordinal)
+                    || xmlid.EndsWith(".tga", StringComparison.Ordinal))
+                {
+                    string filePath = Path.Combine(baseDir, xmlid);
+
+                    if (!File.Exists(filePath))
+                        throw new InvalidDataException(string.Format("Cannot find referenced file '{0}'", filePath));
+
+                    if (tag == TemplateTag.TRCM)
+                    {
+                        var bodyImporter = new Totoro.BodyDaeImporter(args);
+                        descriptor = bodyImporter.Import(filePath, ImporterFile);
+                    }
+                    else if (tag == TemplateTag.M3GM
+                        && (currentDescriptor.Tag == TemplateTag.ONWC
+                            || currentDescriptor.Tag == TemplateTag.CONS
+                            || currentDescriptor.Tag == TemplateTag.DOOR
+                            || currentDescriptor.Tag == TemplateTag.OFGA))
+                    {
+                        var geometryImporter = new Motoko.GeometryImporter(args);
+                        descriptor = geometryImporter.Import(filePath, ImporterFile);
+                    }
+                    else
+                    {
+                        AddDependency(filePath, tag);
+                        var name = Importer.DecodeFileName(Path.GetFileNameWithoutExtension(filePath));
+                        descriptor = ImporterFile.CreateInstance(tag, name);
+                    }
+                }
+                else
+                {
+                    if (tag != TemplateTag.NONE)
+                    {
+                        //
+                        // If the link has a known type tag then make sure the xml id
+                        // is prefixed with the tag name.
+                        //
+
+                        var typeName = tag.ToString();
+
+                        if (!xmlid.StartsWith(typeName, StringComparison.Ordinal))
+                            xmlid = typeName + xmlid;
+                    }
+                    else
+                    {
+                        //
+                        // IGPG contains a link that can point to either TXMP or PSpc.
+                        // In this case the xml id should be already prefixed with 
+                        // the tag name because we have no way to guess what type the link is.
+                        //
+
+                        var tagName = xmlid.Substring(0, 4);
+                        tag = (TemplateTag)Enum.Parse(typeof(TemplateTag), tagName);
+                    }
+
+                    descriptor = ImporterFile.CreateInstance(tag, xmlid);
+                }
+
+                externalRefs.Add(xmlid, descriptor);
+            }
+
+            return descriptor;
+        }
+    }
+}
Index: /OniSplit/Xml/XmlReaderExtensions.cs
===================================================================
--- /OniSplit/Xml/XmlReaderExtensions.cs	(revision 1114)
+++ /OniSplit/Xml/XmlReaderExtensions.cs	(revision 1114)
@@ -0,0 +1,303 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Xml;
+
+namespace Oni.Xml
+{
+    internal static class XmlReaderExtensions
+    {
+        private static readonly char[] emptyChars = new char[0];
+
+        [ThreadStatic]
+        private static char[] charBuffer;
+
+        private static char[] CharBuffer
+        {
+            get
+            {
+                if (charBuffer == null)
+                    charBuffer = new char[16384];
+
+                return charBuffer;
+            }
+        }
+
+        public static IEnumerable<string> ReadElementContentAsList(this XmlReader xml)
+        {
+            if (xml.SkipEmpty())
+                yield break;
+
+            xml.ReadStartElement();
+
+            if (xml.NodeType == XmlNodeType.EndElement)
+            {
+                xml.ReadEndElement();
+                yield break;
+            }
+
+#if DNET
+            char[] buffer = CharBuffer;
+            int offset = 0;
+            int length;
+
+            while ((length = xml.ReadValueChunk(buffer, offset, buffer.Length - offset)) > 0 || offset > 0)
+            {
+                bool hasMore = length != 0;
+                length += offset;
+                offset = 0;
+
+                for (int i = 0; i < length; i++)
+                {
+                    if (Char.IsWhiteSpace(buffer[i]))
+                        continue;
+
+                    int start = i;
+
+                    do
+                        i++;
+                    while (i < length && !Char.IsWhiteSpace(buffer[i]));
+
+                    if (i == length && hasMore)
+                    {
+                        offset = i - start;
+                        Array.Copy(buffer, start, buffer, 0, offset);
+                        break;
+                    }
+
+                    yield return new string(buffer, start, i - start);
+                }
+            }
+
+            while (xml.NodeType != XmlNodeType.EndElement)
+                xml.Read();
+#else
+            string buffer = xml.ReadContentAsString();
+
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                if (char.IsWhiteSpace(buffer[i]))
+                    continue;
+
+                int start = i;
+
+                do
+                    i++;
+                while (i < buffer.Length && !char.IsWhiteSpace(buffer[i]));
+
+                yield return buffer.Substring(start, i - start);
+            }
+#endif
+
+            xml.ReadEndElement();
+        }
+
+        private static void ReadArrayCore<T>(XmlReader xml, Func<string, T> parser, List<T> list)
+        {
+            foreach (var text in xml.ReadElementContentAsList())
+                list.Add(parser(text));
+        }
+
+        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> parser)
+        {
+            var list = new List<T>();
+            ReadArrayCore<T>(xml, parser, list);
+            return list.ToArray();
+        }
+
+        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> converter, int count)
+        {
+            var list = new List<T>(count);
+            ReadArrayCore(xml, converter, list);
+            var array = new T[count];
+            list.CopyTo(0, array, 0, count);
+            return array;
+        }
+
+        public static void ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> parser, T[] array)
+        {
+            var text = xml.ReadElementContentAsString();
+            var tokens = text.Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);
+
+            for (int i = 0; i < array.Length; i++)
+            {
+                if (i < tokens.Length)
+                    array[i] = parser(tokens[i]);
+                else
+                    array[i] = default(T);
+            }
+        }
+
+        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> parser, string name)
+        {
+            string text;
+
+            if (name == null)
+                text = xml.ReadElementContentAsString();
+            else
+                text = xml.ReadElementContentAsString(name, string.Empty);
+
+            var tokens = text.Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);
+            return tokens.ConvertAll(parser);
+        }
+
+        private static T[] ReadArray<T>(this XmlReader xml, Func<string, T> parser, int count)
+        {
+            var text = xml.ReadElementContentAsString();
+            var tokens = text.Split(emptyChars, StringSplitOptions.RemoveEmptyEntries);
+            var values = tokens.ConvertAll(parser);
+
+            Array.Resize(ref values, count);
+
+            return values;
+        }
+
+        public static T[] ReadElementContentAsArray<T>(this XmlReader xml, Func<string, T> converter, int count, string name)
+        {
+            var array = xml.ReadElementContentAsArray<T>(converter, name);
+            Array.Resize(ref array, count);
+            return array;
+        }
+
+
+        public static Vector2 ReadElementContentAsVector2(this XmlReader xml, string name = null)
+        {
+            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 2, name);
+
+            return new Vector2(values[0], values[1]);
+        }
+
+        public static Vector3 ReadElementContentAsVector3(this XmlReader xml, string name = null)
+        {
+            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 3);
+
+            return new Vector3(values[0], values[1], values[2]);
+        }
+
+        public static Vector4 ReadElementContentAsVector4(this XmlReader xml)
+        {
+            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 4);
+
+            return new Vector4(values[0], values[1], values[2], values[3]);
+        }
+
+        public static Quaternion ReadElementContentAsEulerXYZ(this XmlReader xml)
+        {
+            var values = xml.ReadElementContentAsArray(XmlConvert.ToSingle, 3);
+
+            return Quaternion.CreateFromEulerXYZ(values[0], values[1], values[2]);
+        }
+
+        public static Quaternion ReadElementContentAsQuaternion(this XmlReader xml, string name = null)
+        {
+            var quat = Quaternion.Identity;
+
+            if (xml.IsEmptyElement)
+            {
+                xml.Skip();
+            }
+            else
+            {
+                if (name == null)
+                    xml.ReadStartElement();
+                else
+                    xml.ReadStartElement(name);
+
+                if (xml.NodeType == XmlNodeType.Text)
+                {
+                    var values = xml.ReadArray(XmlConvert.ToSingle, 4);
+
+                    quat = new Quaternion(values[0], values[1], values[2], -values[3]);
+                }
+                else
+                {
+                    var transforms = new List<Quaternion>();
+
+                    while (xml.IsStartElement())
+                    {
+                        switch (xml.LocalName)
+                        {
+                            case "rotate":
+                                var rotate = xml.ReadElementContentAsVector4();
+                                transforms.Add(Quaternion.CreateFromAxisAngle(rotate.XYZ, MathHelper.ToRadians(rotate.W)));
+                                break;
+                            case "euler":
+                                transforms.Add(xml.ReadElementContentAsEulerXYZ());
+                                break;
+                            default:
+                                throw new XmlException(string.Format("Unknown element {0}", xml.LocalName));
+                        }
+                    }
+
+                    foreach (var transform in Utils.Reverse(transforms))
+                        quat *= transform;
+                }
+
+                xml.ReadEndElement();
+            }
+
+            return quat;
+        }
+
+        public static Matrix ReadElementContentAsMatrix43(this XmlReader xml, string name = null)
+        {
+            var matrix = Matrix.Identity;
+
+            if (xml.IsEmptyElement)
+            {
+                xml.Skip();
+            }
+            else
+            {
+                if (name == null)
+                    xml.ReadStartElement();
+                else
+                    xml.ReadStartElement(name);
+
+                if (xml.NodeType == XmlNodeType.Text)
+                {
+                    var values = xml.ReadArray(XmlConvert.ToSingle, 12);
+
+                    matrix = new Matrix(
+                        values[0], values[1], values[2], 0.0f,
+                        values[3], values[4], values[5], 0.0f,
+                        values[6], values[7], values[8], 0.0f,
+                        values[9], values[10], values[11], 1.0f);
+                }
+                else
+                {
+                    var transforms = new List<Matrix>();
+
+                    while (xml.IsStartElement())
+                    {
+                        switch (xml.LocalName)
+                        {
+                            case "translate":
+                                transforms.Add(Matrix.CreateTranslation(xml.ReadElementContentAsVector3()));
+                                break;
+                            case "rotate":
+                                var rotate = xml.ReadElementContentAsVector4();
+                                transforms.Add(Matrix.CreateFromAxisAngle(rotate.XYZ, MathHelper.ToRadians(rotate.W)));
+                                break;
+                            case "euler":
+                                transforms.Add(xml.ReadElementContentAsEulerXYZ().ToMatrix());
+                                break;
+                            case "scale":
+                                transforms.Add(Matrix.CreateScale(xml.ReadElementContentAsVector3()));
+                                break;
+                            default:
+                                throw new XmlException(string.Format("Unknown element {0}", xml.LocalName));
+                        }
+                    }
+
+                    foreach (var transform in Utils.Reverse(transforms))
+                        matrix *= transform;
+                }
+
+                xml.ReadEndElement();
+            }
+
+            return matrix;
+        }
+    }
+}
Index: /OniSplit/Xml/XmlWriterExtensions.cs
===================================================================
--- /OniSplit/Xml/XmlWriterExtensions.cs	(revision 1114)
+++ /OniSplit/Xml/XmlWriterExtensions.cs	(revision 1114)
@@ -0,0 +1,29 @@
+﻿using System;
+using System.Text;
+using System.Xml;
+
+namespace Oni.Xml
+{
+    internal static class XmlWriterExtensions
+    {
+        public static void WriteFloatArray(this XmlWriter writer, float[] array)
+        {
+            writer.WriteArray(array, XmlConvert.ToString);
+        }
+
+        public static void WriteArray<T>(this XmlWriter writer, T[] array, Func<T, string> converter)
+        {
+            var text = new StringBuilder(array.Length * 8);
+
+            for (int i = 0; i < array.Length; i++)
+            {
+                if (i != array.Length - 1)
+                    text.AppendFormat("{0} ", converter(array[i]));
+                else
+                    text.Append(converter(array[i]));
+            }
+
+            writer.WriteValue(text.ToString());
+        }
+    }
+}
Index: /OniSplit/app.config
===================================================================
--- /OniSplit/app.config	(revision 1114)
+++ /OniSplit/app.config	(revision 1114)
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<configuration>
+    <runtime>
+        <gcServer enabled="true"/>
+        <gcConcurrent enabled="true"/>
+    </runtime>
+    <startup>
+        <!--<supportedRuntime version="v2.0.50727"/>-->
+        <supportedRuntime version="v4.0.30319"/>
+    </startup>
+</configuration>
