public override bool Execute(List<string> args)
        {
            if (args.Count != 1)
                return false;

            var outDir = args[0];
            Directory.CreateDirectory(outDir);

            Console.WriteLine("Loading resource caches...");
            var resourceManager = new ResourceDataManager();
            try
            {
                resourceManager.LoadCachesFromDirectory(_info.CacheFile.DirectoryName);
            }
            catch
            {
                Console.WriteLine("Unable to load the resource .dat files.");
                Console.WriteLine("Make sure that they all exist and are valid.");
                return true;
            }

            var extractor = new BitmapDdsExtractor(resourceManager);
            var count = 0;
            using (var tagsStream = _info.OpenCacheRead())
            {
                foreach (var tag in _info.Cache.Tags.FindAllInGroup("bitm"))
                {
                    Console.Write("Extracting ");
                    TagPrinter.PrintTagShort(tag);

                    try
                    {
                        var tagContext = new TagSerializationContext(tagsStream, _info.Cache, _info.StringIds, tag);
                        var bitmap = _info.Deserializer.Deserialize<TagStructures.Bitmap>(tagContext);
                        var ddsOutDir = outDir;
                        if (bitmap.Images.Count > 1)
                        {
                            ddsOutDir = Path.Combine(outDir, tag.Index.ToString("X8"));
                            Directory.CreateDirectory(ddsOutDir);
                        }
                        for (var i = 0; i < bitmap.Images.Count; i++)
                        {
                            var outPath = Path.Combine(ddsOutDir,
                                ((bitmap.Images.Count > 1) ? i.ToString() : tag.Index.ToString("X8")) + ".dds");
                            using (var outStream = File.Open(outPath, FileMode.Create, FileAccess.Write))
                            {
                                extractor.ExtractDds(_info.Deserializer, bitmap, i, outStream);
                            }
                        }
                        count++;
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("ERROR: Failed to extract bitmap: " + ex.Message);
                    }
                }
            }
            Console.WriteLine("Extracted {0} bitmaps.", count);
            return true;
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 1)
                return false;

            var destinationTag = ArgumentParser.ParseTagIndex(Info.Cache, args[0]);

            Scenario destinationScenario = null;

            using (var cacheStream = Info.CacheFile.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                var scenarioContext = new TagSerializationContext(cacheStream, Info.Cache, Info.StringIds, destinationTag);
                destinationScenario = Info.Deserializer.Deserialize<Scenario>(scenarioContext);
            }

            destinationScenario.SandboxBudget = Definition.SandboxBudget;
            destinationScenario.SandboxEquipment = Definition.SandboxEquipment;
            destinationScenario.SandboxGoalObjects = Definition.SandboxGoalObjects;
            destinationScenario.SandboxScenery = Definition.SandboxScenery;
            destinationScenario.SandboxSpawning = Definition.SandboxSpawning;
            destinationScenario.SandboxTeleporters = Definition.SandboxTeleporters;
            destinationScenario.SandboxVehicles = Definition.SandboxVehicles;
            destinationScenario.SandboxWeapons = Definition.SandboxWeapons;

            using (var cacheStream = Info.CacheFile.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                var scenarioContext = new TagSerializationContext(cacheStream, Info.Cache, Info.StringIds, destinationTag);
                Info.Serializer.Serialize(scenarioContext, destinationScenario);
            }

            return true;
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 0)
                return false;

            foreach (var property in Definition.ShaderProperties)
            {
                RenderMethodTemplate template = null;

                using (var cacheStream = Info.CacheFile.Open(FileMode.Open, FileAccess.Read))
                {
                    var context = new TagSerializationContext(cacheStream, Info.Cache, Info.StringIds, property.Template);
                    template = Info.Deserializer.Deserialize<RenderMethodTemplate>(context);
                }

                for (var i = 0; i < template.ShaderMaps.Count; i++)
                {
                    var mapTemplate = template.ShaderMaps[i];

                    Console.WriteLine($"Bitmap {i} ({Info.StringIds.GetString(mapTemplate.Name)}): {property.ShaderMaps[i].Bitmap.Group.Tag} 0x{property.ShaderMaps[i].Bitmap.Index:X4}");
                }
            }

            return true;
        }
        public static void Populate(CommandContext context, OpenTagCache info, TagInstance tag)
        {
            RenderMethod renderMethod = null;

            using (var cacheStream = info.OpenCacheReadWrite())
            {
                var tagContext = new TagSerializationContext(cacheStream, info.Cache, info.StringIds, tag);

                switch (tag.Group.Tag.ToString())
                {
                    case "rm  ": // render_method
                        renderMethod = info.Deserializer.Deserialize<RenderMethod>(tagContext);
                        break;

                    case "rmsh": // shader
                        renderMethod = info.Deserializer.Deserialize<Shader>(tagContext);
                        break;

                    case "rmd ": // shader_decal
                        renderMethod = info.Deserializer.Deserialize<ShaderDecal>(tagContext);
                        break;

                    case "rmfl": // shader_foliage
                        renderMethod = info.Deserializer.Deserialize<ShaderFoliage>(tagContext);
                        break;

                    case "rmhg": // shader_halogram
                        renderMethod = info.Deserializer.Deserialize<ShaderHalogram>(tagContext);
                        break;

                    case "rmss": // shader_screen
                        renderMethod = info.Deserializer.Deserialize<ShaderScreen>(tagContext);
                        break;

                    case "rmtr": // shader_terrain
                        renderMethod = info.Deserializer.Deserialize<ShaderTerrain>(tagContext);
                        break;

                    case "rmw ": // shader_water
                        renderMethod = info.Deserializer.Deserialize<ShaderWater>(tagContext);
                        break;

                    case "rmzo": // shader_zonly
                        renderMethod = info.Deserializer.Deserialize<ShaderZonly>(tagContext);
                        break;

                    case "rmcs": // shader_custom
                        renderMethod = info.Deserializer.Deserialize<ShaderCustom>(tagContext);
                        break;

                    default:
                        throw new NotImplementedException();
                }
            }
            
            context.AddCommand(new ListArgumentsCommand(info, tag, renderMethod));
            context.AddCommand(new ListBitmapsCommand(info, tag, renderMethod));
            context.AddCommand(new SpecifyBitmapsCommand(info, tag, renderMethod));
        }
        private void SetRenderModelName(Stream stream, TagInstance tag, ref Dictionary<int, string> tagNames)
        {
            if (tagNames.ContainsKey(tag.Index))
                return;

            var context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, tag);
            var definition = Info.Deserializer.Deserialize<RenderModel>(context);
            tagNames[tag.Index] = $"{Info.StringIds.GetString(definition.Name)}";
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 1)
                return false;
            var imagePath = args[0];

            Console.WriteLine("Loading textures.dat...");
            var resourceManager = new ResourceDataManager();
            resourceManager.LoadCacheFromDirectory(_info.CacheFile.DirectoryName, ResourceLocation.Textures);

            Console.WriteLine("Importing image...");
            var bitmap = new TagStructures.Bitmap
            {
                Flags = TagStructures.Bitmap.RuntimeFlags.UseResource,
                Sequences = new List<TagStructures.Bitmap.Sequence>
                {
                    new TagStructures.Bitmap.Sequence
                    {
                        FirstBitmapIndex = 0,
                        BitmapCount = 1
                    }
                },
                Images = new List<TagStructures.Bitmap.Image>
                {
                    new TagStructures.Bitmap.Image
                    {
                        Signature = new Tag("bitm").Value,
                        Unknown28 = -1,
                    }
                },
                Resources = new List<TagStructures.Bitmap.BitmapResource>
                {
                    new TagStructures.Bitmap.BitmapResource()
                }
            };
            using (var imageStream = File.OpenRead(imagePath))
            {
                var injector = new BitmapDdsInjector(resourceManager);
                injector.InjectDds(_info.Serializer, _info.Deserializer, bitmap, 0, imageStream);
            }

            Console.WriteLine("Creating a new tag...");
            var tag = _info.Cache.AllocateTag();
            using (var tagsStream = _info.OpenCacheReadWrite())
            {
                var tagContext = new TagSerializationContext(tagsStream, _info.Cache, _info.StringIds, tag);
                _info.Serializer.Serialize(tagContext, bitmap);
            }

            Console.WriteLine();
            Console.WriteLine("All done! The new bitmap tag is:");
            TagPrinter.PrintTagShort(tag);
            return true;
        }
        public override bool Execute(List<string> args)
        {
            using (var stream = Info.OpenCacheReadWrite())
            {
                var context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, Tag);
                Info.Serializer.Serialize(context, Value);
            }

            Console.WriteLine("Done!");

            return true;
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 2)
                return false;
            int imageIndex;
            if (!int.TryParse(args[0], NumberStyles.HexNumber, null, out imageIndex))
                return false;
            if (imageIndex < 0 || imageIndex >= _bitmap.Images.Count)
            {
                Console.Error.WriteLine("Invalid image index.");
                return true;
            }
            var imagePath = args[1];

            Console.WriteLine("Loading resource caches...");
            var resourceManager = new ResourceDataManager();
            try
            {
                resourceManager.LoadCachesFromDirectory(_info.CacheFile.DirectoryName);
            }
            catch
            {
                Console.WriteLine("Unable to load the resource .dat files.");
                Console.WriteLine("Make sure that they all exist and are valid.");
                return true;
            }
            Console.WriteLine("Importing image data...");
            try
            {
                using (var imageStream = File.OpenRead(imagePath))
                {
                    var injector = new BitmapDdsInjector(resourceManager);
                    injector.InjectDds(_info.Serializer, _info.Deserializer, _bitmap, imageIndex, imageStream);
                }
                using (var tagsStream = _info.OpenCacheReadWrite())
                {
                    var tagContext = new TagSerializationContext(tagsStream, _info.Cache, _tag);
                    _info.Serializer.Serialize(tagContext, _bitmap);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Importing image data failed: " + ex.Message);
                return true;
            }
            Console.WriteLine("Done!");
            return true;
        }
        public override bool Execute(List<string> args)
        {
            foreach (var property in Definition.ShaderProperties)
            {
                RenderMethodTemplate template = null;

                using (var cacheStream = Info.CacheFile.Open(FileMode.Open, FileAccess.Read))
                {
                    var context = new TagSerializationContext(cacheStream, Info.Cache, Info.StringIds, property.Template);
                    template = Info.Deserializer.Deserialize<RenderMethodTemplate>(context);
                }

                for (var i = 0; i < template.Arguments.Count; i++)
                {
                    Console.WriteLine("");

                    var argumentName = Info.StringIds.GetString(template.Arguments[i].Name);
                    var argumentValue = new Vector4(
                        property.Arguments[i].Arg1,
                        property.Arguments[i].Arg2,
                        property.Arguments[i].Arg3,
                        property.Arguments[i].Arg4);

                    Console.WriteLine(string.Format("{0}:", argumentName));

                    if (argumentName.EndsWith("_map"))
                    {
                        Console.WriteLine(string.Format("\tX Scale: {0}", argumentValue.X));
                        Console.WriteLine(string.Format("\tY Scale: {0}", argumentValue.Y));
                        Console.WriteLine(string.Format("\tX Offset: {0}", argumentValue.Z));
                        Console.WriteLine(string.Format("\tY Offset: {0}", argumentValue.W));
                    }
                    else
                    {
                        Console.WriteLine(string.Format("\tX: {0}", argumentValue.X));
                        Console.WriteLine(string.Format("\tY: {0}", argumentValue.Y));
                        Console.WriteLine(string.Format("\tZ: {0}", argumentValue.Z));
                        Console.WriteLine(string.Format("\tW: {0}", argumentValue.W));
                    }
                }
            }

            return true;
        }
        public override bool Execute(List<string> args)
        {
            foreach (var material in Definition.Materials)
            {
                Console.Write("Please enter the replacement {0:X8} index: ",
                    material.RenderMethod.Index);

                material.RenderMethod = ArgumentParser.ParseTagIndex(Info.Cache, Console.ReadLine());
            }

            using (var cacheStream = Info.CacheFile.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                var context = new TagSerializationContext(cacheStream, Info.Cache, Info.StringIds, Tag);
                Info.Serializer.Serialize(context, Definition);
            }

            Console.WriteLine("Done!");

            return true;
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 0)
                return false;
            
            var shaderMaps = new Dictionary<StringId, TagInstance>();

            foreach (var property in Definition.ShaderProperties)
            {
                RenderMethodTemplate template = null;

                using (var cacheStream = Info.CacheFile.Open(FileMode.Open, FileAccess.Read))
                {
                    var context = new TagSerializationContext(cacheStream, Info.Cache, Info.StringIds, property.Template);
                    template = Info.Deserializer.Deserialize<RenderMethodTemplate>(context);
                }

                for (var i = 0; i < template.ShaderMaps.Count; i++)
                {
                    var mapTemplate = template.ShaderMaps[i];

                    Console.Write(string.Format("Please enter the {0} index: ", Info.StringIds.GetString(mapTemplate.Name)));
                    shaderMaps[mapTemplate.Name] = ArgumentParser.ParseTagIndex(Info.Cache, Console.ReadLine());
                    property.ShaderMaps[i].Bitmap = shaderMaps[mapTemplate.Name];
                }
            }

            foreach (var import in Definition.ImportData)
                if (shaderMaps.ContainsKey(import.MaterialType))
                    import.Bitmap = shaderMaps[import.MaterialType];

            using (var cacheStream = Info.CacheFile.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                var context = new TagSerializationContext(cacheStream, Info.Cache, Info.StringIds, Tag);
                Info.Serializer.Serialize(context, Definition);
            }

            Console.WriteLine("Done!");

            return true;
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 1)
                return false;

            var builder = new RenderModelBuilder(_info.Version);

            // Add a root node
            var node = builder.AddNode(new RenderModel.Node
            {
                Name = _stringIds.GetStringId("street_cone"),
                ParentNode = -1,
                FirstChildNode = -1,
                NextSiblingNode = -1,
                DefaultRotation = new Vector4(0, 0, 0, -1),
                DefaultScale = 1,
                InverseForward = new Vector3(1, 0, 0),
                InverseLeft = new Vector3(0, 1, 0),
                InverseUp = new Vector3(0, 0, 1),
            });

            // Begin building the default region and permutation
            builder.BeginRegion(_stringIds.GetStringId("default"));
            builder.BeginPermutation(_stringIds.GetStringId("default"));

            using (var importer = new AssimpContext())
            {
                Scene model;
                using (var logStream = new LogStream((msg, userData) => Console.WriteLine(msg)))
                {
                    logStream.Attach();
                    model = importer.ImportFile(args[0],
                        PostProcessSteps.CalculateTangentSpace |
                        PostProcessSteps.GenerateNormals |
                        PostProcessSteps.JoinIdenticalVertices |
                        PostProcessSteps.SortByPrimitiveType |
                        PostProcessSteps.PreTransformVertices |
                        PostProcessSteps.Triangulate);
                    logStream.Detach();
                }

                Console.WriteLine("Assembling vertices...");

                // Build a multipart mesh from the model data,
                // with each model mesh mapping to a part of one large mesh and having its own material
                builder.BeginMesh();
                ushort partStartVertex = 0;
                ushort partStartIndex = 0;
                var vertices = new List<RigidVertex>();
                var indices = new List<ushort>();
                foreach (var mesh in model.Meshes)
                {
                    for (var i = 0; i < mesh.VertexCount; i++)
                    {
                        var position = mesh.Vertices[i];
                        var normal = mesh.Normals[i];
                        var uv = mesh.TextureCoordinateChannels[0][i];
                        var tangent = mesh.Tangents[i];
                        var bitangent = mesh.BiTangents[i];
                        vertices.Add(new RigidVertex
                        {
                            Position = new Vector4(position.X, position.Y, position.Z, 1),
                            Normal = new Vector3(normal.X, normal.Y, normal.Z),
                            Texcoord = new Vector2(uv.X, uv.Y),
                            Tangent = new Vector4(tangent.X, tangent.Y, tangent.Z, 1),
                            Binormal = new Vector3(bitangent.X, bitangent.Y, bitangent.Z),
                        });
                    }

                    // Build the index buffer
                    var meshIndices = mesh.GetIndices();
                    indices.AddRange(meshIndices.Select(i => (ushort)(i + partStartVertex)));

                    // Define a material and part for this mesh
                    var material = builder.AddMaterial(new RenderMaterial
                    {
                        RenderMethod = _cache.Tags[0x101F],
                    });
                    builder.DefinePart(material, partStartIndex, (ushort)meshIndices.Length, (ushort)mesh.VertexCount);

                    // Move to the next part
                    partStartVertex += (ushort)mesh.VertexCount;
                    partStartIndex += (ushort)meshIndices.Length;
                }

                // Bind the vertex and index buffers
                builder.BindRigidVertexBuffer(vertices, node);
                builder.BindIndexBuffer(indices, PrimitiveType.TriangleList);
                builder.EndMesh();
            }

            builder.EndPermutation();
            builder.EndRegion();

            Console.WriteLine("Building Blam mesh data...");

            var resourceStream = new MemoryStream();
            var renderModel = builder.Build(_info.Serializer, resourceStream);

            Console.WriteLine("Writing resource data...");

            // Add a new resource for the model data
            var resources = new ResourceDataManager();
            resources.LoadCachesFromDirectory(_fileInfo.DirectoryName);
            resourceStream.Position = 0;
            resources.Add(renderModel.Geometry.Resource, ResourceLocation.Resources, resourceStream);

            Console.WriteLine("Writing tag data...");

            using (var cacheStream = _fileInfo.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                var tag = _cache.Tags[0x3317];
                var context = new TagSerializationContext(cacheStream, _cache, _stringIds, tag);
                _info.Serializer.Serialize(context, renderModel);
            }
            Console.WriteLine("Model imported successfully!");
            return true;
        }
        /// <summary>
        /// Finds the Model referenced by the tag.
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        public Model getModel(TagInstance tag)
        {
            if (tag == null)
                return null;

            using (var cacheStream = _info.CacheFile.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                var tagContext = new TagSerializationContext(cacheStream, _info.Cache, _info.StringIds, tag);
                TagInstance model = null;
                GameObject obj = null;
                obj = (GameObject)_info.Deserializer.Deserialize(tagContext, TagStructureTypes.FindByGroupTag(tag.Group.ToString()));
                if (obj == null)
                {
                    Console.WriteLine("Could not get GameObject from TagInstance: " + tag.Group.ToString());
                    return null;
                }
                model = obj.Model;
                if (model == null) {
                    return null; //Some obje tags, such as some weapons for vehicles can have no hlmt (Model)
                }
                tagContext = new TagSerializationContext(cacheStream, _info.Cache, _info.StringIds, model);
                return (tagContext!=null)?_info.Deserializer.Deserialize<Model>(tagContext):null;
            }
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 2)
                return false;

            int imageIndex;
            if (!int.TryParse(args[0], NumberStyles.HexNumber, null, out imageIndex))
                return false;

            if (imageIndex < 0 || imageIndex >= Definition.Images.Count)
            {
                Console.WriteLine("Invalid image index.");
                return true;
            }

            var imagePath = args[1];

            Console.WriteLine("Loading resource caches...");
            var resourceManager = new ResourceDataManager();

            try
            {
                resourceManager.LoadCachesFromDirectory(Info.CacheFile.DirectoryName);
            }
            catch
            {
                Console.WriteLine("Unable to load the resource .dat files.");
                Console.WriteLine("Make sure that they all exist and are valid.");
                return true;
            }

            Console.WriteLine("Importing image data...");

            try
            {
                Definition = new Bitmap
                {
                    Flags = Bitmap.RuntimeFlags.UseResource,
                    Sequences = new List<Bitmap.Sequence>
                    {
                        new Bitmap.Sequence
                        {
                            FirstBitmapIndex = 0,
                            BitmapCount = 1
                        }
                    },
                    Images = new List<Bitmap.Image>
                    {
                        new Bitmap.Image
                        {
                            Signature = new Tag("bitm").Value,
                            Unknown28 = -1
                        }
                    },
                    Resources = new List<Bitmap.BitmapResource>
                    {
                        new Bitmap.BitmapResource()
                    }
                };

                using (var imageStream = File.OpenRead(imagePath))
                {
                    var injector = new BitmapDdsInjector(resourceManager);
                    injector.InjectDds(Info.Serializer, Info.Deserializer, Definition, imageIndex, imageStream);
                }

                using (var tagsStream = Info.OpenCacheReadWrite())
                {
                    var tagContext = new TagSerializationContext(tagsStream, Info.Cache, Info.StringIds, Tag);
                    Info.Serializer.Serialize(tagContext, Definition);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Importing image data failed: " + ex.Message);
                return true;
            }

            Console.WriteLine("Done!");
            return true;
        }
        private void SetModelName(Stream stream, TagInstance tag, ref Dictionary<int, string> tagNames)
        {
            if (tag == null || tagNames.ContainsKey(tag.Index))
                return;

            var context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, tag);
            var definition = Info.Deserializer.Deserialize<Model>(context);

            if (definition.RenderModel == null)
                return;

            SetRenderModelName(stream, definition.RenderModel, ref tagNames);

            var tagName = tagNames[definition.RenderModel.Index];

            if (tagName.StartsWith("0x"))
                tagName = $"0x{tag.Index:X4}";

            tagNames[tag.Index] = tagName;

            if (definition.CollisionModel != null && !tagName.StartsWith("0x"))
                tagNames[definition.CollisionModel.Index] = $"{tagName}";

            if (definition.Animation != null && !tagName.StartsWith("0x"))
                tagNames[definition.Animation.Index] = $"{tagName}";
        }
 public TagDataBlock(TagSerializationContext context)
 {
     _context = context;
     Stream   = new MemoryStream();
     Writer   = new BinaryWriter(Stream);
 }
 public TagDataBlock(TagSerializationContext context)
 {
     _context = context;
     Stream = new MemoryStream();
     Writer = new BinaryWriter(Stream);
 }
        public override bool Execute(List<string> args)
        {
            if (args.Count < 2)
                return false;
            var outputPath = args[0];

            // Load each file and do version detection
            var infos = new List<OpenTagCache>();
            foreach (var path in args.Skip(1))
            {
                Console.WriteLine("Loading {0}...", path);

                // Load the cache file
                var info = new OpenTagCache { CacheFile = new FileInfo(path) };
                using (var stream = info.OpenCacheRead())
                    info.Cache = new TagCache(stream);

                // Do version detection, and don't accept the closest version
                // because that might not work
                EngineVersion closestVersion;
                info.Version = VersionDetection.DetectVersion(info.Cache, out closestVersion);
                if (info.Version == EngineVersion.Unknown)
                {
                    Console.WriteLine("- Unrecognized version! Ignoring.");
                    continue;
                }
                info.Deserializer = new TagDeserializer(info.Version);
                infos.Add(info);
            }

            var result = new TagVersionMap();
            using (var baseStream = _info.OpenCacheRead())
            {
                // Get the scenario tags for this cache
                Console.WriteLine("Finding base scenario tags...");
                var baseScenarios = FindScenarios(_info, baseStream);
                var baseVersion = _info.Version;
                var baseTagData = new Dictionary<int, object>();
                foreach (var scenario in baseScenarios)
                    baseTagData[scenario.Tag.Index] = scenario.Data;

                // Now compare with each of the other caches
                foreach (var info in infos)
                {
                    using (var stream = info.OpenCacheRead())
                    {
                        Console.WriteLine("Finding scenario tags in {0}...", info.CacheFile.FullName);

                        // Get the scenario tags and connect them to the base tags
                        var scenarios = FindScenarios(info, stream);
                        var tagsToCompare = new Queue<QueuedTag>();
                        for (var i = 0; i < scenarios.Count; i++)
                        {
                            tagsToCompare.Enqueue(scenarios[i]);
                            result.Add(baseVersion, baseScenarios[i].Tag.Index, info.Version, scenarios[i].Tag.Index);
                        }

                        // Process each tag in the queue, enqueuing all of its dependencies as well
                        while (tagsToCompare.Count > 0)
                        {
                            // Get the tag and its data
                            var tag = tagsToCompare.Dequeue();
                            TagPrinter.PrintTagShort(tag.Tag);
                            var data = tag.Data;
                            if (data == null)
                            {
                                // No data yet - deserialize it
                                var context = new TagSerializationContext(stream, info.Cache, info.StringIds, tag.Tag);
                                var type = TagStructureTypes.FindByGroupTag(tag.Tag.Group.Tag);
                                data = info.Deserializer.Deserialize(context, type);
                            }

                            // Now get the data for the base tag
                            var baseTag = result.Translate(info.Version, tag.Tag.Index, baseVersion);
                            if (baseTag == -1 || _info.Cache.Tags[baseTag].Group.Tag != tag.Tag.Group.Tag)
                                continue;
                            object baseData;
                            if (!baseTagData.TryGetValue(baseTag, out baseData))
                            {
                                // No data yet - deserialize it
                                var context = new TagSerializationContext(baseStream, _info.Cache, _info.StringIds, _info.Cache.Tags[baseTag]);
                                var type = TagStructureTypes.FindByGroupTag(tag.Tag.Group.Tag);
                                baseData = _info.Deserializer.Deserialize(context, type);
                                baseTagData[baseTag] = baseData;
                            }

                            // Compare the two blocks
                            CompareBlocks(baseData, baseVersion, data, info.Version, result, tagsToCompare);
                        }
                    }
                }
            }

            // Write out the CSV
            Console.WriteLine("Writing results...");
            using (var writer = new StreamWriter(File.Open(outputPath, FileMode.Create, FileAccess.Write)))
                result.WriteCsv(writer);

            Console.WriteLine("Done!");
            return true;
        }
        private static List<QueuedTag> FindScenarios(OpenTagCache info, Stream stream)
        {
            // Get a dictionary of scenarios by map ID
            var scenarios = new Dictionary<int, QueuedTag>();
            foreach (var scenarioTag in info.Cache.Tags.FindAllInGroup("scnr"))
            {
                var context = new TagSerializationContext(stream, info.Cache, info.StringIds, scenarioTag);
                var scenario = info.Deserializer.Deserialize<Scenario>(context);
                scenarios[scenario.MapId] = new QueuedTag { Tag = scenarioTag, Data = scenario };
            }
            
            var tags = from id in MapIdsToCompare
                       where scenarios.ContainsKey(id)
                       select scenarios[id];

            return tags.ToList();
        }
        //Problems:
        // 1. (FIXED) Models with no variant named 'default' are not exported
        // 2. (FIXED) Model scale parameters, including scaling of nodes are not applied
        // 3. Some maps have a shit load of unusable placement slots
        public override bool Execute(List<string> args)
        {
            if (args.Count < 2)
            {
                return false;
            }
            if (!Directory.Exists(args[1]))
            {
                Console.WriteLine("<dirpath> was not a valid path of an existing directory.");
                return false;
            }

            int scnrIndex;
            using (var mapReader = new BinaryReader(File.OpenRead(args[0])))
            {
                if (mapReader.ReadInt32() != new Tag("head").Value)
                {
                    Console.Error.WriteLine("Invalid map file");
                    return false;
                }
                mapReader.BaseStream.Position = 0x2DF0;
                scnrIndex = mapReader.ReadInt32();
            }
            //Get models from scenario
            List<Tuple<Model, int>> models = new List<Tuple<Model, int>>();
            Scenario scenario = null;
            TagInstance scnrTag = null;
            using (var cacheStream = _info.CacheFile.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                scnrTag = _info.Cache.Tags[scnrIndex]; //ArgumentParser.ParseTagIndex(_info.Cache, );
                var scenarioContext = new TagSerializationContext(cacheStream, _info.Cache, _info.StringIds, scnrTag);
                scenario = _info.Deserializer.Deserialize<Scenario>(scenarioContext);
            }
            //Scan dependencies for models of gameobjects
            IEnumerable<TagInstance> dependencies = _info.Cache.Tags.FindDependencies(scnrTag);
            int n_deps = 0, n_obje = 0;
            try {
                foreach (TagInstance tag in dependencies)
                {
                    if (tag == null) {
                        Console.WriteLine("null tag");
                        continue;
                    }
                    //tags that are descendants of "obje" have a hlmt (Model).
                    if (tag.IsInGroup(new Tag("obje")))
                    {
                        Console.WriteLine("Getting model for tag: " + tag);
                        Model m = getModel(tag);
                        if (m == null) {
                            Console.WriteLine("model was null");
                            continue;
                        }
                        models.Add(new Tuple<Model, int>(m, tag.Index));
                        n_obje++;
                    }
                    n_deps++;
                }
            }
            catch (Exception e) {
                Console.WriteLine(e.StackTrace);
                Console.WriteLine("Problem encountered with tag: {0:X8}", dependencies.ElementAt(n_deps).Index);

            }
            Console.WriteLine("scanned {0}/{1} deps. Extracted {2} gameobject descendent tags", n_deps, dependencies.Count(), n_obje);

            //Make a subdirectory to put the model files into
            string outdir = Directory.CreateDirectory(args[1]
                + "/" + Path.GetFileNameWithoutExtension(args[0])).FullName;

            //Get the sbsp model (collision-model).

            //    This has been intentionally commented due to incompatibility
            //    with larger bsps (that have more than 65536 planes). It will
            //    work however with simpler maps (turf, guardian, edge..) although
            //    some models may appear to be incomplete.

            /*
            ScenarioStructureBsp sbsp = null;
            using (var cacheStream = _info.CacheFile.Open(FileMode.Open, FileAccess.ReadWrite))
            {
                Console.WriteLine("getting sbsp");
                TagInstance sbspTag = scenario.StructureBsps[0].StructureBsp2;
                var sbspContext = new TagSerializationContext(cacheStream, _info.Cache, _info.StringIds, sbspTag);
                sbsp = _info.Deserializer.Deserialize<ScenarioStructureBsp>(sbspContext);
                Console.WriteLine("deserialized sbsp");
            }

            if (sbsp == null) {
                Console.WriteLine("SBSP tag ref was NULL");
                return false;
            }
            try
            {
                string fname = Path.GetFileNameWithoutExtension(args[0]) + ".obj";
                CollisionModel.Region.Permutation.Bsp bsp = BSPUtils.fromSbsp(sbsp, _info);
                BSPUtils.toOBJ(bsp, outdir + "/" +  fname);
            }
            catch(Exception e) {
                Console.WriteLine(e.StackTrace);
            }
            *///End of commented sbsp code

            foreach (Tuple<Model, int> t in models)
            {
                string variant = "default";
                if (t.Item1.Variants.Count > 0)
                    variant = _info.StringIds.GetString(t.Item1.Variants[0].Name);
                Console.WriteLine("First variant is: {0}", variant);
                ExtractModelCommand c = new ExtractModelCommand(_info, t.Item1);
                c.Execute(new List<string>(new string[] {variant,
                    "obj", String.Format("{0}/{1:X8}.obj", outdir, t.Item2)}));
            }
            Console.WriteLine("output to: " + outdir);
            return true;
        }
        public override bool Execute(List<string> args)
        {
            //Arguments needed: filepath, <new>|<tagIndex>
            if (args.Count < 2)
            {
                return false;
            }

            TagInstance tag = null;
            bool b_duplicate;

            // optional argument: forces overwriting of tags that are not type: coll
            var b_force = (args.Count >= 3 && args[2].ToLower().Equals("force"));

            if (args[1].ToLower().Equals("new"))
            {
                b_duplicate = true;
            }
            else
            {
                tag = ArgumentParser.ParseTagIndex(Info.Cache, args[1]);
                if (tag == null)
                {
                    return false;
                }
                b_duplicate = false;
            }

            if (!b_force && !b_duplicate && !tag.IsInGroup("coll"))
            {
                Console.WriteLine("Tag to override was not of class- 'coll'. Use third argument- 'force' to inject into this tag.");
                return false;
            }

            string filepath = args[0];
            string[] fpaths = null;
            CollisionModel coll = null;
            bool b_singleFile = Path.GetExtension(filepath).Equals(".model_collision_geometry")
                && !Directory.Exists(filepath);

            var modelbuilder = new CollisionGeometryBuilder();
            int n_objects = 1;

            if (!b_singleFile)
            {
                fpaths = Directory.GetFiles(filepath, "*.model_collision_geometry");

                if (fpaths.Length == 0)
                {
                    Console.WriteLine("No Halo 1 coll tags in directory: \"{0}\"", filepath);
                    return false;
                }

                filepath = fpaths[0];
                n_objects = fpaths.Length;
            }

            Console.WriteLine(
                (n_objects == 1 ? "Loading coll tag..." : "Loading coll tags..."), n_objects);

            if (!modelbuilder.ParseFromFile(filepath))
                return false;

            coll = modelbuilder.Build();

            if (coll == null)
            {
                Console.WriteLine("Builder produced null result.");
                return false;
            }

            if (!b_singleFile)
            {
                for (int i = 1; i < fpaths.Length; ++i)
                {
                    if (!modelbuilder.ParseFromFile(fpaths[i]))
                        return false;

                    var coll2 = modelbuilder.Build();

                    if (coll2 == null)
                    {
                        Console.WriteLine("Builder produced null result.");
                        return false;
                    }

                    coll.Regions.Add(coll2.Regions[0]);
                }
            }

            using (var stream = Info.OpenCacheReadWrite())
            {

                if (b_duplicate)
                {
                    //duplicate an existing tag, trashcan phmo
                    tag = Info.Cache.DuplicateTag(stream, Info.Cache.Tags[0x4436]);
                    if (tag == null)
                    {
                        Console.WriteLine("Failed tag duplication.");
                        return false;
                    }
                }

                var context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, tag);
                Info.Serializer.Serialize(context, coll);

            }
            Console.WriteLine(
                (n_objects == 1 ? "Added 1 collision." : "Added {0} collisions in one tag."), n_objects);
            Console.Write("Successfully imported coll to: ");
            TagPrinter.PrintTagShort(tag);

            return true;
        }
        private void SetGameObjectName(Stream stream, TagInstance tag, ref Dictionary<int, string> tagNames)
        {
            var context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, tag);
            var definition = (GameObject)Info.Deserializer.Deserialize(context, TagStructureTypes.FindByGroupTag(tag.Group.Tag));

            if (definition.Model == null)
                return;

            context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, definition.Model);
            var modelDefinition = Info.Deserializer.Deserialize<Model>(context);

            if (modelDefinition.RenderModel == null)
                return;

            context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, modelDefinition.RenderModel);
            var renderModelDefinition = Info.Deserializer.Deserialize<RenderModel>(context);

            var objectName = Info.StringIds.GetString(renderModelDefinition.Name);

            if (tag.Group.Tag == new Tag("bipd"))
            {
                var biped = (Biped)definition;

                var isMultiplayer = objectName.StartsWith("mp_");
                var isMonitor = objectName.StartsWith("monitor");

                var objectRootName = isMultiplayer ?
                    objectName.Substring(3) :
                    objectName;

                var objectGenericName = $"objects\\characters\\{objectRootName}\\{objectRootName}";

                if (objectRootName != objectName)
                    objectName = $"objects\\characters\\{objectRootName}\\{objectName}\\{objectName}";
                else if (isMonitor)
                    objectName = $"{objectGenericName}_editor";
                else
                    objectName = objectGenericName;

                tagNames[definition.Model.Index] = objectName;

                if (modelDefinition.RenderModel != null)
                    tagNames[modelDefinition.RenderModel.Index] = objectName;

                if (modelDefinition.CollisionModel != null)
                    tagNames[modelDefinition.CollisionModel.Index] = objectGenericName;

                if (modelDefinition.PhysicsModel != null)
                    tagNames[modelDefinition.PhysicsModel.Index] = objectGenericName;

                if (modelDefinition.Animation != null)
                    tagNames[modelDefinition.Animation.Index] = objectGenericName;

                if (biped.CollisionDamage != null && !tagNames.ContainsKey(biped.CollisionDamage.Index))
                    tagNames[biped.CollisionDamage.Index] = isMonitor ?
                        "globals\\collision_damage\\invulnerable_harmless" :
                        "globals\\collision_damage\\biped_player";

                if (biped.MaterialEffects != null && !tagNames.ContainsKey(biped.MaterialEffects.Index))
                    tagNames[biped.MaterialEffects.Index] =
                        $"fx\\material_effects\\objects\\characters\\{objectRootName}";

                if (biped.MeleeImpact != null && !tagNames.ContainsKey(biped.MeleeImpact.Index))
                    tagNames[biped.MeleeImpact.Index] =
                        "sounds\\materials\\soft\\organic_flesh\\melee_impact";

                if (biped.CameraTracks.Count != 0 && biped.CameraTracks[0].Track != null && !tagNames.ContainsKey(biped.CameraTracks[0].Track.Index))
                    tagNames[biped.CameraTracks[0].Track.Index] = isMonitor ?
                        "camera\\biped_follow_camera" :
                        "camera\\biped_support_camera";

                if (biped.MeleeDamage != null && !tagNames.ContainsKey(biped.MeleeDamage.Index))
                    tagNames[biped.MeleeDamage.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_melee";

                if (biped.BoardingMeleeDamage != null && !tagNames.ContainsKey(biped.BoardingMeleeDamage.Index))
                    tagNames[biped.BoardingMeleeDamage.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_boarding_melee";

                if (biped.BoardingMeleeResponse != null && !tagNames.ContainsKey(biped.BoardingMeleeResponse.Index))
                    tagNames[biped.BoardingMeleeResponse.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_boarding_melee_response";

                if (biped.EjectionMeleeDamage != null && !tagNames.ContainsKey(biped.EjectionMeleeDamage.Index))
                    tagNames[biped.EjectionMeleeDamage.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_ejection_melee";

                if (biped.EjectionMeleeResponse != null && !tagNames.ContainsKey(biped.EjectionMeleeResponse.Index))
                    tagNames[biped.EjectionMeleeResponse.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_ejection_melee_response";

                if (biped.LandingMeleeDamage != null && !tagNames.ContainsKey(biped.LandingMeleeDamage.Index))
                    tagNames[biped.LandingMeleeDamage.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_landing_melee";

                if (biped.FlurryMeleeDamage != null && !tagNames.ContainsKey(biped.FlurryMeleeDamage.Index))
                    tagNames[biped.FlurryMeleeDamage.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_flurry_melee";

                if (biped.ObstacleSmashMeleeDamage != null && !tagNames.ContainsKey(biped.ObstacleSmashMeleeDamage.Index))
                    tagNames[biped.ObstacleSmashMeleeDamage.Index] =
                        $"objects\\characters\\{objectRootName}\\damage_effects\\{objectRootName}_obstacle_smash";

                if (biped.AreaDamageEffect != null && !tagNames.ContainsKey(biped.AreaDamageEffect.Index))
                    tagNames[biped.AreaDamageEffect.Index] =
                        $"fx\\material_effects\\objects\\characters\\contact\\collision\\blood_aoe_{objectRootName}";
            }
            else if (tag.Group.Tag == new Tag("weap"))
            {
                var weapon = (Weapon)definition;

                if (weapon.HudInterface != null && !tagNames.ContainsKey(weapon.HudInterface.Index))
                    tagNames[weapon.HudInterface.Index] = $"ui\\chud\\{objectName}";

                if (weapon.FirstPerson.Count > 0)
                {
                    var spartanJmadTag = weapon.FirstPerson[0].FirstPersonAnimations;
                    if (spartanJmadTag != null)
                        tagNames[spartanJmadTag.Index] = $"objects\\characters\\mp_masterchief\\fp\\weapons\\fp_{objectName}\\fp_{objectName}";
                }

                if (weapon.FirstPerson.Count > 1)
                {
                    var eliteJmadTag = weapon.FirstPerson[1].FirstPersonAnimations;
                    if (eliteJmadTag != null)
                        tagNames[eliteJmadTag.Index] = $"objects\\characters\\mp_elite\\fp\\weapons\\fp_{objectName}\\fp_{objectName}";
                }

                var weaponClassName =
                    // HUNTER WEAPONS
                    objectName.StartsWith("flak_cannon") ?
                        "hunter\\hunter_flak_cannon" :
                    // MELEE WEAPONS
                    objectName.StartsWith("energy_blade") ?
                        "melee\\energy_blade" :
                    objectName.StartsWith("gravity_hammer") ?
                        "melee\\gravity_hammer" :
                    // MULTIPLAYER WEAPONS
                    objectName.StartsWith("assault_bomb") ?
                        "multiplayer\\assault_bomb" :
                    objectName.StartsWith("ball") ?
                        "multiplayer\\ball" :
                    objectName.StartsWith("flag") ?
                        "multiplayer\\flag" :
                    // PISTOL WEAPONS
                    objectName.StartsWith("excavator") ?
                        "pistol\\excavator" :
                    objectName.StartsWith("magnum") ?
                        "pistol\\magnum" :
                    objectName.StartsWith("needler") ?
                        "pistol\\needler" :
                    objectName.StartsWith("plasma_pistol") ?
                        "pistol\\plasma_pistol" :
                    // RIFLE WEAPONS
                    (objectName.StartsWith("assault_rifle") || objectName.StartsWith("ar_variant")) ?
                        "rifle\\assault_rifle" :
                    (objectName.StartsWith("battle_rifle") || objectName.StartsWith("br_variant")) ?
                        "rifle\\battle_rifle" :
                    objectName.StartsWith("beam_rifle") ?
                        "rifle\\beam_rifle" :
                    objectName.StartsWith("covenant_carbine") ?
                        "rifle\\covenant_carbine" :
                    objectName.StartsWith("dmr") ?
                        "rifle\\dmr" :
                    objectName.StartsWith("needle_rifle") ?
                        "rifle\\needle_rifle" :
                    objectName.StartsWith("plasma_rifle") ?
                        "rifle\\plasma_rifle" :
                    objectName.StartsWith("shotgun") ?
                        "rifle\\shotgun" :
                    objectName.StartsWith("smg") ?
                        "rifle\\smg" :
                    objectName.StartsWith("sniper_rifle") ?
                        "rifle\\sniper_rifle" :
                    objectName.StartsWith("spike_rifle") ?
                        "rifle\\spike_rifle" :
                    // SUPPORT WEAPONS
                    objectName.StartsWith("rocket_launcher") ?
                        "support_high\\rocket_launcher" :
                    objectName.StartsWith("spartan_laser") ?
                        "support_high\\spartan_laser" :
                    objectName.StartsWith("brute_shot") ?
                        "support_low\\brute_shot" :
                    objectName.StartsWith("sentinel_gun") ?
                        "support_low\\sentinel_gun" :
                    // OTHER WEAPONS
                    objectName;

                objectName = $"objects\\weapons\\{weaponClassName}\\{objectName}";

                if (objectName.EndsWith("energy_blade") && definition.WaterDensity == GameObject.WaterDensityValue.Default)
                    objectName += "_useless";

                tagNames[definition.Model.Index] = objectName;

                if (modelDefinition.RenderModel != null)
                    tagNames[modelDefinition.RenderModel.Index] = objectName;

                if (modelDefinition.CollisionModel != null)
                    tagNames[modelDefinition.CollisionModel.Index] = objectName;

                if (modelDefinition.PhysicsModel != null)
                    tagNames[modelDefinition.PhysicsModel.Index] = objectName;

                if (modelDefinition.Animation != null)
                    tagNames[modelDefinition.Animation.Index] = objectName;
            }
            else if (tag.Group.Tag == new Tag("eqip"))
            {
                var equipment = (Equipment)definition;

                var equipmentClassName =
                    (objectName.StartsWith("health_pack") || objectName.EndsWith("ammo")) ?
                        $"powerups\\{objectName}" :
                    objectName.StartsWith("powerup") ?
                        $"multi\\powerups\\{objectName}" :
                    objectName.EndsWith("grenade") ?
                        $"weapons\\grenade\\{objectName}" :
                    $"equipment\\{objectName}";

                objectName = $"objects\\{equipmentClassName}\\{objectName}";
            }
            else if (tag.Group.Tag == new Tag("vehi"))
            {

            }
            else if (tag.Group.Tag == new Tag("armr"))
            {
                // TODO: figure out spartan/elite armor name differences

                objectName = $"objects\\characters\\masterchief\\mp_masterchief\\armor\\{objectName}";

                tagNames[definition.Model.Index] = objectName;

                if (modelDefinition.RenderModel != null)
                    tagNames[modelDefinition.RenderModel.Index] = objectName;

                if (modelDefinition.CollisionModel != null)
                    tagNames[modelDefinition.CollisionModel.Index] = objectName;

                if (modelDefinition.PhysicsModel != null)
                    tagNames[modelDefinition.PhysicsModel.Index] = objectName;

                if (modelDefinition.Animation != null)
                    tagNames[modelDefinition.Animation.Index] = objectName;
            }

            tagNames[tag.Index] = objectName;
        }
        private static List<QueuedTag> FindScenarios(OpenTagCache info, Stream stream)
        {
            // Get a dictionary of scenarios by map ID
            var scenarios = new Dictionary<int, QueuedTag>();
            foreach (var scenarioTag in info.Cache.Tags.FindAllInGroup("scnr"))
            {
                var context = new TagSerializationContext(stream, info.Cache, info.StringIds, scenarioTag);
                var scenario = info.Deserializer.Deserialize<Scenario>(context);
                scenarios[scenario.MapId] = new QueuedTag { Tag = scenarioTag, Data = scenario };
            }

            // Get the ones to actually compare
            return MapIdsToCompare.Select(id => scenarios[id]).ToList();
        }
        public override bool Execute(List<string> args)
        {
            //Arguments needed: filepath, <new>|<tagIndex>
            if (args.Count < 2)
            {
                return false;
            }

            TagInstance tag = null;
            bool b_duplicate;
            // optional argument: forces overwriting of tags that are not type: phmo
            bool b_force = (args.Count >= 3 && args[2].ToLower().Equals("force"));

            if (args[1].ToLower().Equals("new"))
            {
                b_duplicate = true;
            }
            else
            {
                tag = ArgumentParser.ParseTagIndex(_info.Cache, args[1]);
                if (tag == null)
                {
                    return false;
                }
                b_duplicate = false;
            }

            if (!b_force && !b_duplicate && !tag.IsInGroup("phmo"))
            {
                Console.WriteLine("Tag to override was not of class- 'phmo'. Use third argument- 'force' to inject.");
                return false;
            }

            var filename = args[0];

            var modelbuilder = new PhysicsModelBuilder();
            if (!modelbuilder.ParseFromFile(filename))
            {
                return false;
            }
            //modelbuilder must also make a node for the physics model
            var phmo = modelbuilder.Build();

            if (phmo == null)
            {
                return false;
            }

            using (var stream = _info.OpenCacheReadWrite())
            {

                if (b_duplicate)
                {
                    //duplicate an existing tag, trashcan phmo
                    tag = _info.Cache.DuplicateTag(stream, _info.Cache.Tags[0x4436]);
                    if (tag == null)
                    {
                        Console.WriteLine("Failed tag duplication.");
                        return false;
                    }
                }

                var context = new TagSerializationContext(stream, _info.Cache, _info.StringIds, tag);
                _info.Serializer.Serialize(context, phmo);

            }

            Console.Write("Successfully imported phmo to: ");
            TagPrinter.PrintTagShort(tag);

            return true;
        }
        public override bool Execute(List<string> args)
        {
            if (args.Count != 3)
                return false;
            var variantName = args[0];
            var fileType = args[1];
            var fileName = args[2];
            if (fileType != "obj")
                return false;

            // Find the variant to extract
            if (_model.RenderModel == null)
            {
                Console.Error.WriteLine("The model does not have a render model associated with it.");
                return true;
            }
            var variant = _model.Variants.FirstOrDefault(v => (_stringIds.GetString(v.Name) ?? v.Name.ToString()) == variantName);
            if (variant == null && _model.Variants.Count > 0)
            {
                Console.Error.WriteLine("Unable to find variant \"{0}\"", variantName);
                Console.Error.WriteLine("Use \"listvariants\" to list available variants.");
                return true;
            }

            // Load resource caches
            Console.WriteLine("Loading resource caches...");
            var resourceManager = new ResourceDataManager();
            try
            {
                resourceManager.LoadCachesFromDirectory(_fileInfo.DirectoryName);
            }
            catch
            {
                Console.WriteLine("Unable to load the resource .dat files.");
                Console.WriteLine("Make sure that they all exist and are valid.");
                return true;
            }

            // Deserialize the render model tag
            Console.WriteLine("Reading model data...");
            RenderModel renderModel;
            using (var cacheStream = _fileInfo.OpenRead())
            {
                var renderModelContext = new TagSerializationContext(cacheStream, _cache, _model.RenderModel);
                renderModel = TagDeserializer.Deserialize<RenderModel>(renderModelContext);
            }
            if (renderModel.Resource == null)
            {
                Console.Error.WriteLine("Render model does not have a resource associated with it");
                return true;
            }

            // Deserialize the resource definition
            var resourceContext = new ResourceSerializationContext(renderModel.Resource);
            var definition = TagDeserializer.Deserialize<RenderGeometryResourceDefinition>(resourceContext);

            using (var resourceStream = new MemoryStream())
            {
                // Extract the resource data
                resourceManager.Extract(renderModel.Resource, resourceStream);
                using (var objFile = new StreamWriter(File.Open(fileName, FileMode.Create, FileAccess.Write)))
                {
                    var objExtractor = new ObjExtractor(objFile);
                    var vertexCompressor = new VertexCompressor(renderModel.Compression[0]); // Create a (de)compressor from the first compression block
                    if (variant != null)
                    {
                        // Extract each region in the variant
                        foreach (var region in variant.Regions)
                        {
                            // Get the corresonding region in the render model tag
                            if (region.RenderModelRegionIndex >= renderModel.Regions.Count)
                                continue;
                            var renderModelRegion = renderModel.Regions[region.RenderModelRegionIndex];

                            // Get the corresponding permutation in the render model tag
                            // (Just extract the first permutation for now)
                            if (region.Permutations.Count == 0)
                                continue;
                            var permutation = region.Permutations[0];
                            if (permutation.RenderModelPermutationIndex >= renderModelRegion.Permutations.Count)
                                continue;
                            var renderModelPermutation = renderModelRegion.Permutations[permutation.RenderModelPermutationIndex];

                            // Extract each mesh in the permutation
                            var meshIndex = renderModelPermutation.MeshIndex;
                            var meshCount = renderModelPermutation.MeshCount;
                            var regionName = _stringIds.GetString(region.Name) ?? region.Name.ToString();
                            var permutationName = _stringIds.GetString(permutation.Name) ?? permutation.Name.ToString();
                            Console.WriteLine("Extracting {0} mesh(es) for {1}:{2}...", meshCount, regionName, permutationName);
                            for (var i = 0; i < meshCount; i++)
                            {
                                // Create a MeshReader for the mesh and pass it to the obj extractor
                                var meshReader = new MeshReader(renderModel.Meshes[meshIndex + i], definition);
                                objExtractor.ExtractMesh(meshReader, vertexCompressor, resourceStream);
                            }
                        }
                    }
                    else
                    {
                        // No variant - just extract every mesh
                        Console.WriteLine("Extracting {0} mesh(es)...", renderModel.Meshes.Count);
                        foreach (var mesh in renderModel.Meshes)
                        {
                            // Create a MeshReader for the mesh and pass it to the obj extractor
                            var meshReader = new MeshReader(mesh, definition);
                            objExtractor.ExtractMesh(meshReader, vertexCompressor, resourceStream);
                        }
                    }
                    objExtractor.Finish();
                }
            }
            Console.WriteLine("Done!");
            return true;
        }
        private void SetScenarioName(Stream stream, TagInstance tag, ref Dictionary<int, string> tagNames)
        {
            var context = new TagSerializationContext(stream, Info.Cache, Info.StringIds, tag);
            var definition = Info.Deserializer.Deserialize<Scenario>(context);

            var tagName = Info.StringIds.GetString(definition.ScenarioZonesetGroups[0].Name);
            var slashIndex = tagName.LastIndexOf('\\');
            var scenarioName = tagName.Substring(slashIndex + 1);

            tagNames[tag.Index] = tagName;

            var bsp = definition.StructureBsps[0].StructureBsp2;
            if (bsp != null)
                tagNames[bsp.Index] = tagName;

            var design = definition.StructureBsps[0].Design;
            if (design != null)
                tagNames[design.Index] = $"{tagName}_design";

            var cubemap = definition.StructureBsps[0].Cubemap;
            if (cubemap != null)
                tagNames[cubemap.Index] = $"{tagName}_{scenarioName}_cubemaps";

            var skyObject = definition.SkyReferences[0].SkyObject;
            if (skyObject != null)
                tagNames[skyObject.Index] = $"{tagName.Substring(0, slashIndex)}\\sky\\sky";
        }