Example #1
0
        public static List <BlobImage> GetBlobsFrom(FileInfo[] files)
        {
            List <BlobImage> result = new List <BlobImage>(files.Length);

            foreach (FileInfo file in files)
            {
                BlobFile blob = new BlobFile();

                using (Stream s = file.Open(FileMode.Open, FileAccess.Read))
                {
                    blob.Deserialize(s);

                    foreach (var entry in blob.Entries)
                    {
                        BlobImage image = new BlobImage();
                        image.BlobReference = new BlobReference();

                        image.BlobReference.Id       = entry.Name;
                        image.BlobReference.FileName = file.Name;

                        s.Seek(entry.Offset, SeekOrigin.Begin);
                        byte[] data = new byte[entry.Size];
                        s.Read(data, 0, data.Length);

                        Bitmap b = null;

                        try
                        {
                            b = GenerateBitmapFromCfs(new MemoryStream(data), file.Name);
                        }
                        catch (Exception)
                        {
                            b = null;
                        }

                        if (b == null)
                        {
                            continue;
                        }

                        image.Image = b;
                        result.Add(image);
                    }
                }
            }

            return(result);
        }
        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var dialog = new OpenFileDialog();

            dialog.Filter = "Infantry Online Blob File (*.blo)|*.blo";

            if (dialog.ShowDialog() == DialogResult.OK)
            {
                blob = new BlobFile();

                fileName = dialog.SafeFileName;
                filePath = dialog.FileName;

                blobStream = dialog.OpenFile();

                blob.Deserialize(blobStream);
                blobListBox.DataSource = blob.Entries;

                UpdateMainWindowTitle();
            }
        }
Example #3
0
        private LoadedBlobFile LoadBlobFile(string path)
        {
            using (var fs = File.OpenRead(path))
            {
                var memoryStream = new MemoryStream();

                fs.CopyTo(memoryStream);
                memoryStream.Seek(0, SeekOrigin.Begin);

                var blob = new BlobFile();
                blob.Deserialize(memoryStream);

                memoryStream.Seek(0, SeekOrigin.Begin);

                return(new LoadedBlobFile
                {
                    BlobName = Path.GetFileName(path),
                    BlobFile = blob,
                    Stream = memoryStream
                });
            }
        }
Example #4
0
        public static void Main(string[] args)
        {
            var xmlSettings = new XmlWriterSettings()
            {
                Indent          = true,
                NewLineChars    = "\r\n",
                NewLineHandling = NewLineHandling.Replace,
            };

            var configBasePath = Path.Combine(GetExecutablePath(), "serializers", "resources");
            var config         = Configuration.Load(configBasePath);

            string parseName = null;
            var    mode      = Mode.Unknown;
            var    showHelp  = false;

            var options = new OptionSet()
            {
                { "b|xml2bin", "convert xml to bin", v => mode = v != null ? Mode.Import : mode },
                { "x|bin2xml", "convert bin to xml", v => mode = v != null ? Mode.Export : mode },
                { "p|parse=", "override parse name", v => parseName = v },
                { "h|help", "show this message and exit", v => showHelp = v != null },
            };

            List <string> extras;

            try
            {
                extras = options.Parse(args);
            }
            catch (OptionException e)
            {
                Console.Write("{0}: ", GetExecutableName());
                Console.WriteLine(e.Message);
                Console.WriteLine("Try `{0} --help' for more information.", GetExecutableName());
                return;
            }

            // try to figure out what they want to do
            if (mode == Mode.Unknown && extras.Count >= 1)
            {
                var testPath = extras[0];

                if (Directory.Exists(testPath) == true)
                {
                    mode = Mode.Import;
                }
                else if (File.Exists(testPath) == true)
                {
                    mode = Mode.Export;
                }
            }

            if (extras.Count < 1 || extras.Count > 2 ||
                showHelp == true || mode == Mode.Unknown)
            {
                Console.WriteLine("Usage: {0} [OPTIONS]+ -x input_bin [output_dir]", GetExecutableName());
                Console.WriteLine("       {0} [OPTIONS]+ -b input_dir [output_bin]", GetExecutableName());
                Console.WriteLine();
                Console.WriteLine("Options:");
                options.WriteOptionDescriptions(Console.Out);
                return;
            }

            if (mode == Mode.Export)
            {
                var inputPath  = extras[0];
                var outputPath = extras.Count > 1
                    ? extras[1]
                    : Path.ChangeExtension(inputPath, null);

                using (var input = File.OpenRead(inputPath))
                {
                    Console.WriteLine("Loading bin...");
                    var blob = new BlobFile();
                    blob.Deserialize(input);

                    if (parseName == null)
                    {
                        parseName = Path.GetFileNameWithoutExtension(inputPath);
                    }

                    var parse = config.GetParse(parseName);
                    if (parse == null)
                    {
                        Console.WriteLine(
                            "Don't know how to handle '{0}' with a hash of '{1}'.",
                            parseName,
                            blob.ParseHash);
                        return;
                    }

                    var target = parse.GetTarget(blob.ParseHash);
                    if (target == null)
                    {
                        Console.WriteLine(
                            "Don't know how to handle '{0}' with a hash of '{1}'.",
                            parseName,
                            blob.ParseHash);
                        return;
                    }

                    var version = target.FirstVersion();
                    if (version == null)
                    {
                        Console.WriteLine(
                            "No support for '{0}' with a hash of '{1}'.",
                            parseName,
                            blob.ParseHash);
                        return;
                    }

                    var assemblyPath = Path.Combine(
                        GetExecutablePath(),
                        "serializers",
                        "assemblies",
                        version + ".dll");
                    if (File.Exists(assemblyPath) == false)
                    {
                        Console.WriteLine(
                            "Assembly '{0}' appears to be missing!",
                            Path.GetFileName(assemblyPath));
                        return;
                    }

                    var assembly = Assembly.LoadFrom(assemblyPath);
                    var type     = assembly.GetType(target.Class);
                    if (type == null)
                    {
                        Console.WriteLine(
                            "Assembly '{0}' does not expose '{1}'!",
                            Path.GetFileName(assemblyPath),
                            target.Class);
                        return;
                    }

                    var resource = new Resource()
                    {
                        Parse     = parseName,
                        ParseHash = blob.ParseHash,
                    };

                    foreach (var file in blob.Files)
                    {
                        resource.Files.Add(new Resource.FileEntry()
                        {
                            Name      = file.Name,
                            Timestamp = file.Timestamp,
                        });
                    }

                    foreach (var dependency in blob.Dependencies)
                    {
                        resource.Dependencies.Add(new Resource.DependencyEntry()
                        {
                            Type = dependency.Type,
                            Name = dependency.Name,
                            Hash = dependency.Hash,
                        });
                    }

                    var loadResource = typeof(BlobDataReader)
                                       .GetMethod("LoadResource", BindingFlags.Public | BindingFlags.Static)
                                       .MakeGenericMethod(type);

                    Console.WriteLine("Loading entries...");

                    Func <int, string> getFileNameFromIndex =
                        i =>
                    {
                        if (i < 0 || i >= resource.Files.Count)
                        {
                            throw new KeyNotFoundException($"file index {i} is out of range");
                        }

                        return(resource.Files[i].Name);
                    };

                    var list = (IList)loadResource.Invoke(
                        null,
                        new object[] { input, parse.IsClient, parse.IsServer, getFileNameFromIndex });

                    var entries  = list.Cast <object>();
                    var listType = typeof(List <>).MakeGenericType(type);

                    Console.WriteLine("Saving entries to XML...");
                    switch (parse.Mode.ToLowerInvariant())
                    {
                    case "single":
                    {
                        var serializer = new DataContractSerializer(listType);

                        const string entryName = "entries.xml";
                        resource.Entries.Add(entryName);

                        var entryPath = Path.Combine(outputPath, entryName);
                        if (File.Exists(entryPath) == true)
                        {
                            throw new InvalidOperationException();
                        }

                        var entryParentPath = Path.GetDirectoryName(entryPath);
                        if (string.IsNullOrEmpty(entryParentPath) == false)
                        {
                            Directory.CreateDirectory(entryParentPath);
                        }

                        using (var output = File.Create(entryPath))
                        {
                            var localList = (IList)Activator.CreateInstance(listType);
                            foreach (var entry in entries)
                            {
                                localList.Add(entry);
                            }

                            var writer = XmlWriter.Create(output, xmlSettings);
                            serializer.WriteStartObject(writer, listType);
                            writer.WriteAttributeString("xmlns", "c", "", "http://datacontract.gib.me/cryptic");
                            //writer.WriteAttributeString("xmlns", "i", "", "http://www.w3.org/2001/XMLSchema-instance");
                            writer.WriteAttributeString(
                                "xmlns",
                                "a",
                                "",
                                "http://schemas.microsoft.com/2003/10/Serialization/Arrays");
                            //writer.WriteAttributeString("xmlns", "s", "", "http://datacontract.gib.me/startrekonline");
                            serializer.WriteObjectContent(writer, localList);
                            serializer.WriteEndObject(writer);
                            writer.Flush();
                        }

                        break;
                    }

                    case "file":
                    {
                        var serializer = new DataContractSerializer(listType);

                        var fileNameFieldName = "FileName";
                        if (string.IsNullOrEmpty(target.FileNameKey) == false)
                        {
                            fileNameFieldName = target.FileNameKey;
                        }

                        var fileNameField = type.GetField(
                            fileNameFieldName,
                            BindingFlags.Public | BindingFlags.Instance);
                        if (fileNameField == null)
                        {
                            Console.WriteLine("Class '{0}' does not expose '{1}'!", target.Class, fileNameFieldName);
                            return;
                        }

                        var uniqueFileNames = entries
                                              .Select(i => (string)fileNameField.GetValue(i))
                                              .Distinct();
                        foreach (var fileName in uniqueFileNames)
                        {
                            var entryName = fileName;
                            entryName = entryName.Replace('/', '\\');
                            entryName = Path.ChangeExtension(entryName, ".xml");
                            resource.Entries.Add(entryName);

                            var entryPath = Path.Combine(outputPath, entryName);
                            if (File.Exists(entryPath) == true)
                            {
                                throw new InvalidOperationException();
                            }

                            var entryParentPath = Path.GetDirectoryName(entryPath);
                            if (string.IsNullOrEmpty(entryParentPath) == false)
                            {
                                Directory.CreateDirectory(entryParentPath);
                            }

                            using (var output = File.Create(entryPath))
                            {
                                var    localEntries = (IList)Activator.CreateInstance(listType);
                                string name         = fileName;
                                foreach (var entry in entries
                                         .Where(e => (string)(fileNameField.GetValue(e)) == name))
                                {
                                    localEntries.Add(entry);
                                }

                                var writer = XmlWriter.Create(output, xmlSettings);
                                serializer.WriteStartObject(writer, listType);
                                writer.WriteAttributeString("xmlns", "c", "", "http://datacontract.gib.me/cryptic");
                                //writer.WriteAttributeString("xmlns", "i", "", "http://www.w3.org/2001/XMLSchema-instance");
                                writer.WriteAttributeString(
                                    "xmlns",
                                    "a",
                                    "",
                                    "http://schemas.microsoft.com/2003/10/Serialization/Arrays");
                                //writer.WriteAttributeString("xmlns", "s", "", "http://datacontract.gib.me/startrekonline");
                                serializer.WriteObjectContent(writer, localEntries);
                                serializer.WriteEndObject(writer);
                                writer.Flush();
                            }
                        }

                        break;
                    }

                    case "name":
                    {
                        var serializer = new DataContractSerializer(type);

                        if (string.IsNullOrEmpty(target.Key) == true)
                        {
                            Console.WriteLine("No key set for '{0}'!", parseName);
                            return;
                        }

                        var keyField = type.GetField(
                            target.Key,
                            BindingFlags.Public | BindingFlags.Instance);
                        if (keyField == null)
                        {
                            Console.WriteLine("Class '{0}' does not expose '{1}'!", target.Class, target.Key);
                            return;
                        }

                        foreach (var entry in entries)
                        {
                            var entryName = Path.ChangeExtension((string)keyField.GetValue(entry), ".xml");

                            resource.Entries.Add(entryName);

                            var entryPath = Path.Combine(outputPath, entryName);
                            if (File.Exists(entryPath) == true)
                            {
                                throw new InvalidOperationException();
                            }

                            var entryParentPath = Path.GetDirectoryName(entryPath);
                            if (string.IsNullOrEmpty(entryParentPath) == false)
                            {
                                Directory.CreateDirectory(entryParentPath);
                            }

                            using (var output = File.Create(entryPath))
                            {
                                var writer = XmlWriter.Create(output, xmlSettings);
                                serializer.WriteStartObject(writer, entry);
                                writer.WriteAttributeString("xmlns", "c", "", "http://datacontract.gib.me/cryptic");
                                //writer.WriteAttributeString("xmlns", "i", "", "http://www.w3.org/2001/XMLSchema-instance");
                                writer.WriteAttributeString(
                                    "xmlns",
                                    "a",
                                    "",
                                    "http://schemas.microsoft.com/2003/10/Serialization/Arrays");
                                //writer.WriteAttributeString("xmlns", "s", "", "http://datacontract.gib.me/startrekonline");
                                serializer.WriteObjectContent(writer, entry);
                                serializer.WriteEndObject(writer);
                                writer.Flush();
                            }
                        }

                        break;
                    }

                    case "entry":
                    {
                        var serializer = new DataContractSerializer(type);

                        var fileNameFieldName = "FileName";
                        if (string.IsNullOrEmpty(target.FileNameKey) == false)
                        {
                            fileNameFieldName = target.FileNameKey;
                        }

                        var fileNameField = type.GetField(
                            fileNameFieldName,
                            BindingFlags.Public | BindingFlags.Instance);
                        if (fileNameField == null)
                        {
                            Console.WriteLine("Class '{0}' does not expose '{1}'!", target.Class, fileNameFieldName);
                            return;
                        }

                        if (string.IsNullOrEmpty(target.Key) == true)
                        {
                            Console.WriteLine("No key set for '{0}'!", parseName);
                            return;
                        }

                        var keyField = type.GetField(
                            target.Key,
                            BindingFlags.Public | BindingFlags.Instance);
                        if (keyField == null)
                        {
                            Console.WriteLine("Class '{0}' does not expose '{1}'!", target.Class, target.Key);
                            return;
                        }

                        foreach (var entry in entries)
                        {
                            var entryName = (string)fileNameField.GetValue(entry);
                            entryName = entryName.Replace('/', '\\');
                            entryName = Path.ChangeExtension(
                                Path.Combine(entryName, (string)keyField.GetValue(entry)),
                                ".xml");

                            resource.Entries.Add(entryName);

                            var entryPath = Path.Combine(outputPath, entryName);
                            if (File.Exists(entryPath) == true)
                            {
                                throw new InvalidOperationException();
                            }

                            var entryParentPath = Path.GetDirectoryName(entryPath);
                            if (string.IsNullOrEmpty(entryParentPath) == false)
                            {
                                Directory.CreateDirectory(entryParentPath);
                            }

                            using (var output = File.Create(entryPath))
                            {
                                var writer = XmlWriter.Create(output, xmlSettings);
                                serializer.WriteStartObject(writer, entry);
                                writer.WriteAttributeString("xmlns", "c", "", "http://datacontract.gib.me/cryptic");
                                //writer.WriteAttributeString("xmlns", "i", "", "http://www.w3.org/2001/XMLSchema-instance");
                                writer.WriteAttributeString(
                                    "xmlns",
                                    "a",
                                    "",
                                    "http://schemas.microsoft.com/2003/10/Serialization/Arrays");
                                //writer.WriteAttributeString("xmlns", "s", "", "http://datacontract.gib.me/startrekonline");
                                serializer.WriteObjectContent(writer, entry);
                                serializer.WriteEndObject(writer);
                                writer.Flush();
                            }
                        }

                        break;
                    }

                    default:
                    {
                        throw new NotSupportedException();
                    }
                    }

                    Console.WriteLine("Saving index...");
                    using (var output = File.Create(Path.Combine(outputPath, "@resource.xml")))
                    {
                        var writer     = XmlWriter.Create(output, xmlSettings);
                        var serializer = new XmlSerializer(typeof(Resource));
                        serializer.Serialize(writer, resource);
                        writer.Flush();
                    }
                }
            }
            else if (mode == Mode.Import)
            {
                var inputPath  = extras[0];
                var outputPath = extras.Count > 1 ? extras[1] : Path.ChangeExtension(inputPath, ".bin");

                Console.WriteLine("Loading index...");
                Resource resource;
                using (var input = File.OpenRead(Path.Combine(inputPath, "@resource.xml")))
                {
                    var reader     = XmlReader.Create(input);
                    var serializer = new XmlSerializer(typeof(Resource));
                    resource = (Resource)serializer.Deserialize(reader);
                }

                var schema = config.GetParse(resource.Parse);
                if (schema == null)
                {
                    Console.WriteLine("Don't know how to handle '{0}'!", resource.Parse);
                    return;
                }

                var target = schema.GetTarget(resource.ParseHash);
                if (target == null)
                {
                    Console.WriteLine(
                        "Don't know how to handle '{0}' with a hash of {1}.",
                        resource.Parse,
                        resource.ParseHash);
                    return;
                }

                var version = target.FirstVersion();
                if (version == null)
                {
                    Console.WriteLine(
                        "No support for '{0}' with a hash of {1:X8}.",
                        resource.Parse,
                        resource.ParseHash);
                    return;
                }

                var assemblyPath = Path.Combine(
                    GetExecutablePath(),
                    "serializers",
                    "assemblies",
                    version + ".dll");
                if (File.Exists(assemblyPath) == false)
                {
                    Console.WriteLine(
                        "Assembly '{0}' appears to be missing!",
                        Path.GetFileName(assemblyPath));
                    return;
                }

                var assembly = Assembly.LoadFrom(assemblyPath);
                var type     = assembly.GetType(target.Class);
                if (type == null)
                {
                    Console.WriteLine(
                        "Assembly '{0}' does not expose '{1}'!",
                        Path.GetFileName(assemblyPath),
                        target.Class);
                    return;
                }

                var blob = new BlobFile()
                {
                    ParseHash = resource.ParseHash,
                };

                foreach (var file in resource.Files)
                {
                    blob.Files.Add(new Blob.FileEntry()
                    {
                        Name      = file.Name,
                        Timestamp = file.Timestamp,
                    });
                }

                foreach (var dependency in resource.Dependencies)
                {
                    blob.Dependencies.Add(new Blob.DependencyEntry()
                    {
                        Type = dependency.Type,
                        Name = dependency.Name,
                        Hash = dependency.Hash,
                    });
                }

                var listType = typeof(List <>).MakeGenericType(type);
                var entries  = (IList)Activator.CreateInstance(listType);

                Console.WriteLine("Loading entries from XML...");
                switch (schema.Mode.ToLowerInvariant())
                {
                case "single":
                case "file":
                {
                    var serializer = new DataContractSerializer(listType);

                    foreach (var entryName in resource.Entries)
                    {
                        var entryPath = Path.IsPathRooted(entryName) == true
                                ? entryName
                                : Path.Combine(inputPath, entryName);

                        using (var input = File.OpenRead(entryPath))
                        {
                            var reader       = XmlReader.Create(input);
                            var localEntries = (IList)serializer.ReadObject(reader);
                            foreach (var entry in localEntries)
                            {
                                entries.Add(entry);
                            }
                        }
                    }

                    break;
                }

                case "name":
                case "entry":
                {
                    var serializer = new DataContractSerializer(type);

                    foreach (var entryName in resource.Entries)
                    {
                        var entryPath = Path.IsPathRooted(entryName) == true
                                ? entryName
                                : Path.Combine(inputPath, entryName);

                        using (var input = File.OpenRead(entryPath))
                        {
                            var reader = XmlReader.Create(input);
                            var entry  = serializer.ReadObject(reader);
                            entries.Add(entry);
                        }
                    }

                    break;
                }

                default:
                {
                    throw new NotSupportedException();
                }
                }

                if (string.IsNullOrEmpty(target.Key) == false)
                {
                    var keyField = type.GetField(
                        target.Key,
                        BindingFlags.Public | BindingFlags.Instance);
                    if (keyField == null)
                    {
                        Console.WriteLine("Class '{0}' does not expose '{1}'!", target.Class, target.Key);
                        return;
                    }

                    Console.WriteLine("Sorting entries...");
                    var sortedEntries = entries
                                        .Cast <object>()
                                        .OrderBy(keyField.GetValue)
                                        .ToList();
                    entries.Clear();
                    foreach (var entry in sortedEntries)
                    {
                        entries.Add(entry);
                    }

                    Func <string, int> getIndexFromFileName = s => blob.Files.FindIndex(fe => fe.Name == s);

                    Console.WriteLine("Saving entries...");
                    var saveResource = typeof(BlobDataWriter)
                                       .GetMethod("SaveResource", BindingFlags.Public | BindingFlags.Static)
                                       .MakeGenericMethod(type);
                    using (var output = File.Create(outputPath))
                    {
                        blob.Serialize(output);

                        saveResource.Invoke(
                            null,
                            new object[] { entries, output, schema.IsClient, schema.IsServer, getIndexFromFileName });
                    }
                }
            }
            else
            {
                throw new InvalidOperationException();
            }
        }
Example #5
0
        public static void Main(string[] args)
        {
            var xmlSettings = new XmlWriterSettings()
            {
                Indent          = true,
                NewLineChars    = "\r\n",
                NewLineHandling = NewLineHandling.Replace,
            };

            var configBasePath = Path.Combine(GetExecutablePath(), "serializers", "objects");
            var config         = Configuration.Load(configBasePath);

            string schemaName = null;
            var    mode       = Mode.Unknown;
            var    showHelp   = false;

            var options = new OptionSet()
            {
                { "b|xml2bin", "convert xml to bin", v => mode = v != null ? Mode.Import : mode },
                { "x|bin2xml", "convert bin to xml", v => mode = v != null ? Mode.Export : mode },
                { "s|schema=", "override schema name", v => schemaName = v },
                { "h|help", "show this message and exit", v => showHelp = v != null },
            };

            List <string> extras;

            try
            {
                extras = options.Parse(args);
            }
            catch (OptionException e)
            {
                Console.Write("{0}: ", GetExecutableName());
                Console.WriteLine(e.Message);
                Console.WriteLine("Try `{0} --help' for more information.", GetExecutableName());
                return;
            }

            // try to figure out what they want to do
            if (mode == Mode.Unknown &&
                extras.Count >= 1)
            {
                var testPath = extras[0];

                if (Directory.Exists(testPath) == true)
                {
                    mode = Mode.Import;
                }
                else if (File.Exists(testPath) == true)
                {
                    mode = Mode.Export;
                }
            }

            if (extras.Count < 1 || extras.Count > 2 || showHelp == true || mode == Mode.Unknown)
            {
                Console.WriteLine("Usage: {0} [OPTIONS]+ -x input_bin [output_xml]", GetExecutableName());
                Console.WriteLine("       {0} [OPTIONS]+ -b input_xml [output_bin]", GetExecutableName());
                Console.WriteLine();
                Console.WriteLine("Options:");
                options.WriteOptionDescriptions(Console.Out);
                return;
            }

            if (mode == Mode.Export)
            {
                var inputPath  = extras[0];
                var outputPath = extras.Count > 1
                                     ? extras[1]
                                     : Path.ChangeExtension(inputPath, ".xml");

                using (var input = File.OpenRead(inputPath))
                {
                    Console.WriteLine("Loading bin...");
                    var blob = new BlobFile();
                    blob.Deserialize(input);

                    if (schemaName == null)
                    {
                        schemaName = Path.GetFileNameWithoutExtension(inputPath);
                    }

                    var schema = config.GetSchema(schemaName);
                    if (schema == null)
                    {
                        Console.WriteLine("Don't know how to handle '{0}'  with a hash of '{1}'.",
                                          schemaName,
                                          blob.ParserHash);
                        return;
                    }

                    var target = schema.GetTarget(blob.ParserHash);
                    if (target == null)
                    {
                        Console.WriteLine("Don't know how to handle '{0}' with a hash of '{1}'.",
                                          schemaName,
                                          blob.ParserHash);
                        return;
                    }

                    var version = target.FirstVersion();
                    if (version == null)
                    {
                        Console.WriteLine("No support for '{0}' with a hash of '{1}'.",
                                          schemaName,
                                          blob.ParserHash);
                        return;
                    }

                    var assemblyPath = Path.Combine(GetExecutablePath(),
                                                    "serializers",
                                                    "assemblies",
                                                    version + ".dll");
                    if (File.Exists(assemblyPath) == false)
                    {
                        Console.WriteLine("Assembly '{0}' appears to be missing!",
                                          Path.GetFileName(assemblyPath));
                        return;
                    }

                    var assembly = Assembly.LoadFrom(assemblyPath);
                    var type     = assembly.GetType(target.Class);
                    if (type == null)
                    {
                        Console.WriteLine("Assembly '{0}' does not expose '{1}'!",
                                          Path.GetFileName(assemblyPath),
                                          target.Class);
                        return;
                    }

                    var resource = new Resource
                    {
                        Schema     = schemaName,
                        ParserHash = blob.ParserHash,
                    };

                    foreach (var file in blob.Files)
                    {
                        resource.Files.Add(new Resource.FileEntry()
                        {
                            Name      = file.Name,
                            Timestamp = file.Timestamp,
                        });
                    }

                    foreach (var dependency in blob.Dependencies)
                    {
                        resource.Dependencies.Add(new Resource.DependencyEntry()
                        {
                            Type = dependency.Type,
                            Name = dependency.Name,
                            Hash = dependency.Hash,
                        });
                    }

                    Func <int, string> getFileNameFromIndex =
                        i =>
                    {
                        if (i < 0 || i >= resource.Files.Count)
                        {
                            throw new KeyNotFoundException("file index " +
                                                           i.ToString(CultureInfo.InvariantCulture) +
                                                           " is out of range");
                        }

                        return(resource.Files[i].Name);
                    };

                    var loadObject = typeof(BlobDataReader)
                                     .GetMethod("LoadObject", BindingFlags.Public | BindingFlags.Static)
                                     .MakeGenericMethod(type);
                    var data = loadObject.Invoke(
                        null,
                        new object[] { input, schema.IsClient, schema.IsServer, getFileNameFromIndex });

                    Console.WriteLine("Saving object to XML...");
                    using (var output = File.Create(outputPath))
                    {
                        var writer = XmlWriter.Create(output, xmlSettings);
                        writer.WriteStartDocument();
                        writer.WriteStartElement("object");

                        writer.WriteStartElement("data");
                        var objectWriter     = XmlWriter.Create(writer, xmlSettings);
                        var objectSerializer = new DataContractSerializer(type);
                        objectSerializer.WriteStartObject(objectWriter, type);
                        objectWriter.WriteAttributeString("xmlns", "c", "", "http://datacontract.gib.me/cryptic");
                        //objectWriter.WriteAttributeString("xmlns", "i", "", "http://www.w3.org/2001/XMLSchema-instance");
                        objectWriter.WriteAttributeString("xmlns",
                                                          "a",
                                                          "",
                                                          "http://schemas.microsoft.com/2003/10/Serialization/Arrays");
                        //objectWriter.WriteAttributeString("xmlns", "s", "", "http://datacontract.gib.me/startrekonline");
                        objectSerializer.WriteObjectContent(objectWriter, data);
                        objectSerializer.WriteEndObject(objectWriter);
                        objectWriter.Flush();
                        writer.WriteEndElement();

                        var resourceWriter     = XmlWriter.Create(writer, xmlSettings);
                        var resourceSerializer = new XmlSerializer(typeof(Resource));
                        resourceSerializer.Serialize(resourceWriter, resource);
                        resourceWriter.Flush();

                        writer.WriteEndElement();
                        writer.WriteEndDocument();
                        writer.Flush();
                    }
                }
            }
            else if (mode == Mode.Import)
            {
                var inputPath  = extras[0];
                var outputPath = extras.Count > 1 ? extras[1] : Path.ChangeExtension(inputPath, ".bin");

                Console.WriteLine("Loading XML...");

                var blob = new BlobFile();

                using (var input = File.OpenRead(inputPath))
                {
                    var doc = new XPathDocument(input);
                    var nav = doc.CreateNavigator();

                    var resourceNode = nav.SelectSingleNode("/object/resource");
                    if (resourceNode == null)
                    {
                        throw new InvalidOperationException();
                    }
                    var resourceSerializer = new XmlSerializer(typeof(Resource));
                    var resource           = (Resource)resourceSerializer.Deserialize(resourceNode.ReadSubtree());

                    var schema = config.GetSchema(resource.Schema);
                    if (schema == null)
                    {
                        Console.WriteLine("Don't know how to handle '{0}'!", resource.Schema);
                        return;
                    }

                    var target = schema.GetTarget(resource.ParserHash);
                    if (target == null)
                    {
                        Console.WriteLine("Don't know how to handle '{0}' with a hash of {1}.",
                                          resource.Schema,
                                          resource.ParserHash);
                        return;
                    }

                    var version = target.FirstVersion();
                    if (version == null)
                    {
                        Console.WriteLine("No support for '{0}' with a hash of {1:X8}.",
                                          resource.Schema,
                                          resource.ParserHash);
                        return;
                    }

                    var assemblyPath = Path.Combine(GetExecutablePath(),
                                                    "serializers",
                                                    "assemblies",
                                                    version + ".dll");
                    if (File.Exists(assemblyPath) == false)
                    {
                        Console.WriteLine("Assembly '{0}' appears to be missing!",
                                          Path.GetFileName(assemblyPath));
                        return;
                    }

                    var assembly = Assembly.LoadFrom(assemblyPath);
                    var type     = assembly.GetType(target.Class);
                    if (type == null)
                    {
                        Console.WriteLine("Assembly '{0}' does not expose '{1}'!",
                                          Path.GetFileName(assemblyPath),
                                          target.Class);
                        return;
                    }

                    blob.ParserHash = resource.ParserHash;

                    foreach (var file in resource.Files)
                    {
                        blob.Files.Add(new Blob.FileEntry()
                        {
                            Name      = file.Name,
                            Timestamp = file.Timestamp,
                        });
                    }

                    foreach (var dependency in resource.Dependencies)
                    {
                        blob.Dependencies.Add(new Blob.DependencyEntry()
                        {
                            Type = dependency.Type,
                            Name = dependency.Name,
                            Hash = dependency.Hash,
                        });
                    }

                    var objectNode = nav.SelectSingleNode("/object/data");
                    if (objectNode == null)
                    {
                        throw new InvalidOperationException();
                    }

                    objectNode.MoveToFirstChild();

                    var subtree = objectNode.ReadSubtree();

                    var objectSerializer = new DataContractSerializer(type);
                    var data             = objectSerializer.ReadObject(subtree);

                    Func <string, int> getIndexFromFileName = s => blob.Files.FindIndex(fe => fe.Name == s);

                    Console.WriteLine("Saving object...");
                    var saveResource = typeof(BlobDataWriter)
                                       .GetMethod("SaveObject", BindingFlags.Public | BindingFlags.Static)
                                       .MakeGenericMethod(type);
                    using (var output = File.Create(outputPath))
                    {
                        blob.Serialize(output);
                        saveResource.Invoke(
                            null,
                            new[] { data, output, schema.IsClient, schema.IsServer, getIndexFromFileName });
                    }
                }
            }
            else
            {
                throw new InvalidOperationException();
            }
        }
Example #6
0
        static void Main(string[] args)
        {
            string infPath;

            if (args.Length == 0)
            {
                infPath = Directory.GetCurrentDirectory();
            }
            else
            {
                infPath = args[0];
            }

            var md5             = MD5.Create();
            var extractLocation = Path.Combine(infPath, "BloExtraction");

            if (Directory.Exists(extractLocation))
            {
                Directory.CreateDirectory(extractLocation);
            }

            Console.SetCursorPosition(0, 0);
            Console.Write("Blo Extractor");
            Console.SetCursorPosition(0, 2);
            Console.Write("Working directory: " + infPath);

            foreach (var inputPath in Directory.GetFiles(infPath, "*.blo"))
            {
                var bloName     = Path.GetFileName(inputPath);
                var bloLocation = Path.Combine(extractLocation, bloName);

                if (!Directory.Exists(bloLocation))
                {
                    Directory.CreateDirectory(bloLocation);
                }

                using (var input = File.OpenRead(inputPath))
                {
                    var blob = new BlobFile();
                    blob.Deserialize(input);

                    Console.SetCursorPosition(0, 3);
                    Console.Write(String.Format("Current blo: {0}", bloName).PadRight(Console.WindowWidth));

                    foreach (var cfsEntry in blob.Entries)
                    {
                        input.Seek(cfsEntry.Offset, SeekOrigin.Begin);

                        var data = new byte[cfsEntry.Size];
                        input.Read(data, 0, data.Length);

                        var hash         = md5.ComputeHash(data);
                        var friendlyHash = BitConverter
                                           .ToString(hash)
                                           .Replace("-", "")
                                           .ToLowerInvariant();

                        Console.SetCursorPosition(0, 4);
                        Console.Write(String.Format("Current cfs: {0}", cfsEntry.Name).PadRight(Console.WindowWidth));

                        //Output the cfs file
                        string cfsFile = Path.Combine(bloLocation, cfsEntry.Name);
                        using (var output = File.Create(cfsFile))
                        {
                            output.Write(data, 0, data.Length);
                        }

                        //Output a settings file
                        using (var outputinfo = File.CreateText(Path.Combine(bloLocation, cfsEntry.Name + "-info.txt")))
                        {
                            outputinfo.WriteLine("name=" + cfsEntry.Name);
                            outputinfo.WriteLine("size=" + cfsEntry.Size);
                            outputinfo.WriteLine("md5=" + friendlyHash);
                            outputinfo.WriteLine("blo=" + bloName);
                            outputinfo.WriteLine("newname=none");
                            outputinfo.WriteLine("newblo=none");
                        }

                        //Lets try to create a sample image file, too
                        if (Path.GetExtension(cfsEntry.Name) != ".cfs")
                        {
                            continue;
                        }

                        string previewFile = Path.Combine(bloLocation, cfsEntry.Name + "-preview.png");
                        Helpers.GenerateCFSPreview(cfsFile, previewFile, true);
                    }
                }
            }
            Console.SetCursorPosition(0, 6);
            Console.Write("Blo Extraction complete...");
            Console.ReadKey();
        }
Example #7
0
        public static void Main(string[] args)
        {
            bool showHelp        = false;
            bool resourceMapping = true;
            bool generateBlobs   = false;
            bool stripDumbBlobs  = false;

            var options = new OptionSet()
            {
                {
                    "no-resource-mapping",
                    "don't do resource mapping for older level files",
                    v => resourceMapping = v == null
                },
                {
                    "g|generate-blobs",
                    "generate blobs for older level files when resource mapping fails",
                    v => generateBlobs = v != null
                },
                {
                    "s|strip-lvb-blob-references",
                    "strip references to .lvb.blo blob files",
                    v => stripDumbBlobs = v != null
                },
                {
                    "h|help",
                    "show this message and exit",
                    v => showHelp = v != null
                },
            };

            List <string> extras;

            try
            {
                extras = options.Parse(args);
            }
            catch (OptionException e)
            {
                Console.Write("{0}: ", GetExecutableName());
                Console.WriteLine(e.Message);
                Console.WriteLine("Try `{0} --help' for more information.", GetExecutableName());
                return;
            }

            if (extras.Count < 1 || extras.Count > 2 || showHelp == true)
            {
                Console.WriteLine("Usage: {0} [OPTIONS]+ input_lvl [output_map]", GetExecutableName());
                Console.WriteLine();
                Console.WriteLine("Options:");
                options.WriteOptionDescriptions(Console.Out);
                return;
            }

            string inputPath  = extras[0];
            string outputPath = extras.Count > 1 ? extras[1] : Path.ChangeExtension(inputPath, ".map");
            string lvbPath    = Path.ChangeExtension(inputPath, ".lvb");
            string lvbName    = Path.GetFileName(lvbPath);

            var level = new LevelFile();

            using (var input = File.OpenRead(inputPath))
            {
                level.Deserialize(input);
            }

            if (stripDumbBlobs == true)
            {
                for (int i = 0; i < level.Floors.Count; i++)
                {
                    var floor = level.Floors[i];
                    if (floor.FileName != null &&
                        floor.FileName.EndsWith(".lvb.blo") == true)
                    {
                        floor.FileName  = null;
                        level.Floors[i] = floor;
                    }
                }

                for (int i = 0; i < level.Objects.Count; i++)
                {
                    var obj = level.Objects[i];
                    if (obj.FileName != null &&
                        obj.FileName.EndsWith(".lvb.blo") == true)
                    {
                        obj.FileName     = null;
                        level.Objects[i] = obj;
                    }
                }
            }

            var hasLevelBlob =
                level.Floors.Any(f => f.FileName == null) == true ||
                level.Objects.Any(o => o.FileName == null) == true;

            var remap =
                new Dictionary <string, string>();

            if (hasLevelBlob == true)
            {
                // TODO: decrap this block of code

                var resources = resourceMapping == true
                                    ? LoadBloTable()
                                    : new SortedDictionary <string, string>();

                if (File.Exists(lvbPath) == false)
                {
                    Console.WriteLine("Could not open '{0}'!", lvbPath);
                    Console.WriteLine();
                    Console.WriteLine("Mapping of LVB resources to real locations isn't going to happen.");
                }
                else
                {
                    using (var lvb = File.OpenRead(lvbPath))
                    {
                        var levelBlob = new BlobFile();
                        levelBlob.Deserialize(lvb);

                        var md5 = MD5.Create();

                        var unknownEntries = new List <BlobFile.Entry>();

                        foreach (var entry in levelBlob.Entries)
                        {
                            lvb.Seek(entry.Offset, SeekOrigin.Begin);

                            var data = new byte[entry.Size];
                            if (lvb.Read(data, 0, data.Length) != data.Length)
                            {
                                throw new FormatException();
                            }

                            var hash         = md5.ComputeHash(data);
                            var friendlyHash = BitConverter
                                               .ToString(hash)
                                               .Replace("-", "")
                                               .ToLowerInvariant();

                            if (resources.ContainsKey(friendlyHash) == true)
                            {
                                remap[entry.Name] = resources[friendlyHash];
                            }
                            else
                            {
                                Console.WriteLine("Could not find real location for '{0},{1}'.", lvbName, entry.Name);
                                unknownEntries.Add(entry);
                            }
                        }

                        if (unknownEntries.Count > 0)
                        {
                            Console.WriteLine("Could not find all of the level blob resources.");

                            if (generateBlobs == true)
                            {
                                // ReSharper disable JoinDeclarationAndInitializer
                                string floorBlobName;
                                // ReSharper restore JoinDeclarationAndInitializer

                                floorBlobName  = "f_!";
                                floorBlobName += Path.GetFileNameWithoutExtension(inputPath);
                                floorBlobName  = Path.ChangeExtension(floorBlobName, ".blo");

                                // ReSharper disable JoinDeclarationAndInitializer
                                string floorBlobPath;
                                // ReSharper restore JoinDeclarationAndInitializer

                                floorBlobPath = Path.GetDirectoryName(inputPath);
                                if (floorBlobPath == null)
                                {
                                    throw new InvalidOperationException();
                                }

                                floorBlobPath = Path.Combine(floorBlobPath, floorBlobName);

                                Console.WriteLine("Creating '{0}'...", floorBlobPath);

                                using (var output = File.Create(floorBlobPath))
                                {
                                    var floorBlob = new BlobFile
                                    {
                                        Version = 2,
                                    };

                                    // generate fake entries
                                    var floors = unknownEntries
                                                 .Where(f => f.Name.StartsWith("f"))
                                                 .ToArray();

                                    foreach (var floor in floors)
                                    {
                                        floorBlob.Entries.Add(new BlobFile.Entry()
                                        {
                                            Name = floor.Name,
                                        });
                                    }
                                    floorBlob.Serialize(output);

                                    // generate real entries
                                    floorBlob.Entries.Clear();
                                    foreach (var floor in floors)
                                    {
                                        Console.WriteLine("  adding '{0}'", floor.Name);

                                        lvb.Seek(floor.Offset, SeekOrigin.Begin);

                                        floorBlob.Entries.Add(new BlobFile.Entry()
                                        {
                                            Name   = floor.Name,
                                            Offset = output.Position,
                                            Size   = floor.Size,
                                        });

                                        output.WriteFromStream(lvb, floor.Size);

                                        remap[floor.Name] = string.Format("{0},{1}", floorBlobName, floor.Name);
                                    }

                                    output.Seek(0, SeekOrigin.Begin);
                                    floorBlob.Serialize(output);
                                }

                                // ReSharper disable JoinDeclarationAndInitializer
                                string objectBlobName;
                                // ReSharper restore JoinDeclarationAndInitializer

                                objectBlobName  = "o_!";
                                objectBlobName += Path.GetFileNameWithoutExtension(inputPath);
                                objectBlobName  = Path.ChangeExtension(objectBlobName, ".blo");

                                // ReSharper disable JoinDeclarationAndInitializer
                                string objectBlobPath;
                                // ReSharper restore JoinDeclarationAndInitializer

                                objectBlobPath = Path.GetDirectoryName(inputPath);
                                if (objectBlobPath == null)
                                {
                                    throw new InvalidOperationException();
                                }

                                objectBlobPath = Path.Combine(objectBlobPath, objectBlobName);

                                Console.WriteLine("Creating '{0}'...", objectBlobPath);

                                using (var output = File.Create(objectBlobPath))
                                {
                                    var objectBlob = new BlobFile
                                    {
                                        Version = 2,
                                    };

                                    // generate fake entries
                                    var objects = unknownEntries
                                                  .Where(o => o.Name.StartsWith("o"))
                                                  .ToArray();
                                    foreach (var obj in objects)
                                    {
                                        objectBlob.Entries.Add(new BlobFile.Entry()
                                        {
                                            Name = obj.Name,
                                        });
                                    }
                                    objectBlob.Serialize(output);

                                    // generate real entries
                                    objectBlob.Entries.Clear();
                                    foreach (var obj in objects)
                                    {
                                        Console.WriteLine("  adding '{0}'", obj.Name);

                                        lvb.Seek(obj.Offset, SeekOrigin.Begin);

                                        objectBlob.Entries.Add(new BlobFile.Entry()
                                        {
                                            Name   = obj.Name,
                                            Offset = output.Position,
                                            Size   = obj.Size,
                                        });

                                        output.WriteFromStream(lvb, obj.Size);

                                        remap[obj.Name] = string.Format("{0},{1}", objectBlobName, obj.Name);
                                    }

                                    output.Seek(0, SeekOrigin.Begin);
                                    objectBlob.Serialize(output);
                                }
                            }
                        }
                    }
                }
            }

            using (var output = File.Create(outputPath))
            {
                var header = new Map.Header
                {
                    Version     = 9,
                    Width       = level.Width,
                    Height      = level.Height,
                    EntityCount = level.Entities.Count,
                    PhysicsLow  = new short[32],
                };

                // not right? DERP
                //header.OffsetX = level.OffsetX;
                //header.OffsetY = level.OffsetY;

                Array.Copy(level.PhysicsLow, header.PhysicsLow, level.PhysicsLow.Length);
                header.PhysicsHigh = new short[32];
                Array.Copy(level.PhysicsHigh, header.PhysicsHigh, level.PhysicsHigh.Length);

                header.LightColorWhite = level.LightColorWhite;
                header.LightColorRed   = level.LightColorRed;
                header.LightColorGreen = level.LightColorGreen;
                header.LightColorBlue  = level.LightColorBlue;

                output.WriteStructure(header);

                for (int i = 0; i < 8192; i++)
                {
                    if (i < level.TerrainIds.Length)
                    {
                        output.WriteValueU8((byte)level.TerrainIds[i]);
                    }
                    else
                    {
                        output.WriteValueU8(0);
                    }
                }

                for (int i = 0; i < 2048; i++)
                {
                    var reference = new Map.BlobReference();

                    if (i < level.Floors.Count)
                    {
                        var floor = level.Floors[i];

                        if (floor.FileName == null)
                        {
                            if (remap.ContainsKey(floor.Id) == true)
                            {
                                reference.Path = remap[floor.Id];
                            }
                            else
                            {
                                reference.Path = string.Format("{0},{1}",
                                                               lvbName,
                                                               floor.Id);
                            }
                        }
                        else
                        {
                            reference.Path = string.Format("{0},{1}",
                                                           floor.FileName,
                                                           floor.Id);
                        }
                    }

                    output.WriteStructure(reference);
                }

                var tiles = new byte[level.Width * level.Height * 4];
                // ReSharper disable UnusedVariable
                int offset = 0;
                // ReSharper restore UnusedVariable

                for (int i = 0, j = 0; i < level.Tiles.Length; i++, j += 4)
                {
                    tiles[j + 0]  = level.Tiles[i].BitsA;
                    tiles[j + 0] &= 0x7F;
                    tiles[j + 1]  = 0;
                    tiles[j + 2]  = level.Tiles[i].BitsC;
                    tiles[j + 3]  = level.Tiles[i].BitsB;
                }

                using (var rle = new MemoryStream())
                {
                    rle.WriteRLE(tiles, 4, level.Tiles.Length, false);
                    rle.Position = 0;

                    output.WriteValueS32((int)rle.Length);
                    output.WriteFromStream(rle, rle.Length);
                }

                foreach (var t in level.Entities)
                {
                    output.WriteStructure(t);

                    var obj = level.Objects[t.ObjectId];

                    var reference = new Map.BlobReference();

                    if (obj.FileName == null)
                    {
                        if (remap.ContainsKey(obj.Id) == true)
                        {
                            reference.Path = remap[obj.Id];
                        }
                        else
                        {
                            reference.Path = string.Format("{0},{1}",
                                                           lvbName,
                                                           obj.Id);
                        }
                    }
                    else
                    {
                        reference.Path = string.Format("{0},{1}",
                                                       obj.FileName,
                                                       obj.Id);
                    }

                    output.WriteStructure(reference);
                }
            }
        }
Example #8
0
        public static void Main(string[] args)
        {
            bool showHelp = false;

            var options = new OptionSet()
            {
                {
                    "h|help",
                    "show this message and exit",
                    v => showHelp = v != null
                },
            };

            List <string> extras;

            try
            {
                extras = options.Parse(args);
            }
            catch (OptionException e)
            {
                Console.Write("{0}: ", GetExecutableName());
                Console.WriteLine(e.Message);
                Console.WriteLine("Try `{0} --help' for more information.", GetExecutableName());
                return;
            }

            if (extras.Count < 0 || extras.Count > 1 || showHelp == true)
            {
                Console.WriteLine("Usage: {0} [OPTIONS]+ [input_directory]", GetExecutableName());
                Console.WriteLine();
                Console.WriteLine("Options:");
                options.WriteOptionDescriptions(Console.Out);
                return;
            }

            var directoryPath = extras.Count == 0
                                    ? Directory.GetCurrentDirectory()
                                    : extras[0];

            var md5       = MD5.Create();
            var resources = new SortedDictionary <string, List <ResourceLocation> >();

            foreach (var inputPath in Directory.GetFiles(directoryPath, "*.blo"))
            {
                var fileName = Path.GetFileName(inputPath);
                if (fileName == null)
                {
                    continue;
                }

                if (fileName.StartsWith("o_") == false &&
                    fileName.StartsWith("f_") == false)
                {
                    continue;
                }

                if (fileName.EndsWith(".lvb.blo") == true)
                {
                    continue;
                }

                Console.WriteLine("Processing '{0}'...", Path.GetFileName(inputPath));

                using (var input = File.OpenRead(inputPath))
                {
                    var blob = new BlobFile();
                    blob.Deserialize(input);

                    foreach (var entry in blob.Entries)
                    {
                        Console.WriteLine("  Hashing '{0}'", entry.Name);

                        input.Seek(entry.Offset, SeekOrigin.Begin);

                        var data = new byte[entry.Size];
                        if (input.Read(data, 0, data.Length) != data.Length)
                        {
                            throw new FormatException();
                        }

                        var hash         = md5.ComputeHash(data);
                        var friendlyHash = BitConverter
                                           .ToString(hash)
                                           .Replace("-", "")
                                           .ToLowerInvariant();

                        if (resources.ContainsKey(friendlyHash) == false)
                        {
                            resources[friendlyHash] = new List <ResourceLocation>();
                        }

                        resources[friendlyHash].Add(
                            new ResourceLocation()
                        {
                            FileName = fileName.ToLowerInvariant(),
                            Id       = entry.Name.ToLowerInvariant(),
                        });
                    }
                }
            }

            var custom = LoadBloTableCustom();

            // ReSharper disable JoinDeclarationAndInitializer
            string outputPath;

            // ReSharper restore JoinDeclarationAndInitializer

            outputPath = Path.GetDirectoryName(GetExecutablePath());
            if (outputPath == null)
            {
                throw new InvalidOperationException();
            }

            outputPath = Path.Combine(outputPath, "blotable.xml");

            var settings = new XmlWriterSettings()
            {
                Indent = true,
            };

            using (var xml = XmlWriter.Create(outputPath, settings))
            {
                xml.WriteStartDocument();
                xml.WriteStartElement("resources");

                xml.WriteStartElement("auto");
                foreach (var resource in resources)
                {
                    xml.WriteStartElement("resource");
                    xml.WriteAttributeString("hash", resource.Key);

                    foreach (var location in resource.Value)
                    {
                        xml.WriteStartElement("source");
                        xml.WriteAttributeString("filename", location.FileName);
                        xml.WriteAttributeString("id", location.Id);
                        xml.WriteEndElement();
                    }

                    xml.WriteEndElement();
                }
                xml.WriteEndElement();

                if (custom.Count > 0)
                {
                    xml.WriteStartElement("custom");
                    foreach (var resource in custom)
                    {
                        xml.WriteStartElement("resource");
                        xml.WriteAttributeString("hash", resource.Key);

                        foreach (var location in resource.Value)
                        {
                            xml.WriteStartElement("source");
                            xml.WriteAttributeString("filename", location.FileName);
                            xml.WriteAttributeString("id", location.Id);
                            xml.WriteEndElement();
                        }

                        xml.WriteEndElement();
                    }
                    xml.WriteEndElement();
                }

                xml.WriteEndElement();
                xml.WriteEndDocument();
            }
        }