using System; using System.Collections; using System.Collections.Generic; using NW4RTools.Models; using NW4RTools.Models.Animation; namespace NW4RTools { public class BrresReader { public static ResFile LoadFile(byte[] data) { return LoadFile(data, true); } public static ResFile LoadFile(byte[] data, out SortedDictionary offsetMap) { return LoadFile(data, true, out offsetMap); } public static ResFile LoadFile(byte[] data, bool debug) { return new BrresReader(debug).Load(new InputStream(data, ByteEndian.BigEndian)); } public static ResFile LoadFile(byte[] data, bool debug, out SortedDictionary offsetMap) { return new BrresReader(debug).Load(new InputStream(data, ByteEndian.BigEndian), out offsetMap); } private class RawStreamResDict : ResDict { } private ResFile File; private ILogger Debug; private SortedDictionary OffsetMap; private BrresReader(bool debug) { if (debug) Debug = new ConsoleLogger(); else Debug = new NullLogger(); OffsetMap = new SortedDictionary(); } public ResFile Load(InputStream ins) { SortedDictionary trashThis; return Load(ins, out trashThis); } public ResFile Load(InputStream ins, out SortedDictionary offsetMap) { File = new ResFile(); // Read BRRES header byte[] magic = ins.ReadBytes(4); UInt16 endian = ins.ReadUInt16(); UInt16 version = ins.ReadUInt16(); UInt32 fileSize = ins.ReadUInt32(); UInt16 headerSize = ins.ReadUInt16(); UInt16 blockCount = ins.ReadUInt16(); File.Version = version; // Read first block header byte[] blkMagic = ins.ReadBytes(4); UInt32 blkSize = ins.ReadUInt32(); using (var c = Debug.Push("Root Dictionary")) { ReadAndParseDict(ins, ParseRootEntry, "Root Dictionary"); } // DONE! using (var c = Debug.Push("Offset Map")) { foreach (var e in OffsetMap) { Debug.Send("0x{0:X} : {1}", e.Key, e.Value); } } offsetMap = OffsetMap; return File; } private void ParseRootEntry(string name, InputStream ins) { using (var c = Debug.Push(name)) { switch (name) { case "3DModels(NW4R)": File.Add(name, ReadAndConvertDict(ins, ConvertModelResource)); break; case "Textures(NW4R)": File.Add(name, ReadAndConvertDict(ins, ConvertTextureResource)); break; case "AnmChr(NW4R)": File.Add(name, ReadAndConvertDict(ins, ConvertAnmChrResource)); break; case "AnmClr(NW4R)": File.Add(name, ReadAndConvertDict(ins, ConvertAnmClrResource)); break; case "AnmTexSrt(NW4R)": File.Add(name, ReadAndConvertDict(ins, ConvertAnmTexSRTResource)); break; case "External": File.Add(name, ReadAndConvertDict(ins, ConvertExternalResource)); break; default: Debug.Send("Not implemented"); return; } } } private byte[] ConvertExternalResource(string name, InputStream ins) { using (var c = Debug.Push(name)) { OffsetMap.Add(ins.Position, "External: " + name); return null; } } private Texture ConvertTextureResource(string name, InputStream ins) { using (var c = Debug.Push(name)) { Texture tex = new Texture(); int startPos = ins.Position; OffsetMap.Add(startPos, "Texture: " + name); byte[] magic = ins.ReadBytes(4); UInt32 size = ins.ReadUInt32(); UInt32 version = ins.ReadUInt32(); Debug.Send("Offset: 0x{0:X}; Size: 0x{1:X}; Version: {2}", startPos, size, version); Int32 resFileOffset = ins.ReadInt32(); Int32 dataOffset = ins.ReadInt32(); Int32 nameOffset = ins.ReadInt32(); // Flags stores nothing interesting, just "is CI" flag (0x1) UInt32 flags = ins.ReadUInt32(); Int16 width = ins.ReadInt16(); Int16 height = ins.ReadInt16(); TextureFormat format = (TextureFormat)ins.ReadUInt32(); int mipmapCount = ins.ReadInt32(); tex.MinLOD = ins.ReadFloat(); tex.MaxLOD = ins.ReadFloat(); ins.Seek(startPos + dataOffset); OffsetMap.Add(ins.Position, String.Format("Texture Data for: {0} [{1}, {2}x{3}, {4} mipmaps]", name, format, width, height, mipmapCount)); // handle mipmaps tex.Images = new System.Drawing.Bitmap[mipmapCount]; #if DEV_DONT_MODIFY_TEXTURE tex.ImageData = new byte[mipmapCount][]; #endif for (int i = 0; i < mipmapCount; i++) { byte[] data = ins.ReadBytes(Texture.GetDataSize(width, height, format)); #if DEV_DONT_MODIFY_TEXTURE tex.ImageData[i] = data; #endif tex.ImportData(i, data, width, height, format); width /= 2; height /= 2; } return tex; } } private Models.Animation.CharacterAnim ConvertAnmChrResource(string name, InputStream ins) { using (var c = Debug.Push(name)) { var anim = new Models.Animation.CharacterAnim(); int startPos = ins.Position; OffsetMap.Add(startPos, "Character Animation: " + name); byte[] magic = ins.ReadBytes(4); UInt32 size = ins.ReadUInt32(); UInt32 version = ins.ReadUInt32(); Debug.Send("Offset: 0x{0:X}; Size: 0x{1:X}; Version: {2}", startPos, size, version); CHR_ANIM_HACK = anim; Int32 resFileOffset = ins.ReadInt32(); Int32 dataOffset = ins.ReadInt32(); ins.Skip(4); // No idea Int32 nameOffset = ins.ReadInt32(); ins.Skip(4); // No idea anim.FrameCount = ins.ReadUInt16(); UInt16 nodeCount = ins.ReadUInt16(); anim.Loop = (ins.ReadUInt32() > 0); anim.ScaleMode = (Model.ScaleModeType)ins.ReadUInt32(); ins.Seek(startPos + dataOffset); anim.Nodes = ReadAndConvertDict(ins, ConvertAnmChrNode); CHR_ANIM_HACK = null; return anim; } } // HACK HACK HACK private CharacterAnim CHR_ANIM_HACK; private Models.Animation.CharacterAnim.Node ConvertAnmChrNode(string name, InputStream ins) { using (var c = Debug.Push(name)) { var node = new Models.Animation.CharacterAnim.Node(); int startPos = ins.Position; OffsetMap.Add(startPos, "AnmChr Node: " + name); UInt32 nameOffset = ins.ReadUInt32(); CharacterAnim.Flags flags = (CharacterAnim.Flags)ins.ReadUInt32(); Debug.Send("{0:X}", (UInt32)flags); Debug.Send("{0}", flags); node.ScaleFormat = (CharacterAnim.STFormatType)((UInt32)(flags & CharacterAnim.Flags.ScaleFormatMask) >> 25); node.RotateFormat = (CharacterAnim.RotateFormatType)((UInt32)(flags & CharacterAnim.Flags.RotateFormatMask) >> 27); node.TranslateFormat = (CharacterAnim.STFormatType)((UInt32)(flags & CharacterAnim.Flags.TranslateFormatMask) >> 30); node.Flags = flags & CharacterAnim.Flags.NonComputableFlags; node.ScaleUseModel = ((flags & CharacterAnim.Flags.ScaleUseModel) != 0); node.RotateUseModel = ((flags & CharacterAnim.Flags.RotateUseModel) != 0); node.TranslateUseModel = ((flags & CharacterAnim.Flags.TranslateUseModel) != 0); // SCALES bool needScaleYZ = true; if ((flags & CharacterAnim.Flags.ScaleXNotExist) == 0) { node.ScaleX = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.ScaleXConstant, (CharacterAnim.RotateFormatType)node.ScaleFormat); if ((flags & CharacterAnim.Flags.ScaleUniform) != 0) { node.ScaleY = node.ScaleX; node.ScaleZ = node.ScaleX; needScaleYZ = false; } } else { node.ScaleX = new KeyframeAnim(1.0f); } if (needScaleYZ) { if ((flags & CharacterAnim.Flags.ScaleYNotExist) == 0) node.ScaleY = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.ScaleYConstant, (CharacterAnim.RotateFormatType)node.ScaleFormat); else node.ScaleY = new KeyframeAnim(1.0f); if ((flags & CharacterAnim.Flags.ScaleZNotExist) == 0) node.ScaleZ = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.ScaleZConstant, (CharacterAnim.RotateFormatType)node.ScaleFormat); else node.ScaleZ = new KeyframeAnim(1.0f); } // ROTATIONS if ((flags & CharacterAnim.Flags.RotateXNotExist) == 0) { node.RotateX = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.RotateXConstant, node.RotateFormat); } else { node.RotateX = new KeyframeAnim(0.0f); } if ((flags & CharacterAnim.Flags.RotateYNotExist) == 0) { node.RotateY = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.RotateYConstant, node.RotateFormat); } else { node.RotateY = new KeyframeAnim(0.0f); } if ((flags & CharacterAnim.Flags.RotateZNotExist) == 0) { node.RotateZ = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.RotateZConstant, node.RotateFormat); } else { node.RotateZ = new KeyframeAnim(0.0f); } // TRANSLATIIONS if ((flags & CharacterAnim.Flags.TranslateXNotExist) == 0) { node.TranslateX = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.TranslateXConstant, (CharacterAnim.RotateFormatType)node.TranslateFormat); } else { node.TranslateX = new KeyframeAnim(0.0f); } if ((flags & CharacterAnim.Flags.TranslateYNotExist) == 0) { node.TranslateY = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.TranslateYConstant, (CharacterAnim.RotateFormatType)node.TranslateFormat); } else { node.TranslateY = new KeyframeAnim(0.0f); } if ((flags & CharacterAnim.Flags.TranslateZNotExist) == 0) { node.TranslateZ = ReadAnmChrElement( ins, startPos, node, flags, CharacterAnim.Flags.TranslateZConstant, (CharacterAnim.RotateFormatType)node.TranslateFormat); } else { node.TranslateZ = new KeyframeAnim(0.0f); } return node; } } private KeyframeAnim ReadAnmChrElement( InputStream ins, int startPos, CharacterAnim.Node node, CharacterAnim.Flags flags, CharacterAnim.Flags isConstF, CharacterAnim.RotateFormatType format) { var elem = new KeyframeAnim(); elem.IsConstant = (flags & isConstF) != 0; OffsetMap.Add(ins.Position, String.Format("Element [{0}]", elem.IsConstant ? "Constant" : "Keyframes")); if (elem.IsConstant) { elem.ConstValue = ins.ReadFloat(); } else if ( format == CharacterAnim.RotateFormatType.Data32 || format == CharacterAnim.RotateFormatType.Data48 || format == CharacterAnim.RotateFormatType.Data96) { // KEYFRAME DATA int savePos = ins.Position; ins.Seek(startPos + ins.ReadInt32()); int kfPos = ins.Position; UInt16 kfCount = ins.ReadUInt16(); ins.Skip(2); OffsetMap[kfPos] = String.Format("Element Keyframe Data for {0:X} [{1} frames]", savePos, kfCount); float invKeyFrameRange = ins.ReadFloat(); elem.Keyframes = new Keyframe[kfCount]; Debug.Send("{0}", format); for (int i = 0; i < kfCount; i++) { switch (format) { case CharacterAnim.RotateFormatType.Data32: UInt32 val = ins.ReadUInt32(); // Is this correct? elem.Keyframes[i].Frame = val >> 24; elem.Keyframes[i].Value = (val >> 12) & 0xFFF; elem.Keyframes[i].Slope = ((float)(val & 0xFFF)) / 32.0f; break; case CharacterAnim.RotateFormatType.Data48: // Is this correct? elem.Keyframes[i].Frame = ((float)ins.ReadInt16()) / 32.0f; elem.Keyframes[i].Value = ((float)ins.ReadUInt16()) * (360.0f / 0x10000); elem.Keyframes[i].Slope = ((float)ins.ReadInt16()) / 256.0f; break; case CharacterAnim.RotateFormatType.Data96: elem.Keyframes[i].Frame = ins.ReadFloat(); elem.Keyframes[i].Value = ins.ReadFloat(); elem.Keyframes[i].Slope = ins.ReadFloat(); break; } } ins.Seek(savePos + 4); } else if ( format == CharacterAnim.RotateFormatType.DataUnk4 || format == CharacterAnim.RotateFormatType.DataUnk5) { // SOMETHING ELSE int savePos = ins.Position; ins.Seek(startPos + ins.ReadInt32()); int kfPos = ins.Position; OffsetMap[kfPos] = String.Format("Element Multiplied Frame Data for {0:X}", savePos); elem.Multiplier = ins.ReadFloat(); elem.BaseValue = ins.ReadFloat(); elem.Values = new int[CHR_ANIM_HACK.FrameCount]; for (int i = 0; i < CHR_ANIM_HACK.FrameCount; i++) { switch (format) { case CharacterAnim.RotateFormatType.DataUnk4: elem.Values[i] = ins.ReadByte(); break; case CharacterAnim.RotateFormatType.DataUnk5: elem.Values[i] = ins.ReadUInt16(); break; } } ins.Seek(savePos + 4); } else if ( format == CharacterAnim.RotateFormatType.DataUnk6) { // WHAT THE FUCK int savePos = ins.Position; ins.Seek(startPos + ins.ReadInt32()); int kfPos = ins.Position; OffsetMap[kfPos] = String.Format("Element Type 6 Frame Data for {0:X}", savePos); elem.BaseValue = ins.ReadFloat(); elem.FloatValues = new float[CHR_ANIM_HACK.FrameCount]; for (int i = 0; i < CHR_ANIM_HACK.FrameCount; i++) { elem.FloatValues[i] = ins.ReadFloat(); } ins.Seek(savePos + 4); } else { Debug.Send("Unimplemented format {0} at {1:X}", format, ins.Position); } return elem; } private Models.Animation.ColorAnim ConvertAnmClrResource(string name, InputStream ins) { using (var c = Debug.Push(name)) { var anim = new Models.Animation.ColorAnim(); int startPos = ins.Position; OffsetMap.Add(startPos, "Color Animation: " + name); byte[] magic = ins.ReadBytes(4); UInt32 size = ins.ReadUInt32(); UInt32 version = ins.ReadUInt32(); Debug.Send("Offset: 0x{0:X}; Size: 0x{1:X}; Version: {2}", startPos, size, version); Int32 resFileOffset = ins.ReadInt32(); Int32 dataOffset = ins.ReadInt32(); ins.Skip(4); // No idea Int32 nameOffset = ins.ReadInt32(); ins.Skip(4); // No idea anim.FrameCount = ins.ReadUInt16(); UInt16 nodeCount = ins.ReadUInt16(); anim.Loop = (ins.ReadUInt32() > 0); COLOR_ANIM_HACK = anim; ins.Seek(startPos + dataOffset); anim.Nodes = ReadAndConvertDict(ins, ConvertAnmClrNode); COLOR_ANIM_HACK = null; return anim; } } // HACK HACK HACK private ColorAnim COLOR_ANIM_HACK; private ColorAnim.Node ConvertAnmClrNode(string name, InputStream ins) { using (var c = Debug.Push(name)) { var node = new ColorAnim.Node(); int startPos = ins.Position; OffsetMap[startPos] = "AnmClr Node: " + name; UInt32 nameOffset = ins.ReadUInt32(); UInt32 flags = ins.ReadUInt32(); node.Elements = new ColorAnim.Element[(int)ColorAnim.TargetType.Count]; for (int i = 0; i < node.Elements.Length; i++) { node.Elements[i] = ReadAnmClrElement(ins, flags); flags >>= 2; } return node; } } private ColorAnim.Element ReadAnmClrElement(InputStream ins, UInt32 flags) { var elem = new ColorAnim.Element(); elem.Exists = (flags & 1) != 0; elem.IsConstant = (flags & 2) != 0; OffsetMap[ins.Position] = String.Format("Element [{0}]", elem.IsConstant ? "Constant" : "Colours"); if (!elem.Exists) return elem; elem.Mask = ins.ReadUInt32(); if (elem.IsConstant) { elem.ConstValue = ins.ReadColor(); } else { int savePos = ins.Position; ins.Seek(savePos + ins.ReadInt32()); elem.Colors = new Color[COLOR_ANIM_HACK.FrameCount + 1]; for (int i = 0; i < elem.Colors.Length; i++) { elem.Colors[i] = ins.ReadColor(); } ins.Seek(savePos + 4); } return elem; } private Models.Animation.TextureSRTAnim ConvertAnmTexSRTResource(string name, InputStream ins) { using (var c = Debug.Push(name)) { var anim = new Models.Animation.TextureSRTAnim(); int startPos = ins.Position; OffsetMap.Add(startPos, "Texture SRT Animation: " + name); byte[] magic = ins.ReadBytes(4); UInt32 size = ins.ReadUInt32(); UInt32 version = ins.ReadUInt32(); Debug.Send("Offset: 0x{0:X}; Size: 0x{1:X}; Version: {2}", startPos, size, version); Int32 resFileOffset = ins.ReadInt32(); Int32 dataOffset = ins.ReadInt32(); ins.Skip(4); // No idea Int32 nameOffset = ins.ReadInt32(); ins.Skip(4); // No idea anim.FrameCount = ins.ReadUInt16(); UInt16 nodeCount = ins.ReadUInt16(); anim.MatrixMode = (Model.TexMatrixModeType)ins.ReadUInt32(); anim.Loop = (ins.ReadUInt32() > 0); ins.Seek(startPos + dataOffset); anim.Nodes = ReadAndConvertDict(ins, ConvertAnmTexSRTNode); return anim; } } private TextureSRTAnim.Node ConvertAnmTexSRTNode(string name, InputStream ins) { using (var c = Debug.Push(name)) { var node = new TextureSRTAnim.Node(); int startPos = ins.Position; OffsetMap.Add(startPos, "AnmTexSRT Node: " + name); UInt32 nameOffset = ins.ReadUInt32(); UInt32 flags = ins.ReadUInt32(); UInt32 indFlags = ins.ReadUInt32(); Debug.Send("Flags: {0:X}, {1:X}", flags, indFlags); node.Textures = new TextureSRTAnim.Element[8]; node.IndirectTextures = new TextureSRTAnim.Element[3]; for (int i = 0; i < 8; i++) { Debug.Send("Texture {0}", i); if ((flags & 1) == 0) node.Textures[i].Exists = false; else node.Textures[i] = ReadAnmTexSRTElement(ins, startPos); flags >>= 1; } for (int i = 0; i < 3; i++) { Debug.Send("Indirect Texture {0}", i); if ((indFlags & 1) == 0) node.IndirectTextures[i].Exists = false; else node.IndirectTextures[i] = ReadAnmTexSRTElement(ins, startPos); indFlags >>= 1; } return node; } } private TextureSRTAnim.Element ReadAnmTexSRTElement(InputStream ins, int startPos) { var elem = new TextureSRTAnim.Element(); int savePos = ins.Position; ins.Seek(startPos + ins.ReadInt32()); elem.Exists = true; OffsetMap.Add(ins.Position, String.Format("Element")); Debug.Send("Offset: {0:X}", ins.Position); UInt32 flags = ins.ReadUInt32(); Debug.Send("Element Flags: {0:X}", flags); if ((flags & 2) != 0) { Debug.Send("Scale 1"); elem.ScaleS = new KeyframeAnim(1.0f); elem.ScaleT = new KeyframeAnim(1.0f); } else { Debug.Send("Reading Scale S @ {0:X}", ins.Position); elem.ScaleS = ReadKeyframeAnim(ins, (flags & 0x20) != 0); if ((flags & 0x10) != 0) { Debug.Send("Uniform Scale"); elem.ScaleT = elem.ScaleS; } else { Debug.Send("Reading Scale T @ {0:X}", ins.Position); elem.ScaleT = ReadKeyframeAnim(ins, (flags & 0x40) != 0); } } if ((flags & 4) != 0) { Debug.Send("Rotate 0"); elem.Rotate = new KeyframeAnim(0.0f); } else { Debug.Send("Reading Rotate @ {0:X}", ins.Position); elem.Rotate = ReadKeyframeAnim(ins, (flags & 0x80) != 0); } if ((flags & 8) != 0) { Debug.Send("Trans 0"); elem.TransS = new KeyframeAnim(0.0f); elem.TransT = new KeyframeAnim(0.0f); } else { Debug.Send("Reading Trans S @ {0:X}", ins.Position); elem.TransS = ReadKeyframeAnim(ins, (flags & 0x100) != 0); Debug.Send("Reading Trans T @ {0:X}", ins.Position); elem.TransT = ReadKeyframeAnim(ins, (flags & 0x200) != 0); } ins.Seek(savePos + 4); return elem; } private KeyframeAnim ReadKeyframeAnim(InputStream ins, bool isConst) { var output = new KeyframeAnim(); output.IsConstant = isConst; if (isConst) { output.ConstValue = ins.ReadFloat(); } else { int basePos = ins.Position; int offset = ins.ReadInt32(); ins.Seek(basePos + offset); UInt16 kfCount = ins.ReadUInt16(); ins.Skip(2); float invKeyFrameRange = ins.ReadFloat(); output.Keyframes = new Keyframe[kfCount]; for (int i = 0; i < kfCount; i++) { output.Keyframes[i].Frame = ins.ReadFloat(); output.Keyframes[i].Value = ins.ReadFloat(); output.Keyframes[i].Slope = ins.ReadFloat(); } ins.Seek(basePos + 4); } return output; } private Model ConvertModelResource(string name, InputStream ins) { using (var c = Debug.Push(name)) { Model mdl = new Model(); int startPos = ins.Position; OffsetMap.Add(startPos, "Model: " + name); byte[] magic = ins.ReadBytes(4); UInt32 size = ins.ReadUInt32(); UInt32 version = ins.ReadUInt32(); Debug.Send("Offset: 0x{0:X}; Size: 0x{1:X}; Version: {2}", startPos, size, version); Int32 resFileOffset = ins.ReadInt32(); Int32 bytecodeOffset = ins.ReadInt32(); Int32 nodeOffset = ins.ReadInt32(); Int32 vtxPosOffset = ins.ReadInt32(); Int32 vtxNrmOffset = ins.ReadInt32(); Int32 vtxClrOffset = ins.ReadInt32(); Int32 texCoordOffset = ins.ReadInt32(); Int32 vtxFurVecOffset = ins.ReadInt32(); Int32 vtxFurPosOffset = ins.ReadInt32(); Int32 materialOffset = ins.ReadInt32(); Int32 shaderOffset = ins.ReadInt32(); Int32 shapeOffset = ins.ReadInt32(); Int32 textureOffset = ins.ReadInt32(); Int32 paletteOffset = ins.ReadInt32(); Int32 unkOffset = ins.ReadInt32(); if (unkOffset != 0) { Debug.Send("WARNING: Model {0} has unhandled unknown data", name); OffsetMap.Add(startPos + unkOffset, "Model Unknown Data: " + name); } Int32 nameOffset = ins.ReadInt32(); int infoStructPos = ins.Position; OffsetMap.Add(infoStructPos, "Model Info Struct for: " + name); UInt32 infoSize = ins.ReadUInt32(); // should be 0x40 Int32 mdlOffset = ins.ReadInt32(); mdl.ScaleMode = (Model.ScaleModeType)ins.ReadUInt32(); mdl.TexMatrixMode = (Model.TexMatrixModeType)ins.ReadUInt32(); mdl.VertexCount = ins.ReadUInt32(); mdl.TriangleCount = ins.ReadUInt32(); UInt32 unk = ins.ReadUInt32(); UInt32 matrixCount = ins.ReadUInt32(); mdl.UsesNrmMtxArray = (bool)(ins.ReadByte() != 0); mdl.UsesTexMtxArray = (bool)(ins.ReadByte() != 0); ins.Skip(2); Int32 mtxDataOffset = ins.ReadInt32(); mdl.Minimum = ins.ReadVec3(); mdl.Maximum = ins.ReadVec3(); ins.Seek(infoStructPos + mtxDataOffset); OffsetMap.Add(ins.Position, "Matrix ID to Node ID Data for: " + name); UInt32 mtxDataCount = ins.ReadUInt32(); mdl.MatrixIDtoNodeID = new int[mtxDataCount]; for (int i = 0; i < mtxDataCount; i++) { mdl.MatrixIDtoNodeID[i] = ins.ReadInt32(); } // Load bytecode using (var c2 = Debug.Push("Bytecode")) mdl.Bytecode = ReadAndConvertDict(ins.At(startPos + bytecodeOffset), ConvertModelBytecode); // Load nodes and fix up pointers StartNodeLoading(); using (var c2 = Debug.Push("Nodes")) mdl.Nodes = ReadAndConvertDict(ins.At(startPos + nodeOffset), ConvertModelNode); FinishNodeLoading(); // Load vertex data VtxPosIndexLookup = new Dictionary(); VtxNrmIndexLookup = new Dictionary(); VtxClrIndexLookup = new Dictionary(); VtxTexCoordIndexLookup = new Dictionary(); using (var c2 = Debug.Push("Vertex Position Data")) { if (vtxPosOffset == 0) { Debug.Send("None (what?)"); mdl.VtxPosData = new ResDict(); } else { mdl.VtxPosData = ReadAndConvertDict(ins.At(startPos + vtxPosOffset), ConvertVtxPosData); } } using (var c2 = Debug.Push("Vertex Normal Data")) { if (vtxNrmOffset == 0) { Debug.Send("None"); } else { mdl.VtxNrmData = ReadAndConvertDict(ins.At(startPos + vtxNrmOffset), ConvertVtxNrmData); } } using (var c2 = Debug.Push("Vertex Colour Data")) { if (vtxClrOffset == 0) { Debug.Send("None"); } else { mdl.VtxClrData = ReadAndConvertDict(ins.At(startPos + vtxClrOffset), ConvertVtxClrData); } } using (var c2 = Debug.Push("Vertex TexCoord Data")) { if (texCoordOffset == 0) { Debug.Send("None"); } else { mdl.VtxTexCoordData = ReadAndConvertDict(ins.At(startPos + texCoordOffset), ConvertVtxTexCoordData); } } // Load materials and associated structs // Material classes refer to Shaders using an offset, so this is used to fix those up // Also, Pairings refer to Texture Info ShaderOffsetRefs = new Dictionary(); MaterialOffsetRefs = new Dictionary(); TextureInfoRefs = new Dictionary(); using (var c2 = Debug.Push("Shaders")) mdl.Shaders = ReadAndConvertDict(ins.At(startPos + shaderOffset), ConvertModelShader); using (var c2 = Debug.Push("Materials")) mdl.Materials = ReadAndConvertDict(ins.At(startPos + materialOffset), ConvertModelMaterial); using (var c2 = Debug.Push("Shapes")) mdl.Shapes = ReadAndConvertDict(ins.At(startPos + shapeOffset), ConvertModelShape); if (textureOffset != 0) { using (var c2 = Debug.Push("Texture Lookup for Texture Info/Material Pairing")) mdl.PairingLookupByTexture = ReadAndConvertDict>(ins.At(startPos + textureOffset), ConvertPairingList); } else { mdl.PairingLookupByTexture = new ResDict>(); Debug.Send("Texture Lookup for Texture Info/Material Pairing: none"); } if (paletteOffset != 0) { using (var c2 = Debug.Push("Palette Lookup for Texture Info/Material Pairing")) mdl.PairingLookupByPalette = ReadAndConvertDict>(ins.At(startPos + paletteOffset), ConvertPairingList); } else { mdl.PairingLookupByPalette = new ResDict>(); Debug.Send("Palette Lookup for Texture Info/Material Pairing: none"); } return mdl; } } private Models.ByteCode ConvertModelBytecode(string name, InputStream ins) { var bc = new Models.ByteCode(); OffsetMap.Add(ins.Position, "ByteCode: " + name); while (true) { ByteCode.OpType op = (ByteCode.OpType)ins.ReadByte(); switch (op) { case ByteCode.OpType.Done: bc.Instructions.Add(new ByteCode.DoneInstruction()); return bc; case ByteCode.OpType.AssignNodeToParentMtx: var insn2 = new ByteCode.AssignNodeToParentMtxInstruction(); insn2.NodeID = ins.ReadUInt16(); insn2.ParentMatrixID = ins.ReadUInt16(); bc.Instructions.Add(insn2); break; case ByteCode.OpType.BlendMatrices: var insn3 = new ByteCode.BlendMatricesInstruction(); insn3.MatrixID = ins.ReadUInt16(); insn3.BlendedMatrices = new ByteCode.BlendMatricesInstruction.BlendedMatrix[ins.ReadByte()]; for (int i = 0; i < insn3.BlendedMatrices.Length; i++) { insn3.BlendedMatrices[i].MatrixID = ins.ReadUInt16(); insn3.BlendedMatrices[i].Ratio = ins.ReadFloat(); } bc.Instructions.Add(insn3); break; case ByteCode.OpType.DrawShape: var insn4 = new ByteCode.DrawShapeInstruction(); insn4.MaterialID = ins.ReadUInt16(); insn4.ShapeID = ins.ReadUInt16(); insn4.NodeID = ins.ReadUInt16(); insn4.Unk = ins.ReadByte(); bc.Instructions.Add(insn4); break; case ByteCode.OpType.AssignMtxToNode: var insn5 = new ByteCode.AssignMtxToNodeInstruction(); insn5.MatrixID = ins.ReadUInt16(); insn5.NodeID = ins.ReadUInt16(); bc.Instructions.Add(insn5); break; default: bc.Instructions.Add(new ByteCode.Instruction()); break; } } } private class NodeLoadInfo { public Models.Node Node; public string Name; public int Position; public Int32 ParentOffset; public Int32 ChildOffset; public Int32 NextOffset; public Int32 PrevOffset; } private Dictionary NodeLoadData; private void StartNodeLoading() { NodeLoadData = new Dictionary(); } private Models.Node ConvertModelNode(string name, InputStream ins) { var n = new Models.Node(); OffsetMap.Add(ins.Position, "Node: " + name); int startPos = ins.Position; UInt32 size = ins.ReadUInt32(); Int32 mdlOffset = ins.ReadInt32(); Int32 nameOffset = ins.ReadInt32(); n.Index = ins.ReadUInt32(); n.MatrixID = ins.ReadUInt32(); n.Flags = ins.ReadUInt32(); n.BillboardMode = (Node.BillboardType)ins.ReadUInt32(); UInt32 unk = ins.ReadUInt32(); // might be swapped with bbmode, check asm to confirm n.Scale = ins.ReadVec3(); n.Rotation = ins.ReadVec3(); n.Translation = ins.ReadVec3(); n.BoxMin = ins.ReadVec3(); n.BoxMax = ins.ReadVec3(); NodeLoadInfo loadInfo = new NodeLoadInfo(); loadInfo.ParentOffset = ins.ReadInt32(); loadInfo.ChildOffset = ins.ReadInt32(); loadInfo.NextOffset = ins.ReadInt32(); loadInfo.PrevOffset = ins.ReadInt32(); loadInfo.Position = startPos; loadInfo.Node = n; loadInfo.Name = ins.At(startPos + nameOffset - 4).ReadName(); NodeLoadData[startPos] = loadInfo; Int32 extraDataOffset = ins.ReadInt32(); if (extraDataOffset != 0) { Debug.Send("WARNING: Node {0} has unhandled extra data!", name); OffsetMap.Add(startPos + extraDataOffset, "Node Extra Data: " + name); } n.NodeMatrix = ins.ReadMatrix(); n.NodeInvMatrix = ins.ReadMatrix(); return n; } private void FinishNodeLoading() { foreach (var dictEntry in NodeLoadData) { var entry = dictEntry.Value; var node = entry.Node; if (entry.ParentOffset == 0) { node.Parent = null; } else { NodeLoadInfo n = NodeLoadData[entry.Position + entry.ParentOffset]; node.Parent = n.Node; Debug.Send("Set {0}'s parent to {1}", entry.Name, n.Name); } if (entry.ChildOffset == 0) node.FirstChild = null; else node.FirstChild = NodeLoadData[entry.Position + entry.ChildOffset].Node; if (entry.NextOffset == 0) node.Next = null; else node.Next = NodeLoadData[entry.Position + entry.NextOffset].Node; if (entry.PrevOffset == 0) node.Previous = null; else node.Previous = NodeLoadData[entry.Position + entry.PrevOffset].Node; } NodeLoadData = null; } private void LoadVertexDataBase(InputStream ins, Models.VertexDataBase n) { int startPos = ins.Position; UInt32 size = ins.ReadUInt32(); Int32 mdlOffset = ins.ReadInt32(); Int32 dataOffset = ins.ReadInt32(); Int32 nameOffset = ins.ReadInt32(); // Note: we're relying on this value to be correct, for Shape mappings n.Index = ins.ReadUInt32(); n.ComponentCount = (VertexSettings.CompCount)ins.ReadUInt32(); n.ComponentType = (VertexSettings.CompType)ins.ReadUInt32(); // ridiculous and hacky. thanks for requiring this, Nintendo if (n is VertexClrData) { n.EntrySize = ins.ReadByte(); ins.Skip(1); } else { n.Fraction = ins.ReadByte(); n.EntrySize = ins.ReadByte(); } n.EntryCount = ins.ReadUInt16(); n.RawData = ins.At(startPos + dataOffset).ReadBytes(n.EntrySize * n.EntryCount); n.Parse(); OffsetMap.Add(startPos + dataOffset, n.GetType().ToString() + " Data"); } private Dictionary VtxPosIndexLookup; private Dictionary VtxNrmIndexLookup; private Dictionary VtxClrIndexLookup; private Dictionary VtxTexCoordIndexLookup; private Models.VertexPosData ConvertVtxPosData(string name, InputStream ins) { var n = new Models.VertexPosData(); OffsetMap.Add(ins.Position, "VertexPosData: " + name); LoadVertexDataBase(ins, n); n.Minimum = ins.ReadVec3(); n.Maximum = ins.ReadVec3(); VtxPosIndexLookup[(int)n.Index] = n; return n; } private Models.VertexNrmData ConvertVtxNrmData(string name, InputStream ins) { var n = new Models.VertexNrmData(); OffsetMap.Add(ins.Position, "VertexNrmData: " + name); LoadVertexDataBase(ins, n); VtxNrmIndexLookup[(int)n.Index] = n; return n; } private Models.VertexClrData ConvertVtxClrData(string name, InputStream ins) { var n = new Models.VertexClrData(); OffsetMap.Add(ins.Position, "VertexClrData: " + name); LoadVertexDataBase(ins, n); VtxClrIndexLookup[(int)n.Index] = n; return n; } private Models.VertexTexCoordData ConvertVtxTexCoordData(string name, InputStream ins) { var n = new Models.VertexTexCoordData(); OffsetMap.Add(ins.Position, "VertexTexCoordData: " + name); LoadVertexDataBase(ins, n); n.Minimum = ins.ReadVec2(); n.Maximum = ins.ReadVec2(); VtxTexCoordIndexLookup[(int)n.Index] = n; return n; } private Dictionary MaterialOffsetRefs; private Dictionary TextureInfoRefs; private Models.Material ConvertModelMaterial(string name, InputStream ins) { Debug.Send("Reading {0}...", name); var m = new Models.Material(); int startPos = ins.Position; OffsetMap.Add(ins.Position, "Material: " + name); UInt32 size = ins.ReadUInt32(); Int32 mdlOffset = ins.ReadInt32(); Int32 nameOffset = ins.ReadInt32(); m.Index = ins.ReadUInt32(); m.Flags = ins.ReadUInt32(); // ResGenMode m.TexCoordGenCount = ins.ReadByte(); m.ChanCount = ins.ReadByte(); m.TevStageCount = ins.ReadByte(); m.IndStageCount = ins.ReadByte(); m.CullMode = ins.ReadUInt32(); // ResMatMisc m.ZCompLoc = ins.ReadByte(); m.LightSetID = ins.ReadByte(); m.FogID = ins.ReadByte(); m.IndirectTexMtxCalcMethod1 = ins.ReadBytes(4); m.IndirectTexMtxCalcMethod2 = ins.ReadBytes(4); ins.Skip(1); // Other stuff Int32 shaderOffset = ins.ReadInt32(); m.ShaderRef = ShaderOffsetRefs[startPos + shaderOffset]; //Debug.Send("Shader offset for {0}: {1:X} [dest {2:X}]", name, shaderOffset, startPos + shaderOffset); UInt32 texInfoCount = ins.ReadUInt32(); Int32 texInfoOffset = ins.ReadInt32(); Int32 furOffset = ins.ReadInt32(); Debug.Send("Fur offset for {0}: {1:X} [dest {2:X}]", name, furOffset, startPos + furOffset); Int32 unkOffset = ins.ReadInt32(); Debug.Send("Unknown offset for {0}: {1:X} [dest {2:X}]", name, unkOffset, startPos + unkOffset); Int32 dlOffset = ins.ReadInt32(); //Debug.Send("DL offset for {0}: {1:X} [dest {2:X}]", name, dlOffset, startPos + dlOffset); // ResTexObj int ResTexObjPos = ins.Position; //OffsetMap.Add(ins.Position, "Material ResTexObj: " + name); UInt32 textureFlag = ins.ReadUInt32(); m.TexObj = new byte[8][]; for (int i = 0; i < 8; i++) { if ((textureFlag & (1 << i)) != 0) { m.TexObj[i] = ins.ReadBytes(0x20); } } ins.Seek(ResTexObjPos + 0x104); // ResTlutObj int ResTlutObjPos = ins.Position; //OffsetMap.Add(ins.Position, "Material ResTlutObj: " + name); m.TlutFlag = ins.ReadUInt32(); if ((m.TlutFlag & ~3) == 0) { m.TlutObj = ins.ReadBytes(0x20); } else if ((m.TlutFlag & ~0x1F) == 0) { m.TlutObj = ins.ReadBytes(0x40); } else { m.TlutObj = ins.ReadBytes(0x60); } ins.Seek(ResTlutObjPos + 0x64); // ResTexSrt int ResTexSrtPos = ins.Position; //OffsetMap.Add(ins.Position, "Material ResTexSrt: " + name); UInt32 srtFlag = ins.ReadUInt32(); m.TexMatrixType = ins.ReadUInt32(); m.SRTSettings = new SRTSettingInfo[8]; InputStream texSrtStream = ins.At(ins.Position); InputStream texSetStream = ins.At(ins.Position + (0x14 * 8)); for (int i = 0; i < 8; i++) { if ((srtFlag & (0xF << (i * 4))) != 0) { var srtInfo = m.SRTSettings[i] = new SRTSettingInfo(); srtInfo.ScaleX = texSrtStream.ReadFloat(); srtInfo.ScaleY = texSrtStream.ReadFloat(); srtInfo.Rotate = texSrtStream.ReadFloat(); srtInfo.TranslateX = texSrtStream.ReadFloat(); srtInfo.TranslateY = texSrtStream.ReadFloat(); srtInfo.CameraID = texSetStream.ReadByte(); srtInfo.LightID = texSetStream.ReadByte(); srtInfo.MapType = texSetStream.ReadByte(); srtInfo.Flags = texSetStream.ReadByte(); srtInfo.TexMatrix = texSetStream.ReadMatrix(); } } ins.Seek(ResTexSrtPos + 8 + (0x14 * 8) + (0x34 * 8)); // ResMatChan //OffsetMap.Add(ins.Position, "Material ResMatChan: " + name); m.ChanCtrls = new ChanCtrl[2]; for (int i = 0; i < 2; i++) { var chanInfo = m.ChanCtrls[i] = new ChanCtrl(); chanInfo.Flags = ins.ReadUInt32(); chanInfo.MatColor = ins.ReadColor(); chanInfo.AmbColor = ins.ReadColor(); chanInfo.FlagC = ins.ReadUInt32(); chanInfo.FlagA = ins.ReadUInt32(); } // Texture Info if (texInfoOffset != 0) OffsetMap.Add(startPos + texInfoOffset, "Material Texture Info: " + name); ins.Seek(startPos + texInfoOffset); m.TextureInfos = new List((int)texInfoCount); for (int i = 0; i < texInfoCount; i++) { //Debug.Send("Reading {0} texture info at {1:X}", i, ins.Position); int texInfoPos = ins.Position; var texInfo = ReadTextureInfo(ins); m.TextureInfos.Add(texInfo); TextureInfoRefs[texInfoPos] = texInfo; } // Display Lists OffsetMap.Add(startPos + dlOffset, "Material Display Lists: " + name); ins.Seek(startPos + dlOffset); m.PixDL = ins.ReadBytes(0x20); m.TevColorDL = ins.ReadBytes(0x80); m.IndMtxAndScaleDL = ins.ReadBytes(0x40); m.TexCoordGenDL = ins.ReadBytes(0xA0); MaterialOffsetRefs[startPos] = m; return m; } private Dictionary ShaderOffsetRefs; private Models.Shader ConvertModelShader(string name, InputStream ins) { var s = new Models.Shader(); //Debug.Send("{0} @ {1:X}", name, ins.Position); int startPos = ins.Position; // Note: duplicate shader entries seem to be common, so OffsetMap.Add can't be used // Looks like Nintendo's converter checks for them when writing and optimises them // into one entry. That'll sure be fun to implement here... probably requiring // messing around with IEqualityComparer or whatever it's called. I don't even know // if I'm making sense or not. Too tired to look it up, it's 4:51am... OffsetMap[ins.Position] = "Shader: " + name; UInt32 size = ins.ReadUInt32(); Int32 mdlOffset = ins.ReadInt32(); s.Index = ins.ReadUInt32(); s.TevStageCount = ins.ReadByte(); ins.Skip(3); s.Unk1 = ins.ReadUInt32(); s.Unk2 = ins.ReadUInt32(); ins.Skip(8); s.DisplayList = ins.ReadBytes(0x1E0); ShaderOffsetRefs[startPos] = s; return s; } private Models.Shape ConvertModelShape(string name, InputStream ins) { var s = new Models.Shape(); int startPos = ins.Position; OffsetMap.Add(ins.Position, "Shape: " + name); // 0x00 UInt32 size = ins.ReadUInt32(); // 0x04 Int32 mdlOffset = ins.ReadInt32(); // 0x08 s.MatrixID = ins.ReadInt32(); // 0x0C s.Unk = ins.ReadBytes(12); int dlBase1 = ins.Position; // 0x18 s.DLBufferSize1 = ins.ReadUInt32(); // 0x1C UInt32 dataSize1 = ins.ReadUInt32(); // 0x20 Int32 offset1 = ins.ReadInt32(); s.DisplayList1 = ins.At(dlBase1 + offset1).ReadBytes((int)dataSize1); OffsetMap.Add(dlBase1 + offset1, String.Format("Shape DL 1: {0} [Buffer Size: 0x{1:X}; Data Size: 0x{2:X}]", name, s.DLBufferSize1, dataSize1)); int dlBase2 = ins.Position; // 0x24 s.DLBufferSize2 = ins.ReadUInt32(); // 0x28 UInt32 dataSize2 = ins.ReadUInt32(); // 0x2C Int32 offset2 = ins.ReadInt32(); s.DisplayList2 = ins.At(dlBase2 + offset2).ReadBytes((int)dataSize2); OffsetMap.Add(dlBase2 + offset2, String.Format("Shape DL 2: {0} [Buffer Size: 0x{1:X}; Data Size: 0x{2:X}]", name, s.DLBufferSize2, dataSize2)); // 0x30 s.DataFlags = ins.ReadUInt32(); // 0x34 s.Flags = ins.ReadUInt32(); // 0x38 int nameOffset = ins.ReadInt32(); // 0x3C s.Index = ins.ReadUInt32(); // 0x40 s.VertexCount = ins.ReadUInt32(); // 0x44 s.PolygonCount = ins.ReadUInt32(); // 0x48 s.PosData = VtxPosIndexLookup[ins.ReadInt16()]; // 0x4A short nrmIndex = ins.ReadInt16(); s.NrmData = nrmIndex == -1 ? null : VtxNrmIndexLookup[nrmIndex]; // 0x4C s.ClrData = new VertexClrData[2]; for (int i = 0; i < 2; i++) { short clrIndex = ins.ReadInt16(); s.ClrData[i] = clrIndex == -1 ? null : VtxClrIndexLookup[clrIndex]; } // 0x50 s.TexCoordData = new VertexTexCoordData[8]; for (int i = 0; i < 8; i++) { short tcIndex = ins.ReadInt16(); s.TexCoordData[i] = tcIndex == -1 ? null : VtxTexCoordIndexLookup[tcIndex]; } // 0x60 s.FurVecDataIndex = ins.ReadInt16(); // 0x62 s.FurPosDataIndex = ins.ReadInt16(); // 0x64 Int32 extraDataOffset = ins.ReadInt32(); var ed = ins.At(startPos + extraDataOffset); UInt32 edCount = ed.ReadUInt32(); s.ExtraData = new ushort[edCount]; for (int i = 0; i < edCount; i++) { s.ExtraData[i] = ed.ReadUInt16(); } OffsetMap.Add(startPos + extraDataOffset, String.Format("Shape Extra Data: {0} [{1} entries]", name, edCount)); return s; } public List ConvertPairingList(string name, InputStream ins) { var list = new List(); int startPos = ins.Position; OffsetMap.Add(ins.Position, "Texture/Material Pairing List for: " + name); UInt32 count = ins.ReadUInt32(); for (int i = 0; i < count; i++) { var p = new TexMatPairing(); p.Material = MaterialOffsetRefs[startPos + ins.ReadInt32()]; p.Texture = TextureInfoRefs[startPos + ins.ReadInt32()]; list.Add(p); } return list; } public TextureInfo ReadTextureInfo(InputStream ins) { int bPos = ins.Position; var bTex = new Models.TextureInfo(); Debug.Send("Reading a texture info from {0:X}", bPos); Int32 texOffs = ins.ReadInt32(); bTex.TextureName = texOffs == 0 ? null : ins.At(bPos + texOffs - 4).ReadName(); Int32 palOffs = ins.ReadInt32(); bTex.PaletteName = palOffs == 0 ? null : ins.At(bPos + palOffs - 4).ReadName(); Debug.Send("Was {0}", bTex.TextureName); // placeholder pointers, don't need these ins.Skip(8); bTex.TexMapID = ins.ReadUInt32(); bTex.TlutID = ins.ReadUInt32(); bTex.WrapS = (TextureWrapType)ins.ReadUInt32(); bTex.WrapT = (TextureWrapType)ins.ReadUInt32(); bTex.MinFilt = ins.ReadUInt32(); bTex.MagFilt = ins.ReadUInt32(); bTex.LODBias = ins.ReadFloat(); bTex.MaxAniso = ins.ReadUInt32(); bTex.BiasClamp = (ins.ReadByte() != 0); bTex.DoEdgeLOD = (ins.ReadByte() != 0); ins.Skip(2); return bTex; } private RawStreamResDict ReadDict(InputStream ins) { RawStreamResDict output = new RawStreamResDict(); int dictPos = ins.Position; UInt32 dataSize = ins.ReadUInt32(); UInt32 entryCount = ins.ReadUInt32(); for (int i = 0; i <= entryCount; i++) { UInt16 eRef = ins.ReadUInt16(); UInt16 flag = ins.ReadUInt16(); UInt16 iLeft = ins.ReadUInt16(); UInt16 iRight = ins.ReadUInt16(); Int32 nameOffset = ins.ReadInt32(); Int32 dataOffset = ins.ReadInt32(); //Debug.Send("Position: {6:X}, eRef: {0:X}, flag: {1:X}, iLeft: {2:X}, iRight: {3:X}, nameOffset: {4:X}, dataOffset: {5:X}", eRef, flag, iLeft, iRight, nameOffset, dataOffset, ins.Position); if (nameOffset != 0 && dataOffset != 0) { string name = ins.At(dictPos + nameOffset - 4).ReadName(); Debug.Send("Entry: {0} at offset 0x{1:X}", name, dictPos + dataOffset); InputStream entryStream = ins.At(dictPos + dataOffset); output.Add(name, entryStream); } } return output; } private delegate void ParseResourceDelegate(string name, InputStream ins); private delegate TValue ConvertResourceDelegate(string name, InputStream ins); private void ReadAndParseDict(InputStream ins, ParseResourceDelegate func) { ReadAndParseDict(ins, func, "Unnamed"); } private void ReadAndParseDict(InputStream ins, ParseResourceDelegate func, string dictName) { int dictPos = ins.Position; RawStreamResDict theDict = ReadDict(ins); OffsetMap.Add(dictPos, string.Format("ResDict: {0} [Data ends at 0x{1:X}]", dictName, ins.Position)); foreach (var entry in theDict) { func(entry.Key, entry.Value); } } private ResDict ReadAndConvertDict(InputStream ins, ConvertResourceDelegate func) { int dictPos = ins.Position; RawStreamResDict theDict = ReadDict(ins); var outDict = new ResDict(); // Hack to get the type name string valueTypeName = outDict.GetType().GetGenericArguments()[0].ToString(); OffsetMap.Add(dictPos, string.Format("ResDict: {0} [Data ends at 0x{1:X}]", valueTypeName, ins.Position)); foreach (var entry in theDict) { TValue returnedObject = func(entry.Key, entry.Value); outDict.Add(entry.Key, returnedObject); } return outDict; } } }