using System; using System.Collections.Generic; using System.IO; using Oni.Imaging; namespace Oni.Akira { internal class AkiraDatWriter { #region Private data private Importer importer; private string name; private bool debug; private PolygonMesh source; private List polygons = new List(); private Dictionary polygonMap = new Dictionary(); private UniqueList points = new UniqueList(); private UniqueList texCoords = new UniqueList(); private UniqueList materials = new UniqueList(); private UniqueList planes = new UniqueList(); private List alphaBspNodes = new List(); private List rooms = new List(); private Dictionary roomMap = new Dictionary(); private List roomBspNodes = new List(); private List roomSides = new List(); private List roomAdjacencies = new List(); private DatOctree octree; #endregion #region private class UniqueList private class UniqueList : ICollection { private readonly List list = new List(); private readonly Dictionary indices = new Dictionary(); public int Add(T t) { int index; if (!indices.TryGetValue(t, out index)) { index = list.Count; indices.Add(t, index); list.Add(t); } return index; } #region ICollection Members void ICollection.Add(T item) => Add(item); public int Count => list.Count; void ICollection.Clear() { list.Clear(); indices.Clear(); } bool ICollection.Contains(T item) => indices.ContainsKey(item); void ICollection.CopyTo(T[] array, int arrayIndex) { list.CopyTo(array, arrayIndex); } bool ICollection.IsReadOnly => false; bool ICollection.Remove(T item) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => list.GetEnumerator(); #endregion } #endregion #region private class DatObject private class DatObject { private readonly T source; public DatObject(T source) { this.source = source; } public T Source => source; } #endregion #region private class DatPolygon private class DatPolygon : DatObject { private static readonly Color defaultColor = new Color(207, 207, 207, 255); private static readonly Color[] defaultColors = new[] { defaultColor, defaultColor, defaultColor, defaultColor }; private GunkFlags flags; private readonly int index; public readonly int[] PointIndices = new int[4]; public readonly int[] TexCoordIndices = new int[4]; public readonly Color[] Colors = new Color[4]; private readonly int objectId; private readonly int scriptId; private int materialIndex; private int planeIndex; public DatPolygon(Polygon source, int index, UniqueList points, UniqueList texCoords) : base(source) { this.index = index; this.scriptId = source.ScriptId; objectId = source.ObjectType << 24 | (source.ObjectId & 0xffffff); flags = source.Flags; if (source.VertexCount == 3) { flags |= GunkFlags.Triangle; Array.Copy(source.PointIndices, PointIndices, 3); PointIndices[3] = PointIndices[2]; Array.Copy(source.TexCoordIndices, TexCoordIndices, 3); TexCoordIndices[3] = TexCoordIndices[2]; if (source.Colors == null) { Colors = defaultColors; } else { Array.Copy(source.Colors, Colors, 3); Colors[3] = Colors[2]; } } else { Array.Copy(source.PointIndices, PointIndices, 4); Array.Copy(source.TexCoordIndices, TexCoordIndices, 4); if (source.Colors != null) Colors = source.Colors; else Colors = defaultColors; } for (int i = 0; i < 4; i++) { PointIndices[i] = points.Add(source.Mesh.Points[PointIndices[i]]); TexCoordIndices[i] = texCoords.Add(source.Mesh.TexCoords[TexCoordIndices[i]]); } } public int Index => index; public int ObjectId => objectId; public int ScriptId => scriptId; public GunkFlags Flags { get { return flags; } set { flags = value; } } public int MaterialIndex { get { return materialIndex; } set { materialIndex = value; } } public int PlaneIndex { get { return planeIndex; } set { planeIndex = value; } } } #endregion #region private class DatBspNode private class DatBspNode : DatObject where T : BspNode { public int PlaneIndex; public int FrontChildIndex = -1; public int BackChildIndex = -1; public DatBspNode(T source) : base(source) { } } #endregion #region private class DatAlphaBspNode private class DatAlphaBspNode : DatBspNode { public readonly int PolygonIndex; public DatAlphaBspNode(AlphaBspNode source, int polygonIndex) : base(source) { PolygonIndex = polygonIndex; } } #endregion #region private class DatRoom private class DatRoom : DatObject { private readonly int index; private readonly RoomFlags flags; public int BspTreeIndex; public int SideListStart; public int SideListEnd; public int ChildIndex = -1; public int SiblingIndex = -1; public byte[] CompressedGridData; public byte[] DebugData; public DatRoom(Room source, int index) : base(source) { this.index = index; this.flags = RoomFlags.Room; if (source.FloorPlane.Normal.Y < 0.999f) this.flags |= RoomFlags.Stairs; } public int Index => index; public int Flags => (int)flags; } #endregion #region private class DatRoomBspNode private class DatRoomBspNode : DatBspNode { public DatRoomBspNode(RoomBspNode source) : base(source) { } } #endregion #region private class DatRoomSide private class DatRoomSide { public int AdjacencyListStart; public int AdjacencyListEnd; } #endregion #region private class DatRoomAdjacency private class DatRoomAdjacency : DatObject { public DatRoomAdjacency(RoomAdjacency source) : base(source) { } public int AdjacentRoomIndex; public int GhostIndex; } #endregion #region private class DatOctree private class DatOctree { public int[] Nodes; public int[] QuadTrees; public int[] Adjacency; public DatOctreeBoundingBox[] BoundingBoxes; public DatOctreePolygonRange[] PolygonLists; public DatOctreeBnvRange[] BnvLists; public int[] PolygonIndex; public int[] BnvIndex; } #endregion #region private struct DatOctreeBoundingBox private struct DatOctreeBoundingBox { private readonly uint value; public DatOctreeBoundingBox(BoundingBox bbox) { int size = (int)(Math.Log(bbox.Max.X - bbox.Min.X, 2)) - 4; int maxX = (int)(bbox.Max.X + 4080.0f); int maxY = (int)(bbox.Max.Y + 4080.0f); int maxZ = (int)(bbox.Max.Z + 4080.0f); value = (uint)((maxX << 14) | (maxY << 5) | (maxZ >> 4) | (size << 27)); } public uint PackedValue => value; } #endregion #region private struct DatOctreeBnvRange private struct DatOctreeBnvRange { private const int indexBitOffset = 8; private const int lengthBitMask = 255; private readonly uint value; public DatOctreeBnvRange(int start, int length) { ValidateRange(start, length); value = (uint)((start << indexBitOffset) | (length & lengthBitMask)); } private static void ValidateRange(int start, int length) { if (start > 16777215) throw new ArgumentException(string.Format("Invalid bnv list start index {0}", start), "start"); if (length > 255) throw new ArgumentException(string.Format("Invalid bnv list length {0}", length), "length"); } public uint PackedValue => value; } #endregion #region private struct DatOctreePolygonRange private struct DatOctreePolygonRange { private const int indexBitOffset = 12; private const int lengthBitMask = 4095; private readonly uint value; public DatOctreePolygonRange(int start, int length) { //ValidateRange(start, length); if (start > 1048575) { start = 1048575; length = 0; } value = (uint)((start << indexBitOffset) | (length & lengthBitMask)); } private static void ValidateRange(int start, int length) { if (start > 1048575) throw new ArgumentException(string.Format("Invalid quad list start index {0}", start), "start"); if (length > 4095) throw new ArgumentException(string.Format("Invalid quad list length {0}", length), "length"); } public uint PackedValue => value; } #endregion public static void Write(PolygonMesh mesh, Importer importer, string name, bool debug) { var writer = new AkiraDatWriter { name = name, importer = importer, source = mesh, debug = debug }; writer.Write(); } private void Write() { Console.Error.WriteLine("Environment bounding box is {0}", source.GetBoundingBox()); RoomBuilder.BuildRooms(source); ConvertPolygons(source.Polygons); ConvertPolygons(source.Doors); ConvertPolygons(source.Ghosts); ConvertAlphaBspTree(AlphaBspBuilder.Build(source, debug)); ConvertRooms(); ConvertOctree(); WriteAKEV(); //foreach (Material material in materials) //{ // if (File.Exists(material.ImageFilePath)) // importer.AddDependency(material.ImageFilePath, TemplateTag.TXMP); //} } private void ConvertPolygons(List sourcePolygons) { foreach (var polygon in sourcePolygons) { if (polygon.VertexCount > 4) { Console.Error.WriteLine("Geometry '{0}' has a {1}-gon, ignoring.", polygon.ObjectName, polygon.VertexCount); continue; } if (polygon.TexCoordIndices == null) { Console.Error.WriteLine("Geometry '{0}' does not contain texture coordinates, ignoring.", polygon.ObjectName); continue; } var datPolygon = new DatPolygon(polygon, polygons.Count, points, texCoords) { PlaneIndex = planes.Add(polygon.Plane), MaterialIndex = materials.Add(polygon.Material) }; polygons.Add(datPolygon); polygonMap.Add(polygon, datPolygon); } //var noneMaterial = source.Materials.GetMaterial("NONE"); //int noneMaterialIndex = materials.Add(noneMaterial); //foreach (var polygon in polygons) //{ // var material = polygon.Source.Material; //if (material.IsMarker) //{ // polygon.MaterialIndex = noneMaterialIndex; // if (material != source.Materials.Markers.Blackness && (material.Flags & GunkFlags.Invisible) == 0) // polygon.Flags |= GunkFlags.Transparent; //} //else //{ // polygon.MaterialIndex = materials.Add(material); //} //} } private int ConvertAlphaBspTree(AlphaBspNode source) { if (source == null) return -1; int index = alphaBspNodes.Count; var node = new DatAlphaBspNode(source, polygonMap[source.Polygon].Index) { PlaneIndex = planes.Add(source.Plane) }; alphaBspNodes.Add(node); if (source.FrontChild != null) node.FrontChildIndex = ConvertAlphaBspTree((AlphaBspNode)source.FrontChild); if (source.BackChild != null) node.BackChildIndex = ConvertAlphaBspTree((AlphaBspNode)source.BackChild); return index; } private void ConvertRooms() { foreach (var room in source.Rooms) { var datRoom = new DatRoom(room, rooms.Count) { BspTreeIndex = ConvertRoomBspTree(room.BspTree), CompressedGridData = room.Grid.Compress(), DebugData = room.Grid.DebugData, SideListStart = roomSides.Count }; if (room.Ajacencies.Count > 0) { var datSide = new DatRoomSide { AdjacencyListStart = roomAdjacencies.Count }; foreach (var adjacency in room.Ajacencies) { roomAdjacencies.Add(new DatRoomAdjacency(adjacency) { AdjacentRoomIndex = source.Rooms.IndexOf(adjacency.AdjacentRoom), GhostIndex = polygonMap[adjacency.Ghost].Index }); } datSide.AdjacencyListEnd = roomAdjacencies.Count; roomSides.Add(datSide); } datRoom.SideListEnd = roomSides.Count; rooms.Add(datRoom); roomMap.Add(room, datRoom); } } private int ConvertRoomBspTree(RoomBspNode node) { int index = roomBspNodes.Count; var datNode = new DatRoomBspNode(node) { PlaneIndex = planes.Add(node.Plane) }; roomBspNodes.Add(datNode); if (node.FrontChild != null) datNode.FrontChildIndex = ConvertRoomBspTree(node.FrontChild); if (node.BackChild != null) datNode.BackChildIndex = ConvertRoomBspTree(node.BackChild); return index; } private void ConvertOctree() { Console.Error.WriteLine("Building octtree for {0} polygons...", source.Polygons.Count); var root = OctreeBuilder.Build(source, debug); var nodeList = new List(); var leafList = new List(); int quadListLength = 0; int roomListLength = 0; // // Assign indices to nodes/leafs and compute the length of the quad and room indexes. // root.DfsTraversal(node => { if (node.IsLeaf) { node.Index = leafList.Count; leafList.Add(node); quadListLength += node.Polygons.Count; roomListLength += node.Rooms.Count; } else { node.Index = nodeList.Count; nodeList.Add(node); } }); // // Create the octtree data structure that will be written to the file. // octree = new DatOctree { Nodes = new int[nodeList.Count * OctreeNode.ChildCount], Adjacency = new int[leafList.Count * OctreeNode.FaceCount], BoundingBoxes = new DatOctreeBoundingBox[leafList.Count], PolygonIndex = new int[quadListLength], PolygonLists = new DatOctreePolygonRange[leafList.Count], BnvLists = new DatOctreeBnvRange[leafList.Count], BnvIndex = new int[roomListLength] }; Console.WriteLine("Octtree: {0} interior nodes, {1} leafs", nodeList.Count, leafList.Count); // // Populate the node array. // The octree builder stores child nodes in a different order than Oni (by mistake) // var interiorNodeIndexRemap = new int[] { 0, 4, 2, 6, 1, 5, 3, 7 }; var datOcttree = octree; foreach (var node in nodeList) { int i = node.Index * OctreeNode.ChildCount; for (int j = 0; j < OctreeNode.ChildCount; j++) { var child = node.Children[j]; int k = interiorNodeIndexRemap[j]; if (child.IsLeaf) datOcttree.Nodes[i + k] = child.Index | int.MinValue; else datOcttree.Nodes[i + k] = child.Index; } } // // Generate the data needed by the leafs: bounding box, quad range and room range. // int quadListIndex = 0; int bnvListIndex = 0; foreach (var leaf in leafList) { datOcttree.BoundingBoxes[leaf.Index] = new DatOctreeBoundingBox(leaf.BoundingBox); if (leaf.Polygons.Count > 0) { datOcttree.PolygonLists[leaf.Index] = new DatOctreePolygonRange(quadListIndex, leaf.Polygons.Count); foreach (var polygon in leaf.Polygons) datOcttree.PolygonIndex[quadListIndex++] = polygonMap[polygon].Index; } if (leaf.Rooms.Count > 0) { datOcttree.BnvLists[leaf.Index] = new DatOctreeBnvRange(bnvListIndex, leaf.Rooms.Count); foreach (var room in leaf.Rooms) datOcttree.BnvIndex[bnvListIndex++] = roomMap[room].Index; } } // // Generate the quad trees. Note that the octtree builder doesn't build quad trees because // they're only needed when writing the octtree to the file. Currently OniSplit doesn't use // the octtree for raycasting. // var quadTrees = new List(); foreach (var leaf in leafList) { leaf.RefineAdjacency(); foreach (var face in OctreeNode.Face.All) { var adjacentLeaf = leaf.Adjacency[face.Index]; int index = leaf.Index * OctreeNode.FaceCount + face.Index; if (adjacentLeaf == null) { // // There's no adjacent node or leaf, this should only happen // on the edges of the octtree. // datOcttree.Adjacency[index] = -1; } else if (adjacentLeaf.IsLeaf) { // // The adjacent node is a leaf, there's no need for a quad tree. // datOcttree.Adjacency[index] = adjacentLeaf.Index | int.MinValue; } else { // // The adjacent node has children, a quad tree needs to be built. // int quadTreeBaseIndex = quadTrees.Count / 4; datOcttree.Adjacency[index] = quadTreeBaseIndex; var quadTreeRoot = leaf.BuildFaceQuadTree(face); foreach (var node in quadTreeRoot.GetDfsList()) { for (int i = 0; i < 4; i++) { if (node.Nodes[i] != null) quadTrees.Add(quadTreeBaseIndex + node.Nodes[i].Index); else quadTrees.Add(node.Leafs[i].Index | int.MinValue); } } } } } datOcttree.QuadTrees = quadTrees.ToArray(); } private void WriteAKEV() { var akev = importer.CreateInstance(TemplateTag.AKEV, name); var pnta = importer.CreateInstance(TemplateTag.PNTA); var plea = importer.CreateInstance(TemplateTag.PLEA); var txca = importer.CreateInstance(TemplateTag.TXCA); var agqg = importer.CreateInstance(TemplateTag.AGQG); var agqr = importer.CreateInstance(TemplateTag.AGQR); var agqc = importer.CreateInstance(TemplateTag.AGQC); var agdb = importer.CreateInstance(TemplateTag.AGDB); var txma = importer.CreateInstance(TemplateTag.TXMA); var akva = importer.CreateInstance(TemplateTag.AKVA); var akba = importer.CreateInstance(TemplateTag.AKBA); var idxa1 = importer.CreateInstance(TemplateTag.IDXA); var idxa2 = importer.CreateInstance(TemplateTag.IDXA); var akbp = importer.CreateInstance(TemplateTag.AKBP); var abna = importer.CreateInstance(TemplateTag.ABNA); var akot = importer.CreateInstance(TemplateTag.AKOT); var akaa = importer.CreateInstance(TemplateTag.AKAA); var akda = importer.CreateInstance(TemplateTag.AKDA); using (var writer = akev.OpenWrite()) { writer.Write(pnta); writer.Write(plea); writer.Write(txca); writer.Write(agqg); writer.Write(agqr); writer.Write(agqc); writer.Write(agdb); writer.Write(txma); writer.Write(akva); writer.Write(akba); writer.Write(idxa1); writer.Write(idxa2); writer.Write(akbp); writer.Write(abna); writer.Write(akot); writer.Write(akaa); writer.Write(akda); writer.Write(source.GetBoundingBox()); writer.Skip(24); writer.Write(12.0f); } pnta.WritePoints(points); plea.WritePlanes(planes); txca.WriteTexCoords(texCoords); WriteAGQG(agqg); WriteAGQR(agqr); WriteAGQC(agqc); WriteTXMA(txma); WriteAKVA(akva); WriteAKBA(akba); WriteAKBP(akbp); WriteABNA(abna); WriteAKOT(akot); WriteAKAA(akaa); WriteAKDA(akda); WriteScriptIds(idxa1, idxa2); WriteAGDB(agdb); } private void WriteAGQG(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(polygons.Count); foreach (var polygon in polygons) { writer.Write(polygon.PointIndices); writer.Write(polygon.TexCoordIndices); writer.Write(polygon.Colors); writer.Write((uint)polygon.Flags); writer.Write(polygon.ObjectId); } } } private void WriteAGQC(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(polygons.Count); foreach (var polygon in polygons) { writer.Write(polygon.PlaneIndex); writer.Write(polygon.Source.BoundingBox); } } } private void WriteAGQR(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(polygons.Count); foreach (var polygon in polygons) { writer.Write(polygon.MaterialIndex); } } } private void WriteTXMA(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(materials.Count); foreach (var material in materials) { writer.Write(importer.CreateInstance(TemplateTag.TXMP, material.Name)); } } } private void WriteABNA(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(alphaBspNodes.Count); foreach (var node in alphaBspNodes) { writer.Write(node.PolygonIndex); writer.Write(node.PlaneIndex); writer.Write(node.FrontChildIndex); writer.Write(node.BackChildIndex); } } } private void WriteAKVA(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(rooms.Count); foreach (var room in rooms) { writer.Write(room.BspTreeIndex); writer.Write(room.Index); writer.Write(room.SideListStart); writer.Write(room.SideListEnd); writer.Write(room.ChildIndex); writer.Write(room.SiblingIndex); writer.Skip(4); writer.Write(room.Source.Grid.XTiles); writer.Write(room.Source.Grid.ZTiles); writer.Write(importer.WriteRawPart(room.CompressedGridData)); writer.Write(room.CompressedGridData.Length); writer.Write(room.Source.Grid.TileSize); writer.Write(room.Source.BoundingBox); writer.WriteInt16(room.Source.Grid.XOrigin); writer.WriteInt16(room.Source.Grid.ZOrigin); writer.Write(room.Index); writer.Skip(4); if (room.DebugData != null) { writer.Write(room.DebugData.Length); writer.Write(importer.WriteRawPart(room.DebugData)); } else { writer.Write(0); writer.Write(0); } writer.Write(room.Flags); writer.Write(room.Source.FloorPlane); writer.Write(room.Source.Height); } } } private void WriteAKBA(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(roomSides.Count); foreach (var side in roomSides) { writer.Write(0); writer.Write(side.AdjacencyListStart); writer.Write(side.AdjacencyListEnd); writer.Skip(16); } } } private void WriteAKBP(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(22)) { writer.WriteUInt16(roomBspNodes.Count); foreach (var node in roomBspNodes) { writer.Write(node.PlaneIndex); writer.Write(node.BackChildIndex); writer.Write(node.FrontChildIndex); } } } private void WriteAKAA(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(roomAdjacencies.Count); foreach (var adjacency in roomAdjacencies) { writer.Write(adjacency.AdjacentRoomIndex); writer.Write(adjacency.GhostIndex); writer.Write(0); } } } private void WriteAKDA(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(0); } } private void WriteAKOT(ImporterDescriptor akot) { var otit = importer.CreateInstance(TemplateTag.OTIT); var otlf = importer.CreateInstance(TemplateTag.OTLF); var qtna = importer.CreateInstance(TemplateTag.QTNA); var idxa1 = importer.CreateInstance(TemplateTag.IDXA); var idxa2 = importer.CreateInstance(TemplateTag.IDXA); using (var writer = akot.OpenWrite()) { writer.Write(otit); writer.Write(otlf); writer.Write(qtna); writer.Write(idxa1); writer.Write(idxa2); } WriteOTIT(otit); WriteOTLF(otlf); WriteQTNA(qtna); idxa1.WriteIndices(octree.PolygonIndex); idxa2.WriteIndices(octree.BnvIndex); } private void WriteOTIT(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(octree.Nodes.Length / OctreeNode.ChildCount); writer.Write(octree.Nodes); } } private void WriteOTLF(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(octree.BoundingBoxes.Length); for (int i = 0; i < octree.BoundingBoxes.Length; i++) { writer.Write(octree.PolygonLists[i].PackedValue); writer.Write(octree.Adjacency, i * OctreeNode.FaceCount, OctreeNode.FaceCount); writer.Write(octree.BoundingBoxes[i].PackedValue); writer.Write(octree.BnvLists[i].PackedValue); } } } private void WriteQTNA(ImporterDescriptor descriptor) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(octree.QuadTrees.Length / 4); writer.Write(octree.QuadTrees); } } private void WriteScriptIds(ImporterDescriptor idxa1, ImporterDescriptor idxa2) { var scriptIdMap = new List>(256); foreach (var polygon in polygons) { if (polygon.ScriptId != 0) scriptIdMap.Add(new KeyValuePair(polygon.ScriptId, polygon.Index)); } scriptIdMap.Sort((x, y) => x.Key.CompareTo(y.Key)); var scriptIds = new int[scriptIdMap.Count]; var polygonIndices = new int[scriptIdMap.Count]; for (int i = 0; i < scriptIdMap.Count; i++) { scriptIds[i] = scriptIdMap[i].Key; polygonIndices[i] = scriptIdMap[i].Value; } idxa1.WriteIndices(polygonIndices); idxa2.WriteIndices(scriptIds); } private void WriteAGDB(ImporterDescriptor descriptor) { if (!debug) { using (var writer = descriptor.OpenWrite(20)) { writer.Write(0); } return; } var objectNames = new Dictionary(polygons.Count, StringComparer.Ordinal); var fileNames = new Dictionary(polygons.Count, StringComparer.Ordinal); using (var writer = descriptor.OpenWrite(20)) { writer.Write(polygons.Count); foreach (var polygon in polygons) { var objectName = polygon.Source.ObjectName; var fileName = polygon.Source.FileName; if (string.IsNullOrEmpty(objectName)) objectName = "(none)"; if (string.IsNullOrEmpty(fileName)) fileName = "(none)"; int objectOffset; int fileOffset; if (!objectNames.TryGetValue(objectName, out objectOffset)) { objectOffset = importer.WriteRawPart(objectName); objectNames.Add(objectName, objectOffset); } if (!fileNames.TryGetValue(fileName, out fileOffset)) { fileOffset = importer.WriteRawPart(fileName); fileNames.Add(fileName, fileOffset); } writer.Write(objectOffset); writer.Write(fileOffset); } } } } }