using System; using System.Collections; using System.Collections.Generic; using NW4RTools.Models; namespace NW4RTools { public class BrresWriter { public static byte[] WriteFile(ResFile file) { return new BrresWriter().Save(file); } private ResFile File; private ILogger Debug; private SortedDictionary OffsetMap; private OutputStream Output; private BrresWriter() { Debug = new ConsoleLogger(); OffsetMap = new SortedDictionary(); } private byte[] Save(ResFile file) { Output = new OutputStream(ByteEndian.BigEndian); File = file; using (var c = Debug.Push("Offset Calculation")) CalculateRoot(); using (var c = Debug.Push("Data")) WriteRoot(); using (var c = Debug.Push("Offset Map")) { foreach (var e in OffsetMap) { // Commented for easier output reading atm Debug.Send("0x{0:X} : {1}", e.Key, e.Value); } } return Output.GetBuffer(); } private void LogPosition(string text) { Debug.Send("0x{0:X} : {1}", CurrentPos, text); OffsetMap.Add(CurrentPos, text); } // OK, here's how I'm going to code this: kinda like how BrawlLib works, first I'll calculate the size of // each written element, and use that to build up a list of offsets. Then, I'll actually write out the data. // This code will also handle building the string table. #region Offset Data Storage private Dictionary ModelOffsets; private Dictionary BytecodeOffsets; private Dictionary NodeOffsets; private Dictionary VtxPosOffsets; private Dictionary VtxNrmOffsets; private Dictionary VtxClrOffsets; private Dictionary VtxTexCoordOffsets; private Dictionary MaterialOffsets; private Dictionary MaterialDLOffsets; private Dictionary ShaderOffsets; private Dictionary ShapeOffsets; private Dictionary ShapeDL1Offsets; private Dictionary ShapeDL2Offsets; private Dictionary ShapeSizes; private Dictionary, int> PairingOffsets; private Dictionary TextureInfoOffsets; // Models have lots of extra offset data attached private class ModelCalcInfo { public int MatrixIDtoNodeID, Bytecode, Nodes; public int VtxPosData, VtxNrmData, VtxClrData, VtxTexCoordData; public int Materials, Shaders, Shapes; public int PairingLookupByTexture; public int BlockSize; // Duplicate shader structs are removed from the model, so they require special tracking public List UniqueShaders; } private Dictionary ModelCalcInfos; private Dictionary TextureOffsets; private Dictionary TextureDataOffsets; private Dictionary RootDictOffsets; private int CurrentPos; private int BlockCount; private int RootBlockSize; #endregion #region Offset/String Table Calculation private void CalculateRoot() { // Where it all starts! InitialiseStringTable(); // First up: BRRES header CurrentPos = 0x10; // First block, and ResDict CurrentPos += 8; CurrentPos += GetSizeForResDict(File.Count); RootBlockSize = 8 + GetSizeForResDict(File.Count); // "root" block counts BlockCount = 1; RootDictOffsets = new Dictionary(); // Now do each ResDict in the File foreach (var kv in File) { LogPosition("ResDict: " + kv.Key); RootDictOffsets[kv.Value] = CurrentPos; CurrentPos += GetSizeForResDict((kv.Value as ICollection).Count); RootBlockSize += GetSizeForResDict((kv.Value as ICollection).Count); BlockCount += (kv.Value as ICollection).Count; } // OK, so that's done. Process each type foreach (var name in File.Keys) { AddString(name); switch (name) { case "3DModels(NW4R)": CalculateModels(); break; case "Textures(NW4R)": CalculateTextures(); break; default: Debug.Send("[[ UNIMPLEMENTED {0} ]]", name); break; } } // ... and done with that. Build the string table and go! AlignCalcPos(4); LogPosition("String Table"); CalculateStringTable(); } #region Model Calculation private void CalculateModels() { ModelOffsets = new Dictionary(); ModelCalcInfos = new Dictionary(); BytecodeOffsets = new Dictionary(); NodeOffsets = new Dictionary(); VtxPosOffsets = new Dictionary(); VtxNrmOffsets = new Dictionary(); VtxClrOffsets = new Dictionary(); VtxTexCoordOffsets = new Dictionary(); MaterialOffsets = new Dictionary(); MaterialDLOffsets = new Dictionary(); ShaderOffsets = new Dictionary(); ShapeOffsets = new Dictionary(); ShapeDL1Offsets = new Dictionary(); ShapeDL2Offsets = new Dictionary(); ShapeSizes = new Dictionary(); PairingOffsets = new Dictionary, int>(); TextureInfoOffsets = new Dictionary(); foreach (var kv in File.GetGroup("3DModels(NW4R)")) { AddString(kv.Key); AlignCalcPos(0x20); // 0x40? dunno Model model = kv.Value; var calcInfo = ModelCalcInfos[model] = new BrresWriter.ModelCalcInfo(); // This is used later to calculate the MDL0 block size easily int startPos = CurrentPos; LogPosition("Model: " + kv.Key); ModelOffsets.Add(model, CurrentPos); CurrentPos += 0x4C; LogPosition("Model Info Struct for: " + kv.Key); CurrentPos += 0x40; LogPosition("Matrix ID to Node ID Data for: " + kv.Key); calcInfo.MatrixIDtoNodeID = CurrentPos; CurrentPos += 4 + (model.MatrixIDtoNodeID.Length * 4); LogPosition("ResDict: ByteCode"); calcInfo.Bytecode = CurrentPos; CurrentPos += GetSizeForResDict(model.Bytecode.Count); LogPosition("ResDict: Nodes"); calcInfo.Nodes = CurrentPos; CurrentPos += GetSizeForResDict(model.Nodes.Count); LogPosition("ResDict: VertexPosData"); calcInfo.VtxPosData = CurrentPos; CurrentPos += GetSizeForResDict(model.VtxPosData.Count); if (model.VtxNrmData.Count > 0) { LogPosition("ResDict: VertexNrmData"); calcInfo.VtxNrmData = CurrentPos; CurrentPos += GetSizeForResDict(model.VtxNrmData.Count); } if (model.VtxClrData.Count > 0) { LogPosition("ResDict: VertexClrData"); calcInfo.VtxClrData = CurrentPos; CurrentPos += GetSizeForResDict(model.VtxClrData.Count); } if (model.VtxTexCoordData.Count > 0) { LogPosition("ResDict: VertexTexCoordData"); calcInfo.VtxTexCoordData = CurrentPos; CurrentPos += GetSizeForResDict(model.VtxTexCoordData.Count); } LogPosition("ResDict: Materials"); calcInfo.Materials = CurrentPos; CurrentPos += GetSizeForResDict(model.Materials.Count); LogPosition("ResDict: Shaders"); calcInfo.Shaders = CurrentPos; CurrentPos += GetSizeForResDict(model.Shaders.Count); LogPosition("ResDict: Shapes"); calcInfo.Shapes = CurrentPos; CurrentPos += GetSizeForResDict(model.Shapes.Count); if (model.PairingLookupByTexture.Count > 0) { LogPosition("ResDict: Texture Lookup"); calcInfo.PairingLookupByTexture = CurrentPos; CurrentPos += GetSizeForResDict(model.PairingLookupByTexture.Count); } // todo: palette lookup, checking if dicts are empty or do not exist // todo: can dicts even NOT exist? must find this out CalculatePairings(model, model.PairingLookupByTexture); CalculateBytecode(model); CalculateNodes(model); CalculateMaterials(model); calcInfo.UniqueShaders = new List(); CalculateShaders(model); CalculateShapes(model); CalculateVtxPosData(model); CalculateVtxNrmData(model); CalculateVtxClrData(model); CalculateVtxTexCoordData(model); // Here we go! calcInfo.BlockSize = CurrentPos - startPos; } } private void CalculatePairings(Model m, ResDict> dict) { foreach (var kv in dict) { LogPosition("Texture/Material Pairing List for: " + kv.Key); PairingOffsets.Add(kv.Value, CurrentPos); CurrentPos += 4 + (kv.Value.Count * 8); } } private void CalculateBytecode(Model m) { foreach (var kv in m.Bytecode) { AddString(kv.Key); ByteCode bc = kv.Value; LogPosition("ByteCode: " + kv.Key); BytecodeOffsets.Add(kv.Value, CurrentPos); foreach (var insn in bc.Instructions) { switch (insn.GetOp()) { case ByteCode.OpType.None: CurrentPos += 1; break; case ByteCode.OpType.Done: CurrentPos += 1; break; case ByteCode.OpType.AssignNodeToParentMtx: CurrentPos += 5; break; case ByteCode.OpType.BlendMatrices: CurrentPos += 4 + (6 * (insn as ByteCode.BlendMatricesInstruction).BlendedMatrices.Length); break; case ByteCode.OpType.DrawShape: CurrentPos += 8; break; case ByteCode.OpType.AssignMtxToNode: CurrentPos += 5; break; } } } AlignCalcPos(4); // should be per-bytecode maybe? } private void CalculateNodes(Model m) { foreach (var kv in m.Nodes) { AddString(kv.Key); LogPosition("Node: " + kv.Key); NodeOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0xD0; } } private void CalculateMaterials(Model m) { foreach (var kv in m.Materials) { AddString(kv.Key); LogPosition("Material: " + kv.Key); MaterialOffsets.Add(kv.Value, CurrentPos); // Base material struct CurrentPos += 0x14; // ResGenMode CurrentPos += 8; // ResMatMode CurrentPos += 0xC; // other stuff CurrentPos += 0x18; // ResTexObj LogPosition("Material ResTexObj: " + kv.Key); CurrentPos += 0x104; // ResTlutObj LogPosition("Material ResTlutObj: " + kv.Key); CurrentPos += 0x64; // ResTexSrt LogPosition("Material ResTexSrt: " + kv.Key); CurrentPos += 8 + (0x14 * 8) + (0x34 * 8); // ResMatChan LogPosition("Material ResMatChan: " + kv.Key); CurrentPos += 0x28; // Texture Infos if (kv.Value.TextureInfos.Count > 0) LogPosition("Material Texture Infos: " + kv.Key); for (int i = 0; i < kv.Value.TextureInfos.Count; i++) { TextureInfoOffsets[kv.Value.TextureInfos[i]] = CurrentPos; CurrentPos += 0x34; } // Display Lists AlignCalcPos(0x20); LogPosition("Material Display Lists: " + kv.Key); MaterialDLOffsets[kv.Value] = CurrentPos; CurrentPos += 0x20 + 0x80 + 0x40 + 0xA0; } } private void CalculateShaders(Model m) { // Oh great, now I need to optimise this by removing duplicate shaders. // I'm doing it like this: I'll loop through every shader in the ResDict. // I'll check each one against the UniqueShaders list. // If it's not in the list, I'll add it there and add it to the resource data. var calcInfo = ModelCalcInfos[m]; foreach (var kv in m.Shaders) { AddString(kv.Key); // Check to see if it's in the UniqueShaders list already // This is made slightly difficult -- shaders can be different object instances // yet still match, so simple reference comparison isn't good enough. // So, I added a "DataMatches" method to the Shader class that checks the // data in two Shaders and sees if they match. bool wasFound = false; Shader match = null; foreach (var checkAgainst in calcInfo.UniqueShaders) { if (checkAgainst.DataMatches(kv.Value)) { wasFound = true; match = checkAgainst; break; } } if (wasFound) { // It's already in there. int positionOfMatch = ShaderOffsets[match]; OffsetMap[positionOfMatch] = OffsetMap[positionOfMatch] + ", " + kv.Key; // Add it to ShaderOffsets so we can link it up to the shader ResDict ShaderOffsets.Add(kv.Value, positionOfMatch); } else { // This is a new shader, add it! calcInfo.UniqueShaders.Add(kv.Value); LogPosition("Shader: " + kv.Key); ShaderOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0x200; } } } private void CalculateShapes(Model m) { foreach (var kv in m.Shapes) { AddString(kv.Key); LogPosition("Shape: " + kv.Key); ShapeOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0x68; // extra data CurrentPos += 4; CurrentPos += (kv.Value.ExtraData.Length * 2); // display lists AlignCalcPos(0x20); ShapeDL1Offsets.Add(kv.Value, CurrentPos); LogPosition("Shape DL 1: " + kv.Key); CurrentPos += (int)kv.Value.DLBufferSize1; AlignCalcPos(0x20); ShapeDL2Offsets.Add(kv.Value, CurrentPos); LogPosition("Shape DL 2: " + kv.Key); CurrentPos += (int)kv.Value.DLBufferSize2; // Should this line be after the final alignment? // Does it even matter? I'd assume DL sizes will be aligned to 0x20... ShapeSizes[kv.Value] = CurrentPos - ShapeOffsets[kv.Value]; AlignCalcPos(0x20); } } private void CalculateVtxPosData(Model m) { foreach (var kv in m.VtxPosData) { AddString(kv.Key); LogPosition("VertexPosData: " + kv.Key); VtxPosOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; // Minimum/maximum VEC3 (specific to VtxPosData) CurrentPos += 0x18; AlignCalcPos(0x20); LogPosition("Data: " + kv.Key); CurrentPos += kv.Value.EntryCount * kv.Value.EntrySize; AlignCalcPos(0x20); } } private void CalculateVtxNrmData(Model m) { foreach (var kv in m.VtxNrmData) { AddString(kv.Key); LogPosition("VertexNrmData: " + kv.Key); VtxNrmOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; AlignCalcPos(0x20); LogPosition("Data: " + kv.Key); CurrentPos += kv.Value.EntryCount * kv.Value.EntrySize; AlignCalcPos(0x20); } } private void CalculateVtxClrData(Model m) { foreach (var kv in m.VtxClrData) { AddString(kv.Key); LogPosition("VertexClrData: " + kv.Key); VtxClrOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; AlignCalcPos(0x20); LogPosition("Data: " + kv.Key); CurrentPos += kv.Value.EntryCount * kv.Value.EntrySize; AlignCalcPos(0x20); } } private void CalculateVtxTexCoordData(Model m) { foreach (var kv in m.VtxTexCoordData) { AddString(kv.Key); LogPosition("VertexTexCoordData: " + kv.Key); VtxTexCoordOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; // Minimum/maximum VEC2 (specific to VtxTexCoordData) CurrentPos += 0x10; AlignCalcPos(0x20); LogPosition("Data: " + kv.Key); CurrentPos += kv.Value.EntryCount * kv.Value.EntrySize; AlignCalcPos(0x20); } } #endregion #region Texture Calculation private void CalculateTextures() { TextureOffsets = new Dictionary(); TextureDataOffsets = new Dictionary(); var textureDict = File.GetGroup("Textures(NW4R)"); foreach (var kv in textureDict) { AddString(kv.Key); AlignCalcPos(0x20); Texture texture = kv.Value; Debug.Send("Current: {0}", kv.Key); LogPosition("Texture: " + kv.Key); TextureOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0x30; AlignCalcPos(0x20); LogPosition("Texture Data for: " + kv.Key); TextureDataOffsets.Add(kv.Value, CurrentPos); CurrentPos += texture.GetDataSize(); AlignCalcPos(0x20); } } #endregion private void AlignCalcPos(int alignTo) { if ((CurrentPos & (alignTo - 1)) == 0) return; CurrentPos += (alignTo - (CurrentPos & (alignTo - 1))); } private int GetSizeForResDict(int entryCount) { return 8 + ((entryCount + 1) * 0x10); } #endregion #region Writing Data private void WriteRoot() { // magic "bres", endian, version, file size Output.WriteUInt32(0x62726573); Output.WriteUInt16(0xFEFF); Output.WriteUInt16(File.Version); Output.WriteUInt32((uint)(((CurrentPos + StringTableData.Position) + 0x3F) & ~0x3F)); // header size, block count Output.WriteUInt16(0x10); Output.WriteUInt16((ushort)BlockCount); // first block: magic "root", block size Output.WriteUInt32(0x726F6F74); Output.WriteUInt32((uint)RootBlockSize); // root dictionaries WriteResDict(File, RootDictOffsets); using (var c = Debug.Push("Root Dictionaries")) { foreach (var kv in File) { Debug.Send(kv.Key); switch (kv.Key) { case "3DModels(NW4R)": WriteResDict(kv.Value as ResDict, ModelOffsets); break; case "Textures(NW4R)": WriteResDict(kv.Value as ResDict, TextureOffsets); break; default: // for testing, boo Output.AddPadding(GetSizeForResDict((kv.Value as ICollection).Count)); Debug.Send("UNHANDLED RESOURCE TYPE: {0}", kv.Key); break; } } } // now write the actual data using (var c = Debug.Push("Root Data")) { foreach (var kv in File) { switch (kv.Key) { case "3DModels(NW4R)": WriteModels(); break; case "Textures(NW4R)": WriteTextures(); break; default: Debug.Send("UNHANDLED RESOURCE TYPE: {0}", kv.Key); break; } } } Output.AlignTo(4); Output.WriteBytes(StringTableData.GetBuffer()); // before we finish. pad the file Output.AlignTo(0x40); } #region Model Writing private void WriteModels() { foreach (var kv in File.GetGroup("3DModels(NW4R)")) { using (var c = Debug.Push("Model: {0}", kv.Key)) { Output.AlignTo(0x20); int startPos = Output.Position; Model model = kv.Value; var calcInfo = ModelCalcInfos[model]; // Base struct: magic 'MDL0', block size, version [currently 11], resfile offset Output.WriteUInt32(0x4D444C30); Output.WriteUInt32((uint)calcInfo.BlockSize); Output.WriteUInt32(11); // Note: "0 - startPos" *DOESN'T* work when compiling under Mono! // Details: https://bugzilla.novell.com/show_bug.cgi?id=675777 // So just use "-startPos". Output.WriteInt32(-startPos); // More offsets for a ton of crap Output.WriteInt32(calcInfo.Bytecode - startPos); Output.WriteInt32(calcInfo.Nodes - startPos); Output.WriteInt32(calcInfo.VtxPosData - startPos); Output.WriteInt32((calcInfo.VtxNrmData == 0) ? 0 : (calcInfo.VtxNrmData - startPos)); Output.WriteInt32((calcInfo.VtxClrData == 0) ? 0 : (calcInfo.VtxClrData - startPos)); Output.WriteInt32((calcInfo.VtxTexCoordData == 0) ? 0 : (calcInfo.VtxTexCoordData - startPos)); // VtxFurVec, VtxFurPos (not handled) Output.WriteInt32(0); Output.WriteInt32(0); Output.WriteInt32(calcInfo.Materials - startPos); Output.WriteInt32(calcInfo.Shaders - startPos); Output.WriteInt32(calcInfo.Shapes - startPos); Output.WriteInt32((calcInfo.PairingLookupByTexture == 0) ? 0 : (calcInfo.PairingLookupByTexture - startPos)); // Pairing lookup by palette, unhandled atm Output.WriteInt32(0); // Unknown extra data Output.WriteInt32(0); // Name offset Output.WriteInt32(StringPositions[kv.Key] - startPos); // Model Info struct: struct size, model offset, some other stuff int infoStructPos = Output.Position; Output.WriteUInt32(0x40); Output.WriteInt32(startPos - infoStructPos); Output.WriteUInt32((uint)model.ScaleMode); Output.WriteUInt32((uint)model.TexMatrixMode); Output.WriteUInt32((uint)model.VertexCount); Output.WriteUInt32((uint)model.TriangleCount); Output.WriteUInt32(0); Output.WriteUInt32((uint)model.MatrixIDtoNodeID.Length); Output.WriteByte(model.UsesNrmMtxArray ? (byte)1 : (byte)0); Output.WriteByte(model.UsesTexMtxArray ? (byte)1 : (byte)0); Output.AddPadding(2); Output.WriteInt32(calcInfo.MatrixIDtoNodeID - infoStructPos); Output.WriteVec3(model.Minimum); Output.WriteVec3(model.Maximum); // Matrix ID to Node ID data Output.WriteUInt32((uint)model.MatrixIDtoNodeID.Length); for (int i = 0; i < model.MatrixIDtoNodeID.Length; i++) { Output.WriteInt32(model.MatrixIDtoNodeID[i]); } // ResDicts WriteResDict(model.Bytecode, BytecodeOffsets); WriteResDict(model.Nodes, NodeOffsets); WriteResDict(model.VtxPosData, VtxPosOffsets); if (model.VtxNrmData.Count > 0) WriteResDict(model.VtxNrmData, VtxNrmOffsets); if (model.VtxClrData.Count > 0) WriteResDict(model.VtxClrData, VtxClrOffsets); if (model.VtxTexCoordData.Count > 0) WriteResDict(model.VtxTexCoordData, VtxTexCoordOffsets); WriteResDict(model.Materials, MaterialOffsets); WriteResDict(model.Shaders, ShaderOffsets); WriteResDict(model.Shapes, ShapeOffsets); if (model.PairingLookupByTexture.Count > 0) WriteResDict>(model.PairingLookupByTexture, PairingOffsets); // TODO: Palette pairing lookups WritePairings(model, model.PairingLookupByTexture); WriteBytecode(model); WriteNodes(model); WriteMaterials(model); WriteShaders(model); WriteShapes(model); WriteVertexData(model, model.VtxPosData); WriteVertexData(model, model.VtxNrmData); WriteVertexData(model, model.VtxClrData); WriteVertexData(model, model.VtxTexCoordData); } } } private void WritePairings(Model m, ResDict> dict) { foreach (var kv in dict) { int startPos = Output.Position; Output.WriteUInt32((uint)kv.Value.Count); foreach (var pairing in kv.Value) { // Material offset Output.WriteInt32(MaterialOffsets[pairing.Material] - startPos); // Texture info offset, points to the TextureInfo struct in the Material data Output.WriteInt32(TextureInfoOffsets[pairing.Texture] - startPos); } } } private void WriteBytecode(Model m) { foreach (var kv in m.Bytecode) { Debug.Send("Writing bytecode {0} @ offset {1:X}", kv.Key, Output.Position); ByteCode bc = kv.Value; foreach (var insn in bc.Instructions) { Output.WriteByte((byte)insn.GetOp()); switch (insn.GetOp()) { case ByteCode.OpType.None: break; case ByteCode.OpType.Done: break; case ByteCode.OpType.AssignNodeToParentMtx: var op2 = insn as ByteCode.AssignNodeToParentMtxInstruction; Output.WriteUInt16(op2.NodeID); Output.WriteUInt16(op2.ParentMatrixID); break; case ByteCode.OpType.BlendMatrices: var op3 = insn as ByteCode.BlendMatricesInstruction; Output.WriteUInt16(op3.MatrixID); Output.WriteByte((byte)op3.BlendedMatrices.Length); foreach (var bm in op3.BlendedMatrices) { Output.WriteUInt16(bm.MatrixID); Output.WriteFloat(bm.Ratio); } break; case ByteCode.OpType.DrawShape: var op4 = insn as ByteCode.DrawShapeInstruction; Output.WriteUInt16(op4.MaterialID); Output.WriteUInt16(op4.ShapeID); Output.WriteUInt16(op4.NodeID); Output.WriteByte(0); break; case ByteCode.OpType.AssignMtxToNode: var op5 = insn as ByteCode.AssignMtxToNodeInstruction; Output.WriteUInt16(op5.MatrixID); Output.WriteUInt16(op5.NodeID); break; } } } Output.AlignTo(4); } private void WriteNodes(Model m) { int currentIndex = 0; foreach (var kv in m.Nodes) { Debug.Send("Writing node {0} @ offset {1:X}", kv.Key, Output.Position); Node node = kv.Value; int startPos = Output.Position; // Size, model offset, name offset, index // Reminds me... TODO: remove "Index" from the Node/Material/... classes Output.WriteUInt32(0xD0); Output.WriteInt32(ModelOffsets[m] - startPos); Output.WriteInt32(StringPositions[kv.Key] - startPos); Output.WriteUInt32((uint)currentIndex); Output.WriteUInt32(node.MatrixID); Output.WriteUInt32(node.Flags); Output.WriteUInt32((uint)node.BillboardMode); Output.WriteUInt32(0); // TODO: might be swapped with bbmode, check asm to confirm Output.WriteVec3(node.Scale); Output.WriteVec3(node.Rotation); Output.WriteVec3(node.Translation); Output.WriteVec3(node.BoxMin); Output.WriteVec3(node.BoxMax); // node offsets: parent, child, next, previous Output.WriteInt32((node.Parent == null) ? 0 : (NodeOffsets[node.Parent] - startPos)); Output.WriteInt32((node.FirstChild == null) ? 0 : (NodeOffsets[node.FirstChild] - startPos)); Output.WriteInt32((node.Next == null) ? 0 : (NodeOffsets[node.Next] - startPos)); Output.WriteInt32((node.Previous == null) ? 0 : (NodeOffsets[node.Previous] - startPos)); // extra data offset (currently unhandled) Output.WriteInt32(0); // matrices Output.WriteMatrix(node.NodeMatrix); Output.WriteMatrix(node.NodeInvMatrix); // done currentIndex++; } } private void WriteMaterials(Model m) { int currentIndex = 0; foreach (var kv in m.Materials) { Debug.Send("Writing material {0} @ offset {1:X}", kv.Key, Output.Position); Material mat = kv.Value; int startPos = Output.Position; // size, model offset, name offset, index // is size static, or does it change? must verify Output.WriteUInt32(0x5E8); Output.WriteInt32(ModelOffsets[m] - startPos); Output.WriteInt32(StringPositions[kv.Key] - startPos); Output.WriteUInt32((uint)currentIndex); Output.WriteUInt32(mat.Flags); // ResGenMode Output.WriteByte(mat.TexCoordGenCount); Output.WriteByte(mat.ChanCount); Output.WriteByte(mat.TevStageCount); Output.WriteByte(mat.IndStageCount); Output.WriteUInt32(mat.CullMode); // ResMatMisc Output.WriteByte(mat.ZCompLoc); Output.WriteByte(mat.LightSetID); Output.WriteByte(mat.FogID); // are these even correct? maybe not Output.WriteBytes(mat.IndirectTexMtxCalcMethod1); Output.WriteBytes(mat.IndirectTexMtxCalcMethod2); // padding -- this is weird, the previous two might be incorrect Output.WriteByte(0xFF); // more stuff: shader offset, texinfo count, texinfo offset, fur offset, unk offset, DL offset Output.WriteInt32(ShaderOffsets[mat.ShaderRef] - startPos); Output.WriteInt32(mat.TextureInfos.Count); if (mat.TextureInfos.Count > 0) Output.WriteInt32(TextureInfoOffsets[mat.TextureInfos[0]] - startPos); else Output.WriteInt32(0); Output.WriteInt32(0); Output.WriteInt32(0); Output.WriteInt32(MaterialDLOffsets[mat] - startPos); // ResTexObj UInt32 textureFlag = 0; int texObjPadding = 0x100; // first calculate the flag for (int i = 0; i < 8; i++) { if (mat.TexObj[i] != null) { textureFlag |= (uint)(1 << i); texObjPadding -= 0x20; } } // now write it Output.WriteUInt32(textureFlag); for (int i = 0; i < 8; i++) { if (mat.TexObj[i] != null) { Output.WriteBytes(mat.TexObj[i]); } } Output.AddPadding(texObjPadding); // ResTlutObj Output.WriteUInt32(mat.TlutFlag); if (mat.TlutObj == null) { Output.AddPadding(0x60); } else { Output.WriteBytes(mat.TlutObj); Output.AddPadding(0x60 - mat.TlutObj.Length); } // ResTexSrt // this one is a bit of a pain UInt32 srtFlag = 0; int unusedSlots = 8; // first, calculate the flag for (int i = 0; i < 8; i++) { byte thisFlag = 0; if (mat.SRTSettings[i] != null) { thisFlag |= 1; if (mat.SRTSettings[i].ScaleX == 1.0f && mat.SRTSettings[i].ScaleY == 1.0f) thisFlag |= 2; if (mat.SRTSettings[i].Rotate == 0.0f) thisFlag |= 4; if (mat.SRTSettings[i].TranslateX == 0.0f && mat.SRTSettings[i].TranslateY == 0.0f) thisFlag |= 8; unusedSlots--; } srtFlag |= ((uint)thisFlag << (i * 4)); } Output.WriteUInt32(srtFlag); Output.WriteUInt32(mat.TexMatrixType); // now write out TexSrt for (int i = 0; i < 8; i++) { if (mat.SRTSettings[i] != null) { Output.WriteFloat(mat.SRTSettings[i].ScaleX); Output.WriteFloat(mat.SRTSettings[i].ScaleY); Output.WriteFloat(mat.SRTSettings[i].Rotate); Output.WriteFloat(mat.SRTSettings[i].TranslateX); Output.WriteFloat(mat.SRTSettings[i].TranslateY); } } // frustratingly, the padding here is NOT a section of zeroes, but instead // a blank section (scale=1.0 rotate=0.0 translate=0.0). for (int i = 0; i < unusedSlots; i++) { Output.WriteFloat(1.0f); Output.WriteFloat(1.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); } // then write out TexSet for (int i = 0; i < 8; i++) { if (mat.SRTSettings[i] != null) { Output.WriteByte(mat.SRTSettings[i].CameraID); Output.WriteByte(mat.SRTSettings[i].LightID); Output.WriteByte(mat.SRTSettings[i].MapType); Output.WriteByte(mat.SRTSettings[i].Flags); Output.WriteMatrix(mat.SRTSettings[i].TexMatrix); } } // and TexSet does the same thing for padding -_- for (int i = 0; i < unusedSlots; i++) { Output.WriteByte(0xFF); Output.WriteByte(0xFF); Output.WriteByte(0x00); Output.WriteByte(0x01); Output.WriteFloat(1.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); Output.WriteFloat(1.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); Output.WriteFloat(0.0f); Output.WriteFloat(1.0f); Output.WriteFloat(0.0f); } // ResMatChan for (int i = 0; i < 2; i++) { Output.WriteUInt32(mat.ChanCtrls[i].Flags); Output.WriteColor(mat.ChanCtrls[i].MatColor); Output.WriteColor(mat.ChanCtrls[i].AmbColor); Output.WriteUInt32(mat.ChanCtrls[i].FlagC); Output.WriteUInt32(mat.ChanCtrls[i].FlagA); } // Texture Infos foreach (var texInfo in mat.TextureInfos) { int texInfoPos = Output.Position; Output.WriteInt32(texInfo.TextureName == null ? 0 : (StringPositions[texInfo.TextureName] - texInfoPos)); Output.WriteInt32(texInfo.PaletteName == null ? 0 : (StringPositions[texInfo.PaletteName] - texInfoPos)); // placeholder pointers Output.WriteUInt32(0); Output.WriteUInt32(0); Output.WriteUInt32(texInfo.TexMapID); Output.WriteUInt32(texInfo.TlutID); Output.WriteUInt32((uint)texInfo.WrapS); Output.WriteUInt32((uint)texInfo.WrapT); Output.WriteUInt32(texInfo.MinFilt); Output.WriteUInt32(texInfo.MagFilt); Output.WriteFloat(texInfo.LODBias); Output.WriteUInt32(texInfo.MaxAniso); Output.WriteBool(texInfo.BiasClamp); Output.WriteBool(texInfo.DoEdgeLOD); Output.AddPadding(2); } // Display Lists Output.AlignTo(0x20); if (mat.PixDL.Length != 0x20) throw new Exception("Material " + kv.Key + " PixDL must be size 0x20"); if (mat.TevColorDL.Length != 0x80) throw new Exception("Material " + kv.Key + " TevColorDL must be size 0x80"); if (mat.IndMtxAndScaleDL.Length != 0x40) throw new Exception("Material " + kv.Key + " IndMtxAndScaleDL must be size 0x40"); if (mat.TexCoordGenDL.Length != 0xA0) throw new Exception("Material " + kv.Key + " TexCoordGenDL must be size 0xA0"); Output.WriteBytes(mat.PixDL); Output.WriteBytes(mat.TevColorDL); Output.WriteBytes(mat.IndMtxAndScaleDL); Output.WriteBytes(mat.TexCoordGenDL); // I can't believe this is done currentIndex++; } } private void WriteShaders(Model m) { int currentIndex = 0; foreach (var shader in ModelCalcInfos[m].UniqueShaders) { Debug.Send("Writing shader @ offset {0:X}", Output.Position); int startPos = Output.Position; // Size, model offset, index Output.WriteUInt32((uint)(0x20 + shader.DisplayList.Length)); Output.WriteInt32(ModelOffsets[m] - startPos); // What do I write here...? Output.WriteUInt32((uint)currentIndex); Output.WriteByte(shader.TevStageCount); Output.AddPadding(3); Output.WriteUInt32(shader.Unk1); Output.WriteUInt32(shader.Unk2); Output.AddPadding(8); Output.WriteBytes(shader.DisplayList); // done currentIndex++; } } private void WriteShapes(Model m) { int currentIndex = 0; foreach (var kv in m.Shapes) { Debug.Send("Writing shape {0} @ offset {1:X}", kv.Key, Output.Position); Shape shape = kv.Value; int startPos = Output.Position; // Size, model offset, matrix ID, unknown data Output.WriteUInt32((uint)ShapeSizes[shape]); Output.WriteInt32(ModelOffsets[m] - startPos); Output.WriteInt32(shape.MatrixID); Output.WriteBytes(shape.Unk); // Display Lists int dlBase1 = Output.Position; Output.WriteUInt32(shape.DLBufferSize1); Output.WriteUInt32((uint)shape.DisplayList1.Length); Output.WriteInt32(ShapeDL1Offsets[shape] - dlBase1); int dlBase2 = Output.Position; Output.WriteUInt32(shape.DLBufferSize2); Output.WriteUInt32((uint)shape.DisplayList2.Length); Output.WriteInt32(ShapeDL2Offsets[shape] - dlBase2); // Indexes and flags and other stuff Output.WriteUInt32(shape.DataFlags); Output.WriteUInt32(shape.Flags); Output.WriteInt32(StringPositions[kv.Key] - startPos); Output.WriteUInt32((uint)currentIndex); Output.WriteUInt32(shape.VertexCount); Output.WriteUInt32(shape.PolygonCount); Output.WriteInt16((short)m.VtxPosData.GetIndexForValue(shape.PosData)); if (shape.NrmData != null) Output.WriteInt16((short)m.VtxNrmData.GetIndexForValue(shape.NrmData)); else Output.WriteInt16(-1); for (int i = 0; i < 2; i++) { if (shape.ClrData[i] != null) Output.WriteInt16((short)m.VtxClrData.GetIndexForValue(shape.ClrData[i])); else Output.WriteInt16(-1); } for (int i = 0; i < 8; i++) { if (shape.TexCoordData[i] != null) Output.WriteInt16((short)m.VtxTexCoordData.GetIndexForValue(shape.TexCoordData[i])); else Output.WriteInt16(-1); } // Indexes for VtxFurVec and VtxFurPos, not used here Output.WriteInt16(-1); Output.WriteInt16(-1); // Extra data offset (fixed) Output.WriteInt32(0x68); // Extra data Output.WriteInt32(shape.ExtraData.Length); for (int i = 0; i < shape.ExtraData.Length; i++) Output.WriteUInt16(shape.ExtraData[i]); // Display lists // Padding is added to fill up the buffer (Display List data size can be smaller than the buffer size) Output.AlignTo(0x20); Output.WriteBytes(shape.DisplayList1); Output.AddPadding((int)(shape.DLBufferSize1 - shape.DisplayList1.Length)); Output.AlignTo(0x20); Output.WriteBytes(shape.DisplayList2); Output.AddPadding((int)(shape.DLBufferSize2 - shape.DisplayList2.Length)); Output.AlignTo(0x20); // done! currentIndex++; } } private void WriteVertexData(Model m, ResDict dict) where T : VertexDataBase { int currentIndex = 0; foreach (var kv in dict) { Debug.Send("Writing vtxdata {0} @ offset {1:X}", kv.Key, Output.Position); int startPos = Output.Position; int structSize = 0x20; if (kv.Value is VertexPosData || kv.Value is VertexTexCoordData) { // Min/max fields, plus alignment structSize += 0x20; } // Size, model offset, data offset, name offset, index // For some reason, the raw data length added to the size is aligned... Output.WriteUInt32((uint)(((structSize + kv.Value.RawData.Length) + 0x1F) & ~0x1F)); Output.WriteInt32(ModelOffsets[m] - startPos); Output.WriteInt32(structSize); Output.WriteInt32(StringPositions[kv.Key] - startPos); Output.WriteUInt32((uint)currentIndex); // Parameters Output.WriteUInt32((uint)kv.Value.ComponentCount); Output.WriteUInt32((uint)kv.Value.ComponentType); if (kv.Value is VertexClrData) { Output.WriteByte(kv.Value.EntrySize); Output.AddPadding(1); } else { Output.WriteByte(kv.Value.Fraction); Output.WriteByte(kv.Value.EntrySize); } Output.WriteUInt16(kv.Value.EntryCount); // Type-specific stuff if (kv.Value is VertexPosData) { var posData = kv.Value as VertexPosData; Output.WriteVec3(posData.Minimum); Output.WriteVec3(posData.Maximum); } else if (kv.Value is VertexTexCoordData) { var tcData = kv.Value as VertexTexCoordData; Output.WriteVec2(tcData.Minimum); Output.WriteVec2(tcData.Maximum); } Output.AlignTo(0x20); Output.WriteBytes(kv.Value.RawData); Output.AlignTo(0x20); // done! currentIndex++; } } #endregion #region Texture Writing private void WriteTextures() { foreach (var kv in File.GetGroup("Textures(NW4R)")) { using (var c = Debug.Push("Texture: {0}", kv.Key)) { Output.AlignTo(0x20); int startPos = Output.Position; Texture tex = kv.Value; // Base struct: magic 'TEX0', block size, version [currently 11], resfile offset Output.WriteUInt32(0x54455830); Output.WriteUInt32((uint)(0x40 + tex.GetDataSize())); Output.WriteUInt32(3); Output.WriteInt32(-startPos); // Data offset, name offset Output.WriteInt32(0x40); Output.WriteInt32(StringPositions[kv.Key] - startPos); // Flags -- Stores nothing interesting, just an "is CI" flag (0x1) // We don't handle that atm, so ignore it Output.WriteUInt32(0); Output.WriteInt16((short)tex.Images[0].Width); Output.WriteInt16((short)tex.Images[0].Height); Output.WriteUInt32((uint)tex.Format); Output.WriteInt32(tex.Images.Length); Output.WriteFloat(tex.MinLOD); Output.WriteFloat(tex.MaxLOD); Output.AlignTo(0x20); for (int i = 0; i < tex.Images.Length; i++) Output.WriteBytes(tex.ExportData(i)); } } } #endregion #region ResDicts private struct RawDictEntry { public ushort Ref; public ushort Unk; public ushort ILeft; public ushort IRight; } private unsafe void WriteResDict(ResDict dict, Dictionary positions) { int dictPos = Output.Position; // First, I've got to build an in-memory representation of the raw ResDict. // Next, I've got to write it out. This'll be fun. // This is partly based off the BrawlLib code. RawDictEntry[] rd = new RawDictEntry[dict.Count + 1]; byte[][] encodedNames = new byte[dict.Count + 1][]; // Before the actual calculation, build that list rd[0].Ref = 0xFFFF; rd[0].Unk = 0; rd[0].ILeft = 0; rd[0].IRight = 0; encodedNames[0] = new byte[] { }; for (int i = 1; i <= dict.Count; i++) { rd[i].Ref = 0; rd[i].Unk = 0; rd[i].ILeft = 0; rd[i].IRight = 0; // I wanted to store the encoded name as a member of RawDictEntry, but C# doesn't let me // get a pointer to the struct if I do that -- see Compiler Error CS0208 on MSDN. string theName = dict.GetKeyForIndex(i - 1); encodedNames[i] = System.Text.Encoding.GetEncoding("Shift_JIS").GetBytes(theName); } // Now calculate indexes, etc for (ushort i = 1; i <= dict.Count; i++) { // Using unsafe pointers for convenience fixed (RawDictEntry* entry = &rd[i]) { ushort prev = 0; ushort current = rd[prev].ILeft; bool isRight = false; int strLen = encodedNames[i].Length; byte[] pChar = encodedNames[i]; byte[] sChar; int eIndex = strLen - 1; int eBits = CompareBits(pChar[eIndex], 0); int val; entry->Ref = (ushort)((eIndex << 3) | eBits); entry->ILeft = (ushort)i; entry->IRight = (ushort)i; while ((entry->Ref <= rd[current].Ref) && (rd[prev].Ref > rd[current].Ref)) { if (entry->Ref == rd[current].Ref) { sChar = encodedNames[current]; for (eIndex = strLen; (--eIndex > 0) && (pChar[eIndex] == sChar[eIndex]);); eBits = CompareBits(pChar[eIndex], sChar[eIndex]); entry->Ref = (ushort)((eIndex << 3) | eBits); if (((sChar[eIndex] >> eBits) & 1) != 0) { entry->ILeft = (ushort)i; entry->IRight = current; } else { entry->ILeft = current; entry->IRight = (ushort)i; } } isRight = ((val = rd[current].Ref >> 3) < strLen) && (((pChar[val] >> (rd[current].Ref & 7)) & 1) != 0); prev = current; current = isRight ? rd[current].IRight : rd[current].ILeft; } sChar = encodedNames[current]; val = sChar == null ? 0 : sChar.Length; if ((val == strLen) && (((sChar[eIndex] >> eBits) & 1) != 0)) entry->IRight = current; else entry->ILeft = current; if (isRight) rd[prev].IRight = i; else rd[prev].ILeft = i; } } // Now write it Output.WriteUInt32((uint)(8 + (rd.Length * 0x10))); Output.WriteUInt32((uint)(rd.Length - 1)); for (int entryID = 0; entryID < rd.Length; entryID++) { Output.WriteUInt16(rd[entryID].Ref); Output.WriteUInt16(rd[entryID].Unk); Output.WriteUInt16(rd[entryID].ILeft); Output.WriteUInt16(rd[entryID].IRight); if (entryID == 0) { Output.WriteInt32(0); Output.WriteInt32(0); } else { // Name offset Output.WriteInt32(StringPositions[dict.GetKeyForIndex(entryID - 1)] - dictPos); // Data offset Output.WriteInt32(positions[dict[entryID - 1]] - dictPos); } } } private static int CompareBits(byte b1, byte b2) { int b = 0x80; for (int i = 7; i != 0; i--) { if ((b1 & b) != (b2 & b)) return i; b >>= 1; } return 0; } #endregion #endregion #region String Table Handling // Contains the strings while they're being pulled from the ResFile data private List StringTable; // Once every string used in the .brres is found, CalculateStringTable() is called, which // writes the table to an OutputStream and saves every offset private Dictionary StringPositions; private OutputStream StringTableData; // Called at the start of the process private void InitialiseStringTable() { StringTable = new List(); } private void AddString(string str) { if (!StringTable.Contains(str)) { StringTable.Add(str); } } private void CalculateStringTable() { // Hmm... this doesn't work for whatever reason? Might be a Mono thing. //StringTable.Sort(StringComparer.Create(System.Globalization.CultureInfo.InvariantCulture, false)); StringTable.Sort(StringComparer.Ordinal); StringPositions = new Dictionary(); StringTableData = new OutputStream(ByteEndian.BigEndian); // Note: once all calculation is done, CurrentPos stores the address of the string table foreach (var s in StringTable) { // Add 4 because the Names are referenced by the string data, and not by the ResName struct itself StringPositions[s] = CurrentPos + StringTableData.Position + 4; StringTableData.WriteName(s); } } #endregion } }