public void ApplyChanges()
        {
            // Models are always the first entry per CGFX standard
            var models = cgfx.Data.Entries[0]?.Entries;

            // Only one model per file for now
            var model = (DICTObjModel)models.First().EntryObject;

            // Textures are the second
            var textures = cgfx.Data.Entries[1]?.Entries.Select(e => e.EntryObject).Cast <DICTObjTexture>().ToList();

            if (textures != null && textures.Count > 0)
            {
                for (var t = 0; t < textures.Count; t++)
                {
                    if (Textures[t].TextureBitmap != null)
                    {
                        var textureBitmap = Textures[t].TextureBitmap;
                        var textureData   = textures[t];

                        if (textureData != null)
                        {
                            if (textureBitmap.Width != textureData.Width || textureBitmap.Height != textureData.Height)
                            {
                                throw new InvalidOperationException($"{textureData.Name} import: Bitmap mismatched expected dimensions -- texture data expected {textureData.Width}x{textureData.Height}, importing image is {textureBitmap.Width}x{textureBitmap.Height}");
                            }

                            var imgData     = textureBitmap.LockBits(new Rectangle(0, 0, (int)textureData.Width, (int)textureData.Height), ImageLockMode.ReadOnly, textureBitmap.PixelFormat);
                            var rawRGBAData = new byte[(imgData.Height * imgData.Stride)];
                            Marshal.Copy(imgData.Scan0, rawRGBAData, 0, rawRGBAData.Length);
                            textureBitmap.UnlockBits(imgData);

                            if (textureBitmap.PixelFormat == PixelFormat.Format24bppRgb)
                            {
                                // Need to upconvert the data to RGBA before going in!
                                var newData = new byte[textureBitmap.Width * textureBitmap.Height * 4];

                                var newDataOffset = 0;
                                for (var oldDataOffset = 0; oldDataOffset < rawRGBAData.Length; oldDataOffset += 3)
                                {
                                    newData[newDataOffset + 0] = rawRGBAData[oldDataOffset + 0];
                                    newData[newDataOffset + 1] = rawRGBAData[oldDataOffset + 1];
                                    newData[newDataOffset + 2] = rawRGBAData[oldDataOffset + 2];
                                    newData[newDataOffset + 3] = 0xFF;

                                    newDataOffset += 4;
                                }

                                rawRGBAData = newData;
                            }
                            else if (textureBitmap.PixelFormat != PixelFormat.Format32bppArgb)
                            {
                                throw new InvalidOperationException($"{textureData.Name} import: Unsupported image format {textureBitmap.PixelFormat}");
                            }

                            var utility = new Utility(null, null, Endianness.Little);
                            textureData.SetTexture(utility, rawRGBAData);       // TODO -- try disabling the safety checks...
                        }
                    }
                }
            }

            for (var m = 0; m < model.Meshes.Length; m++)
            {
                var mesh  = model.Meshes[m];
                var shape = model.Shapes[mesh.ShapeIndex];

                var SMMesh = Meshes[m];

                // NOTE: These are all making the same assumptions as noted in the constructor
                var vertexBuffer = shape.VertexBuffers.Where(vb => vb is VertexBufferInterleaved).Cast <VertexBufferInterleaved>().Single();

                var subMesh        = shape.SubMeshes[0];
                var faceHeader     = subMesh.Faces[0];
                var faceDescriptor = faceHeader.FaceDescriptors[0];

                // Vertices are stored (in GPU-compatible form)
                var vertexBufferIndex = shape.VertexBuffers.IndexOf(vertexBuffer);
                var attributes        = vertexBuffer.Attributes.Select(a => VertexBufferCodec.PICAAttribute.GetPICAAttribute(a)).ToList();

                // Convert to the simplified vertices
                var boneIndexCount  = GetElementsOfAttribute(attributes, VertexBuffer.PICAAttributeName.BoneIndex);  // How many bone indices are actually used
                var boneWeightCount = GetElementsOfAttribute(attributes, VertexBuffer.PICAAttributeName.BoneWeight); // How many bone weights are actually used

                var vertices = SMMesh.Vertices.Select(v => new VertexBufferCodec.PICAVertex
                {
                    Position  = new Vector4(v.Position.X, v.Position.Y, v.Position.Z, 0),
                    Normal    = new Vector4(v.Normal.X, v.Normal.Y, v.Normal.Z, 0),
                    TexCoord0 = new Vector4(v.TexCoord.X, v.TexCoord.Y, v.TexCoord.Z, 0),
                    Indices   = GetBoneIndices(v.BoneIndices),
                    Weights   = GetBoneWeights(v.Weights),
                    Color     = v.Color
                }).ToArray();

                // Same as when we loaded it in the first place, the bone indices need to be changed into a list
                var boneReferences =
                    // Get the absolute indices
                    SMMesh.Vertices.SelectMany(v => v.BoneIndices).Distinct().ToList()

                    // Remap to the bones
                    .SelectMany(smshape => model.Skeleton.Bones.Entries.Select(b => b.EntryObject).Cast <DICTObjBone>().Where(b => b.Index == smshape))

                    .ToList();

                var boneReferenceIndexes = boneReferences.Select(b => b.Index).ToList();

                // Reassign the indices so they're relative to the bone references pool
                for (var v = 0; v < vertices.Length; v++)
                {
                    for (var bi = 0; bi < boneIndexCount; bi++)
                    {
                        var absoluteIndex = vertices[v].Indices[bi];

                        vertices[v].Indices[bi] = boneReferenceIndexes.IndexOf(absoluteIndex);
                    }
                }

                // Convert the vertices back to native interleaved format
                vertexBuffer.RawBuffer = VertexBufferCodec.GetBuffer(vertices, vertexBuffer.Attributes.Select(a => VertexBufferCodec.PICAAttribute.GetPICAAttribute(a)));

                // Align if ending on odd byte
                if ((vertexBuffer.RawBuffer.Length & 1) == 1)
                {
                    vertexBuffer.RawBuffer = vertexBuffer.RawBuffer.Concat(new byte[] { 0 }).ToArray();
                }

                // Replace the bone references in the SubMesh with the generated list
                // NOTE: Don't do this if boneIndexCount = 0, because apparently there
                // is always one defined even if not explicitly used...
                //
                // NOTE 2: I handle this during the Load case but not here but just
                // assuming if the submesh has bone refs then the object probably does
                // want to use them even if this is zero; seems MAYBE that means that
                // vertices don't specify a bone reference if it applies to the entire
                // mesh. Could use some more research to verify this...
                //
                // The big issue is if the editor tries to assign vertices in this mesh
                // to other bones and the assignment will BE IGNORED.
                if (boneIndexCount > 0)
                {
                    subMesh.BoneReferences.Clear();
                    subMesh.BoneReferences.AddRange(boneReferences);
                }

                //// Get indicies of triangle faces
                var indices = SMMesh.Triangles.SelectMany(t => new[] { t.v1, t.v2, t.v3 }).ToList();

                faceDescriptor.Indices.Clear();
                faceDescriptor.Indices.AddRange(indices);
            }
        }
        // This REQUIRES a backing CGFX file and doesn't contain enough data to regenerate one from scratch
        public SimplifiedModel(CGFX cgfx)
        {
            this.cgfx = cgfx;

            // Models are always the first entry per CGFX standard
            var models = cgfx.Data.Entries[0]?.Entries;

            // Textures are the second
            var textures = cgfx.Data.Entries[1]?.Entries.Select(e => e.EntryObject).Cast <DICTObjTexture>().ToList();

            if (textures != null && textures.Count > 0)
            {
                Textures = new SMTexture[textures.Count];

                for (var t = 0; t < textures.Count; t++)
                {
                    Bitmap textureBitmap = null;
                    var    name          = textures[t].Name;
                    var    textureData   = textures[t];

                    if (textureData != null)
                    {
                        var textureRGBA = TextureCodec.ConvertTextureToRGBA(new Utility(null, null, Endianness.Little), textureData.TextureCGFXData, textureData.TextureFormat, (int)textureData.Width, (int)textureData.Height);

                        textureBitmap = new Bitmap((int)textureData.Width, (int)textureData.Height, PixelFormat.Format32bppArgb);
                        var imgData = textureBitmap.LockBits(new Rectangle(0, 0, textureBitmap.Width, textureBitmap.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
                        Marshal.Copy(textureRGBA, 0, imgData.Scan0, textureRGBA.Length);
                        textureBitmap.UnlockBits(imgData);
                        textureBitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
                    }

                    var smTexture = new SMTexture(name, textureBitmap);
                    Textures[t] = smTexture;
                }
            }

            if (models != null && models.Count > 0)
            {
                // This probably isn't a difficult problem to work around, but it's out of my scope at this time
                if (models.Count != 1)
                {
                    throw new InvalidOperationException("File contains more than one model; only supporting one for now.");
                }

                var model = (DICTObjModel)models.First().EntryObject;

                // NOTE: Currently NOT committing the skeleton back, we're just keeping it for model software
                var bones = model.Skeleton?.Bones.Entries.Select(e => e.EntryObject).Cast <DICTObjBone>().ToList() ?? new List <DICTObjBone>();

                Bones = bones.Select(b => new SMBone
                {
                    Name           = b.Name,
                    ParentName     = b.Parent?.Name,
                    Rotation       = b.Rotation,
                    Translation    = b.Translation,
                    Scale          = b.Scale,
                    LocalTransform = b.LocalTransform
                }).ToArray();

                Meshes = new SMMesh[model.Meshes.Length];

                for (var m = 0; m < model.Meshes.Length; m++)
                {
                    var mesh  = model.Meshes[m];
                    var shape = model.Shapes[mesh.ShapeIndex];

                    // There might be some clever way of handling multiple vertex buffers
                    // (if it actually happens) but I'm not worried about it. Only looking
                    // for a single one that is VertexBufferInterleaved.
                    var vertexBuffersInterleaved = shape.VertexBuffers.Where(vb => vb is VertexBufferInterleaved);
                    if (vertexBuffersInterleaved.Count() != 1)
                    {
                        throw new InvalidOperationException("Unsupported count of VertexBuffers in VertexBufferInterleaved format");
                    }

                    // Only expecting / supporting 1 SubMesh entry
                    if (shape.SubMeshes.Count != 1)
                    {
                        throw new InvalidOperationException("Unsupported amount of SubMeshes");
                    }

                    var subMesh = shape.SubMeshes[0];

                    // The BoneReferences in the SubMesh are what the vertex's local index references.
                    var boneReferences = subMesh.BoneReferences;

                    // These aren't "faces" in the geometrical sense, but rather a header of sorts
                    if (subMesh.Faces.Count != 1)
                    {
                        throw new InvalidOperationException("Unsupported amount of Faces");
                    }

                    var faceHeader = subMesh.Faces[0];

                    // Again, just one FaceDescriptor...
                    if (faceHeader.FaceDescriptors.Count != 1)
                    {
                        throw new InvalidOperationException("Unsupported amount of FaceDescriptors");
                    }

                    var faceDescriptor = faceHeader.FaceDescriptors[0];

                    // We're also only supporting triangles at this point; the model format probably
                    // allows for more groups of geometry, but again, out of my scope
                    if (faceDescriptor.PrimitiveMode != FaceDescriptor.PICAPrimitiveMode.Triangles)
                    {
                        throw new InvalidOperationException("Only supporting triangles format");
                    }

                    // Vertices are stored (in GPU-compatible form)
                    var vertexBuffer      = (VertexBufferInterleaved)vertexBuffersInterleaved.Single();
                    var vertexBufferIndex = shape.VertexBuffers.IndexOf(vertexBuffer);
                    var attributes        = vertexBuffer.Attributes.Select(a => VertexBufferCodec.PICAAttribute.GetPICAAttribute(a)).ToList();

                    // The following are the only VertexAttributes we are supporting at this time
                    var supportedAttributes = new List <VertexBuffer.PICAAttributeName>
                    {
                        VertexBuffer.PICAAttributeName.Position,
                        VertexBuffer.PICAAttributeName.Normal,
                        VertexBuffer.PICAAttributeName.TexCoord0,
                        VertexBuffer.PICAAttributeName.BoneIndex,
                        VertexBuffer.PICAAttributeName.BoneWeight,

                        // Caution: Vertex color may not be supported by all model editors!
                        VertexBuffer.PICAAttributeName.Color
                    };

                    // Check if any unsupported attributes are in use
                    var unsupportedAttributes = attributes.Where(a => !supportedAttributes.Contains(a.Name)).Select(a => a.Name);
                    if (unsupportedAttributes.Any())
                    {
                        throw new InvalidOperationException($"This model is using the following unsupported attributes: {string.Join(", ", unsupportedAttributes)}");
                    }

                    var nativeVertices = VertexBufferCodec.GetVertices(shape, vertexBufferIndex);

                    // Convert to the simplified vertices
                    var boneIndexCount  = GetElementsOfAttribute(attributes, VertexBuffer.PICAAttributeName.BoneIndex);  // How many bone indices are actually used
                    var boneWeightCount = GetElementsOfAttribute(attributes, VertexBuffer.PICAAttributeName.BoneWeight); // How many bone weights are actually used

                    // FIXME? There seems to be, on occasion, a bone relationship that points to
                    // the entire mesh but not assigned to any of the vertices. So basically the
                    // vertices are recorded as having no bone indices but in fact are all dependent
                    // upon associating with bone index 0 (?) This will force it to use at least
                    // one bone index even if zero is specified for this case.
                    if (boneReferences != null && boneReferences.Count > 0)
                    {
                        boneIndexCount = Math.Max(boneIndexCount, 1);
                    }

                    var vertices = nativeVertices.Select(v => new SMVertex
                    {
                        Position    = new Vector3(v.Position.X, v.Position.Y, v.Position.Z),
                        Normal      = new Vector3(v.Normal.X, v.Normal.Y, v.Normal.Z),
                        TexCoord    = new Vector3(v.TexCoord0.X, v.TexCoord0.Y, v.TexCoord0.Z),
                        BoneIndices = (new[] { v.Indices.b0, v.Indices.b1, v.Indices.b2, v.Indices.b3 }).Take(boneIndexCount).ToArray(),
                        Weights     = (new[] { v.Weights.w0, v.Weights.w1, v.Weights.w2, v.Weights.w3 }).Take(boneWeightCount).ToArray(),
                        Color       = v.Color // Caution! Not all 3D model editors may support vertex color!
                    }).ToList();

                    // The vertices use relative bone indices based on the SubMesh definitions,
                    // which we're going to make absolute now
                    for (var v = 0; v < vertices.Count; v++)
                    {
                        var vertex = vertices[v];

                        for (var i = 0; i < vertex.BoneIndices.Length; i++)
                        {
                            vertex.BoneIndices[i] = boneReferences[vertex.BoneIndices[i]].Index;

                            // Also, if no bone weights are available, assign a weight of 1 to the first bone.
                            // This won't be stored ultimately as the PICA attributes won't specify it.
                            if (vertex.Weights.Length == 0)
                            {
                                vertex.Weights = new float[] { 1.0f };
                            }
                        }
                    }

                    // Deconstruct into triangle faces
                    var triangles = new List <SMTriangle>();
                    var indices   = faceDescriptor.Indices;
                    for (var i = 0; i < indices.Count; i += 3)
                    {
                        triangles.Add(new SMTriangle
                        {
                            v1 = indices[i + 0],
                            v2 = indices[i + 1],
                            v3 = indices[i + 2]
                        });
                    }

                    // Finally, assign material, if available (mostly for the model editor's benefit)
                    var material  = model.ModelMaterials.Entries.Select(e => e.EntryObject).Cast <DICTObjModelMaterial>().ToList()[mesh.MaterialId];
                    var texture   = material.TextureMappers.First().TextureReference;
                    var name      = texture.ReferenceName;
                    var smTexture = (Textures != null) ? Textures.Where(t => t.Name == name).SingleOrDefault() : null;
                    if (smTexture == null)
                    {
                        smTexture = new SMTexture(name, null);
                    }

                    Meshes[m] = new SMMesh(vertices, triangles, shape, vertexBufferIndex, smTexture);
                }
            }
        }