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(); Output.WriteBytes(StringTableData.GetBuffer()); using (var c = Debug.Push("Offset Map")) { foreach (var e in OffsetMap) { Debug.Send("0x{0:X} : {1}", e.Key, e.Value); } } return Output.GetBuffer(); } // 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 ShaderOffsets; private Dictionary ShapeOffsets; private Dictionary, int> PairingOffsets; private Dictionary TextureOffsets; private Dictionary TextureDataOffsets; #endregion #region Offset/String Table Calculation private int CurrentPos; private void CalculateRoot() { // Where it all starts! InitialiseStringTable(); // First up: BRRES header CurrentPos = 0x10; // First block, and ResDict CurrentPos += 8; CurrentPos += GetSizeForResDict(File.Count); // Now do each ResDict in the File foreach (object dict in File.Values) { // yay stupid hack OffsetMap.Add(CurrentPos, "ResDict: " + dict.GetType().GetGenericArguments()[0].ToString()); CurrentPos += GetSizeForResDict((dict 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! CalculateStringTable(); } #region Model Calculation private void CalculateModels() { ModelOffsets = new Dictionary(); BytecodeOffsets = new Dictionary(); NodeOffsets = new Dictionary(); VtxPosOffsets = new Dictionary(); VtxNrmOffsets = new Dictionary(); VtxClrOffsets = new Dictionary(); VtxTexCoordOffsets = new Dictionary(); MaterialOffsets = new Dictionary(); ShaderOffsets = new Dictionary(); ShapeOffsets = new Dictionary(); PairingOffsets = new Dictionary, int>(); var modelDict = File.GetGroup("3DModels(NW4R)"); foreach (var kv in modelDict) { AddString(kv.Key); AlignCalcPos(0x20); // 0x40? dunno Model model = kv.Value; OffsetMap.Add(CurrentPos, "Model: " + kv.Key); CurrentPos += 0x4C; OffsetMap.Add(CurrentPos, "Model Info Struct for: " + kv.Key); CurrentPos += 0x40; OffsetMap.Add(CurrentPos, "Matrix ID to Node ID Data for: " + kv.Key); CurrentPos += 4 + (model.MatrixIDtoNodeID.Length * 4); OffsetMap.Add(CurrentPos, "ResDict: ByteCode"); CurrentPos += GetSizeForResDict(model.Bytecode.Count); OffsetMap.Add(CurrentPos, "ResDict: Nodes"); CurrentPos += GetSizeForResDict(model.Nodes.Count); OffsetMap.Add(CurrentPos, "ResDict: VertexPosData"); CurrentPos += GetSizeForResDict(model.VtxPosData.Count); if (model.VtxNrmData.Count > 0) { OffsetMap.Add(CurrentPos, "ResDict: VertexNrmData"); CurrentPos += GetSizeForResDict(model.VtxNrmData.Count); } if (model.VtxClrData.Count > 0) { OffsetMap.Add(CurrentPos, "ResDict: VertexClrData"); CurrentPos += GetSizeForResDict(model.VtxClrData.Count); } if (model.VtxTexCoordData.Count > 0) { OffsetMap.Add(CurrentPos, "ResDict: VertexTexCoordData"); CurrentPos += GetSizeForResDict(model.VtxTexCoordData.Count); } OffsetMap.Add(CurrentPos, "ResDict: Materials"); CurrentPos += GetSizeForResDict(model.Materials.Count); OffsetMap.Add(CurrentPos, "ResDict: Shaders"); CurrentPos += GetSizeForResDict(model.Shaders.Count); OffsetMap.Add(CurrentPos, "ResDict: Shapes"); CurrentPos += GetSizeForResDict(model.Shapes.Count); if (model.PairingLookupByTexture.Count > 0) { OffsetMap.Add(CurrentPos, "ResDict: Texture Lookup"); 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); CalculateShaders(model); CalculateShapes(model); CalculateVtxPosData(model); CalculateVtxNrmData(model); CalculateVtxClrData(model); CalculateVtxTexCoordData(model); } } private void CalculatePairings(Model m, ResDict> dict) { foreach (var kv in dict) { OffsetMap.Add(CurrentPos, "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; OffsetMap.Add(CurrentPos, "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); OffsetMap.Add(CurrentPos, "Node: " + kv.Key); NodeOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0xD0; } } private void CalculateMaterials(Model m) { foreach (var kv in m.Materials) { AddString(kv.Key); OffsetMap.Add(CurrentPos, "Material: " + kv.Key); MaterialOffsets.Add(kv.Value, CurrentPos); // Base material struct CurrentPos += 0x14; // ResGenMode CurrentPos += 8; // ResMatMode CurrentPos += 0xC; // other stuff CurrentPos += 0x18; // ResTexObj CurrentPos += 0x104; // ResTlutObj CurrentPos += 0x64; // ResTexSrt CurrentPos += 8 + (0x14 * 8) + (0x34 * 8); // ResMatChan CurrentPos += 0x28; // Texture Infos if (kv.Value.TextureInfos.Count > 0) OffsetMap.Add(CurrentPos, "Material Texture Infos: " + kv.Key); CurrentPos += (kv.Value.TextureInfos.Count * 0x34); // Display Lists AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "Material Display Lists: " + kv.Key); CurrentPos += 0x20 + 0x80 + 0x40 + 0xA0; } } private void CalculateShaders(Model m) { foreach (var kv in m.Shaders) { AddString(kv.Key); OffsetMap.Add(CurrentPos, "Shader: " + kv.Key); ShaderOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0x200; } } private void CalculateShapes(Model m) { foreach (var kv in m.Shapes) { AddString(kv.Key); OffsetMap.Add(CurrentPos, "Shape: " + kv.Key); ShapeOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0x68; AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "Shape DL 1: " + kv.Key); CurrentPos += (int)kv.Value.DLBufferSize1; AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "Shape DL 2: " + kv.Key); CurrentPos += (int)kv.Value.DLBufferSize2; AlignCalcPos(0x20); } } private void CalculateVtxPosData(Model m) { foreach (var kv in m.VtxPosData) { AddString(kv.Key); OffsetMap.Add(CurrentPos, "VertexPosData: " + kv.Key); VtxPosOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; // Minimum/maximum VEC3 (specific to VtxPosData) CurrentPos += 0x18; AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "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); OffsetMap.Add(CurrentPos, "VertexNrmData: " + kv.Key); VtxNrmOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "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); OffsetMap.Add(CurrentPos, "VertexClrData: " + kv.Key); VtxClrOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "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); OffsetMap.Add(CurrentPos, "VertexTexCoordData: " + kv.Key); VtxTexCoordOffsets.Add(kv.Value, CurrentPos); // Main data CurrentPos += 0x20; // Minimum/maximum VEC2 (specific to VtxTexCoordData) CurrentPos += 0x10; AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "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); OffsetMap.Add(CurrentPos, "Texture: " + kv.Key); TextureOffsets.Add(kv.Value, CurrentPos); CurrentPos += 0x30; AlignCalcPos(0x20); OffsetMap.Add(CurrentPos, "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 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 StringTableOffsets; 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() { StringTable.Sort(); StringTableOffsets = new Dictionary(); StringTableData = new OutputStream(ByteEndian.BigEndian); foreach (var s in StringTable) { // Add 4 because the Names are referenced by the string data, and not by the ResName struct itself StringTableOffsets[s] = StringTableData.Position + 4; StringTableData.WriteName(s); } } #endregion } }