Serializes classes into tag data by using reflection.
        public void InjectDds(TagSerializer serializer, TagDeserializer deserializer, Bitmap bitmap, int imageIndex, Stream ddsStream)
        {
            var resource = bitmap.Resources[imageIndex].Resource;
            var newResource = (resource == null);
            ResourceSerializationContext resourceContext;
            BitmapTextureResourceDefinition definition;
            if (newResource)
            {
                // Create a new resource reference
                resource = new ResourceReference
                {
                    DefinitionFixups = new List<ResourceDefinitionFixup>(),
                    D3DObjectFixups = new List<D3DObjectFixup>(),
                    Type = 1, // TODO: Map out this type enum instead of using numbers
                    Unknown68 = 1
                };
                bitmap.Resources[imageIndex].Resource = resource;
                resourceContext = new ResourceSerializationContext(resource);
                definition = new BitmapTextureResourceDefinition
                {
                    Texture = new D3DPointer<BitmapTextureResourceDefinition.BitmapDefinition>
                    {
                        Definition = new BitmapTextureResourceDefinition.BitmapDefinition()
                    }
                };
            }
            else
            {
                // Deserialize the old definition
                resourceContext = new ResourceSerializationContext(resource);
                definition = deserializer.Deserialize<BitmapTextureResourceDefinition>(resourceContext);
            }
            if (definition.Texture == null || definition.Texture.Definition == null)
                throw new ArgumentException("Invalid bitmap definition");
            var texture = definition.Texture.Definition;
            var imageData = bitmap.Images[imageIndex];

            // Read the DDS header and modify the definition to match
            var dds = DdsHeader.Read(ddsStream);
            var dataSize = (int)(ddsStream.Length - ddsStream.Position);
            texture.Data = new ResourceDataReference(dataSize, new ResourceAddress(ResourceAddressType.Resource, 0));
            texture.Width = (short)dds.Width;
            texture.Height = (short)dds.Height;
            texture.Depth = (sbyte)Math.Max(1, dds.Depth);
            texture.Levels = (sbyte)Math.Max(1, dds.MipMapCount);
            texture.Type = BitmapDdsFormatDetection.DetectType(dds);
            texture.D3DFormatUnused = (int)((dds.D3D10Format != DxgiFormat.Bc5UNorm) ? dds.FourCc : DdsFourCc.FromString("ATI2"));
            texture.Format = BitmapDdsFormatDetection.DetectFormat(dds);

            // Set flags based on the format
            switch (texture.Format)
            {
                case BitmapFormat.Dxt1:
                case BitmapFormat.Dxt3:
                case BitmapFormat.Dxt5:
                case BitmapFormat.Dxn:
                    texture.Flags = BitmapFlags.Compressed;
                    break;
                default:
                    texture.Flags = BitmapFlags.None;
                    break;
            }
            if ((texture.Width & (texture.Width - 1)) == 0 && (texture.Height & (texture.Height - 1)) == 0)
                texture.Flags |= BitmapFlags.PowerOfTwoDimensions;

            // If creating a new image, then add a new resource, otherwise replace the existing one
            if (newResource)
                _resourceManager.Add(resource, ResourceLocation.Textures, ddsStream);
            else
                _resourceManager.Replace(resource, ddsStream);

            // Serialize the new resource definition
            serializer.Serialize(resourceContext, definition);

            // Modify the image data in the bitmap tag to match the definition
            imageData.Width = texture.Width;
            imageData.Height = texture.Height;
            imageData.Depth = texture.Depth;
            imageData.Type = texture.Type;
            imageData.Format = texture.Format;
            imageData.Flags = texture.Flags;
            imageData.MipmapCount = (sbyte)(texture.Levels - 1);
            imageData.DataOffset = texture.Data.Address.Offset;
            imageData.DataSize = texture.Data.Size;
            imageData.Unknown15 = texture.Unknown35;
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 1)
                return false;

            var destDir = new DirectoryInfo(args[0]);
            if (!destDir.Exists)
            {
                Write("Destination directory does not exist. Create it? [y/n] ");
                var answer = ReadLine().ToLower();

                if (answer.Length == 0 || !(answer.StartsWith("y") || answer.StartsWith("n")))
                    return false;

                if (answer.StartsWith("y"))
                    destDir.Create();
                else
                    return false;
            }

            WriteLine($"Generating cache files in \"{destDir.FullName}\"...");

            var destTagsFile = new FileInfo(Combine(destDir.FullName, "tags.dat"));

            Write($"Generating {destTagsFile.FullName}...");
            using (var tagCacheStream = destTagsFile.Create())
            using (var writer = new BinaryWriter(tagCacheStream))
            {
                writer.Write((int)0); // padding
                writer.Write((int)0); // tag list offset
                writer.Write((int)0); // tag count
                writer.Write((int)0); // padding
                writer.Write((long)130713360239499012); // timestamp
                writer.Write((long)0); // padding
            }
            WriteLine("done.");

            var destStringIDsFile = new FileInfo(Combine(destDir.FullName, "string_ids.dat"));

            Write($"Generating {destStringIDsFile.FullName}...");
            using (var stringIDCacheStream = destStringIDsFile.Create())
            using (var writer = new BinaryWriter(stringIDCacheStream))
            {
                writer.Write((int)0); // string count
                writer.Write((int)0); // data size
            }
            WriteLine("done.");

            var resourceCachePaths = new string[]
            {
                Combine(destDir.FullName, "audio.dat"),
                Combine(destDir.FullName, "resources.dat"),
                Combine(destDir.FullName, "textures.dat"),
                Combine(destDir.FullName, "textures_b.dat"),
                Combine(destDir.FullName, "video.dat")
            };

            foreach (var resourceCachePath in resourceCachePaths)
            {
                Write($"Generating {resourceCachePath}...");
                using (var resourceCacheStream = File.Create(resourceCachePath))
                using (var writer = new BinaryWriter(resourceCacheStream))
                {
                    writer.Write((int)0); // padding
                    writer.Write((int)0); // table offset
                    writer.Write((int)0); // resource count
                    writer.Write((int)0); // padding
                }
                WriteLine("done.");
            }

            var dependencies = new Dictionary<int, TagInstance>();
            LoadTagDependencies(0, ref dependencies);
            LoadTagDependencies(0x16, ref dependencies);
            LoadTagDependencies(0x27D7, ref dependencies);
            
            var destResourcesFile = new FileInfo(Combine(destDir.FullName, "resources.dat"));
            if (!destResourcesFile.Exists)
            {
                WriteLine($"Destination resource cache file does not exist: {destResourcesFile.FullName}");
                return false;
            }

            var destTexturesFile = new FileInfo(Combine(destDir.FullName, "textures.dat"));
            if (!destTexturesFile.Exists)
            {
                WriteLine($"Destination texture cache file does not exist: {destTexturesFile.FullName}");
                return false;
            }

            var destTexturesBFile = new FileInfo(Combine(destDir.FullName, "textures_b.dat"));
            if (!destTexturesBFile.Exists)
            {
                WriteLine($"Destination texture cache file does not exist: {destTexturesBFile.FullName}");
                return false;
            }

            var destAudioFile = new FileInfo(Combine(destDir.FullName, "audio.dat"));
            if (!destAudioFile.Exists)
            {
                WriteLine($"Destination audio cache file does not exist: {destAudioFile.FullName}");
                return false;
            }
            
            TagCache destTagCache;
            using (var stream = destTagsFile.OpenRead())
                destTagCache = new TagCache(stream);

            DefinitionSet guessedVersion;
            var destVersion = Detect(destTagCache, out guessedVersion);
            if (destVersion == Unknown)
            {
                WriteLine($"Unrecognized target version! (guessed {GetVersionString(guessedVersion)})");
                return true;
            }

            WriteLine($"Destination cache version: {GetVersionString(destVersion)}");

            StringIDCache destStringIDCache;
            using (var stream = destStringIDsFile.OpenRead())
                destStringIDCache = new StringIDCache(stream, Create(destVersion));

            var destResources = new ResourceDataManager();
            destResources.LoadCachesFromDirectory(destDir.FullName);

            var srcResources = new ResourceDataManager();
            srcResources.LoadCachesFromDirectory(Info.CacheFile.DirectoryName);

            var destSerializer = new TagSerializer(destVersion);
            var destDeserializer = new TagDeserializer(destVersion);

            var destInfo = new OpenTagCache
            {
                Cache = destTagCache,
                CacheFile = destTagsFile,
                StringIDs = destStringIDCache,
                StringIDsFile = destStringIDsFile,
                Version = destVersion,
                Serializer = destSerializer,
                Deserializer = destDeserializer
            };

            using (Stream srcStream = Info.OpenCacheRead(), destStream = destInfo.OpenCacheReadWrite())
            {
                var maxDependency = dependencies.Keys.Max();
                for (var i = 0; i <= maxDependency; i++)
                {
                    var srcTag = Info.Cache.Tags[i];
                    if (srcTag == null)
                    {
                        destInfo.Cache.AllocateTag();
                        continue;
                    }

                    var srcData = Info.Cache.ExtractTagRaw(srcStream, srcTag);

                    var destTag = destInfo.Cache.AllocateTag(srcTag.Group);
                    destInfo.Cache.SetTagDataRaw(destStream, destTag, srcData);

                    srcData = new byte[0];
                }
            }
            
            WriteLine($"Done generating cache files in \"{destDir.FullName}\".");

            return true;
        }